详解kubelet 创建pod流程代码图解及日志说明

目录
  • 正文
  • kubernetes调度pod简介
  • kubelet 创建pod代码及图解说明
    • kubelet 简介
  • kubelet创建及启动pod流程
    • kubelet 创建pod代码调用图解
    • kubelet 创建pod详细说明
    • kubelet 调用cri说明
    • kubelet创建pod整体架构图
  • kubelet创建pod日志说明

正文

本文将从如下方面介绍kubelet创建pod的过程

  • kubernetes调度pod简介
  • kubelet 创建pod代码图解说明 (本文重点)
  • kubelet 调用cri创建容器说明 (本文重点)
  • 通过日志来分析kubelet真实创建日志的全过程 (本文重点)

kubernetes调度pod简介

kubernetes(后面简称k8s)主要有三种管理(创建)pod的方式:

  • 一种是直接申明创建一个裸pod
  • 另一种是通过controller 来申明创建pod:比如,deployments、replicationcontrollers、daemonsets或者replicasets
  • 还有一种是static(静态) pod 这种用的比较少,一般是把pod的申明文件放在对应的kubernetes/manifest 目录下,通常用来创建apiserver,controller-manager,scheduler这类k8s管理组件的pod。

k8s推荐使用controller来管理pod,这符合k8s管理pod的习惯,便于使用k8s相关功能,比如弹性扩缩容,pod故障自动拉起等。 我们也以controller管理的pod为例,简单梳理下k8s创建及调度pod流程,如下图

  • 客户端请求apiserver创建replicasets,apiserver通过认证、鉴权、准入后,会把请求相关信息持久化至etcd
  • Controller-manager 管理的replicaset controller 通过list-watch机制,watch到有replicasets创建请求,通过label selector发现集群中与这个replicasets 关联的pod当前状态与期望状态不一致,则会进行调协(reconcile)向apiserver发起创建pod请求
  • Scheduler 通过list-watch机制来发现未绑定的pod,并通过预选及优选策略算法,来计算出pod最终可调度的node节点,并通过apiserver将数据更新至etcd
  • Kubelet 通过list-watch发现有新的pod bound到本node上,则会发起创建pod相关流程

kubelet 创建pod代码及图解说明

kubelet 简介

Kubelet 有点和controller类似,也是通过list-watch相关信息,或者轮询本地pod相关信息及事件,来触发相关动作,使pod处于”期望状态”,并且向apiserver上报本node(宿主机)及node里所有pod的状态信息。

kubelet 不同于其他controller的一点就是,它是部署在每个node节点上的agent,它需要与apiserver 打交道同样也需要与cri(contain-runtime-interface)打交道来管理node上的容器。所以它需要通过apiserver来watch到对本地pod变更的事件,也需要不断轮询pod状态信息,将状态及时同步给apiserver,所以Kubelet整体工作逻辑是loop监听各类生产者产生的消息或者定时触发消息,来调用相应的消费者(不同的子模块)完成不同的操作,比如watch 到apiserver的请求,PLEG(pod lifecycle event generator)产生的事件,定时触发的任务等

kubelet创建及启动pod流程

kubelet 创建pod代码调用图解

kubelet 创建pod详细说明

  • 1.kubelet 会listwatch所有namespace下、绑定到本node上的pod,并将信息传入updatechannel。kubelet 的SyncLoop(是kubele的主循环函数,来控制例行循环往复的事情:同步接收、更新、处理pod变更相关信息)下的syncLoopIteration方法会监听多方消息,会监听各个消息源,来触发相应的操作,这个方法会接收前面listwatch到的updatechannel信息,交由对应的handler:如pod创建:调用HandlePodAdditions处理,pod删除调用HandlePodUpdates处理(DELETE is treated as a UPDATE because of graceful deletion.)
  • 2.HandlePodAdditions 会对pods 进行排序,判断,准入校验,之后调用dispatchWork 把对某个pod的操作 分配给 podWorkers 做异步操作(pod创建、删除、更新)处理
  • 3.异步操作会调用kubelet syncPod(syncPod is the transaction script for the sync of a single pod.)方法,syncPod会做一些pod创建前的准备工作

a.如果pod updateType 为podkill,立即执行并返回(走pod删除流程)

b.pod准入检查检查pod是否能运行在本节点

c.更新状态给 status manager ,status manager将pod状态上报给apiserver

d.检查网络插件是否就绪

e.创建并更新pod cgroups配置

f.为pod创建对应的目录:pod目录,volume目录

g.等待pod sepc中的volme都被attach/mount

h.从apiserver中获取pull secrets

i.调用 containerRuntime 的 SyncPod 方法开始创建容器
复制代码

  • 4.containerRuntime 的 SyncPod 会做如下主要工作

a.创建sandbox

b.Create ephemeral containers

c.Create init containers

d.Create normal containers
复制代码

其中创建sandbox是关键,sandbox可以理解为pod的运行环境,是业务pod的父容器,在k8s里就是pause 容器,所有容器创建前都需要创建pause容器。首先会生成podsandbox相关配置:如dnsconfig,podhostname,设置sysctl,cgroups以及namespace

然后会调用CRI(container-runtime-interface)来调用底层container runtime来真实操作容器,之后还会调用CNI插件来为容器设置网络。

  • 5.我们再来看下创建sandbox:RunpodSandbox的步骤 (ds *dockerService) RunPodSandbox 是在是一个cri的是实现,所以在dockershim下dockershim是内置在kubelet里的cri实现,用来衔接kubelet与docker,dockershim翻译为docker"垫片",很形象)。kubelet通过grp call调用的dockershim来实现容器的创建管理。

a.调用docker API Pull the image for the sandbox.
(kubelet 的sandbox镜像:defaultSandboxImage = "k8s.gcr.io/pause:3.2")

b. 调用docker Create the sandbox container.

c.Create Sandbox Checkpoint.

d.调用docker Start the sandbox container.

e.Rewrite resolv.conf file generated by docker.

f. Setup networking for the sandbox. 调用cni插件为容器设置网络

kubelet 调用cri说明

我们目前container-runtime为docker,docker并不支持CRI,所以要想调用docker 操作容器,k8s内置了dockershim来调用docker,dockershim可以理解为一个满足CRI标准的容器运行时,kubelet通过grpc call 来调用dockershim,dockershim收到kubelet的请求后,将其转化为REST API请求,再发送给docker daemon,docker daemon 在通过组装请求,调用docker API来完成container的最终创建、启动等相关操作。

这块有两个地方需要说明下:

1是为啥会有dockershim? 这里有个小故事,首先k8s再具有一定市场规模后,想与docker 解耦,不想强依赖docker,同时为了支持多种container-runtime,故制定了CRI,只有满足CRI,kubelet便可以直接完成调用来管理container,然而docker一开始并不支持CRI,故k8s想了个这种的方式,开发了一个dockershim(docker "垫片")来转发请求,这样k8s也完成了对docker的解耦,当然这看起来较繁琐且影响性能,故在kubernetes 1.24后,kubernetes宣布启用dockershim,需要我们在该版本后主动配置container-runtime。

2.docker这面也很早就做了应对,docker抽离出了支持CRI标准的containerd,通过containerd来管理容器。

所以如下图,调用docker API创建容器后,docker还会调用docker-containerd来管理创建容器,docker-containerd通过docker-containerd-shim来间接管理container,这样一个好处就是升级或重启docker,我们的业务容器依然可以正常运行,最终docker-containerd-shim通过runc来创建container,runc是docker做的基于oci的实现就是以前的libcontainer,用于容器创建。

kubelet创建pod整体架构图

(container-runtime="docker",大多数企业目前应该都是使用的这种方式)

kubelet创建pod日志说明

我们通过实战,开启debug日志来看下kubelet在创建pod时做了哪些工作

注:日志仅保留主要输出及过滤敏感信息

1.收到新pod创建时间,写入updatechannel通道
I0921 18:10:00.486345   26075 config.go:414] Receiving a new pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"

2.syncLoop: 收到add事件
I0921 18:10:00.757557   26075 kubelet.go:2007] SyncLoop (ADD, "api"): opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)

3.准入验证pod fit success
I0921 18:10:00.759786   26075 predicates.go:986] Pod: opslk1-xxx fit success. Node: xx.xx.10.9 has enough resources.

4.流转至syncPod,SyncPodType=create
I0921 18:10:00.759956   26075 kubelet.go:1498] syncPod "xxx-3995-11ed-80a8-48df37244930" updateType:{{ }  types.SyncPodType=create)

5.获取pod状态
I0921 18:10:00.760128   26075 kubelet_pods.go:1529] Generating status for "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"
I0921 18:10:00.760148   26075 kubelet_pods.go:1494] pod waiting > 0, pending
I0921 18:10:00.760174   26075 kubelet.go:1603] apiPodStatus.Phase:Pending pod:"opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"

6.配置cgroupConfig,设置cpu,内存
I0921 18:10:00.760200   26075 kubelet_resources.go:149] Newest cgroupConfig for pod:"opslk1-5sfjn_lktest01(739e1c1a-3175-11ed-aff8-48df37244926)"
are kubelet.cgroupResource{cpuShares:xxx, cpuQuota:xxx, memoryLimit:xxx, memoryLimitSwap:xxx}.

7.等待pod相关volume attach及挂载
I0921 18:10:00.768211   26075 volume_manager.go:350] Waiting for volumes to attach and mount for pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"

8.向apiserver同步状态,先GET后PATCH
I0921 18:10:00.791361   26075 round_trippers.go:419] curl -k -v -XGET   'https://xxx/api/v1/namespaces/lktest01/pods/opslk1-xxx'
I0921 18:10:00.794250   26075 round_trippers.go:419] curl -k -v -XPATCH  'https://xxx/api/v1/namespaces/lktest01/pods/opslk1-xxx/status'
I0921 18:10:00.798998   26075 status_manager.go:506] Status for pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)" updated successfully: (1, {Phase:Pending Conditions:[{Type:Initialized

9.根据期望状态开始调协,Reconcile Pod "Ready" condition if necessary. Trigger sync pod for reconciliation.
I0921 18:10:00.799365   26075 kubelet.go:2020] SyncLoop (RECONCILE, "api"): "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"

10.mount volume
I0921 18:10:02.177479   26075 operation_generator.go:506] MountVolume.WaitForAttach succeeded for volume "volume"  DevicePath "/dev/mapper/docker-xxx_3995_11ed_80a8_48df37244930"
I0921 18:10:03.136754   26075 operation_generator.go:527] MountVolume.MountDevice succeeded for volume "volume"  device mount path "/export/kubelet/pods/xxx-3995-11ed-80a8-48df37244930/volumes/kubernetes.io~lvm/volume"
I0921 18:10:03.136851   26075 operation_generator.go:567] MountVolume.SetUp succeeded for volume "volume" (UniqueName: "flexvolume-kubernetes.io/lvm/xxx_3995_11ed_80a8_48df37244930") pod "opslk1-xxx"

11.volumes  attached、mounted 完毕
I0921 18:10:03.168555   26075 volume_manager.go:384] All volumes are attached and mounted for pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"

12.调用 containerRuntime 的 SyncPod 方法开始创建容器
I0921 18:10:03.168568   26075 kuberuntime_manager.go:468] Syncing Pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)": &Pod{}

13.创建sandbox容器:Setting cgroup parent,RunPodSandbox,Calling network plugin cni to set up pod
I0921 18:10:03.168833   26075 kuberuntime_manager.go:398] No sandbox for pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)" can be found. Need to start a new one"opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"
I0921 18:10:03.168885   26075 kuberuntime_manager.go:605] SyncPod received new pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)", will create a sandbox for it
I0921 18:10:03.168891   26075 kuberuntime_manager.go:614] Stopping PodSandbox for "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)", will start new one
I0921 18:10:03.168901   26075 kuberuntime_manager.go:841] Stop app containers for pod:"opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)".
I0921 18:10:03.168913   26075 kuberuntime_manager.go:666] Creating sandbox for pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"
I0921 18:10:03.170818   26075 docker_service.go:460] Setting cgroup parent to: "/kubepods/burstable/podxxx-3995-11ed-80a8-48df37244930"
I0921 18:10:03.170827   26075 docker_sandbox.go:108] RunPodSandbox PodName:opslk1-xxx PodUID:xxx-3995-11ed-80a8-48df37244930 NameSpace:lktest01
I0921 18:10:04.297831   26075 plugins.go:377] Calling network plugin cni to set up pod "opslk1-xxx_lktest01"
I0921 18:10:04.298323   26075 manager.go:1011] Added container: "/kubepods/burstable/podxxx-3995-11ed-80a8-48df37244930/805dda102e017247685240c2f740295396edcb7071dfe211979215eac0870e0b" 
I0921 18:10:04.298535   26075 container.go:448] Start housekeeping for container "/kubepods/burstable/podxxx-3995-11ed-80a8-48df37244930/805dda102e017247685240c2f740295396edcb7071dfe211979215eac0870e0b"
I0921 18:10:04.298693   26075 cni.go:337] Got netns path /proc/26876/ns/net
I0921 18:10:04.298701   26075 cni.go:338] Using podns path lktest01
I0921 18:10:04.298820   26075 cni.go:307] About to add CNI network cni-loopback (type=loopback)
I0921 18:10:04.301399   26075 cni.go:337] Got netns path /proc/26876/ns/net
I0921 18:10:04.301405   26075 cni.go:338] Using podns path lktest01
I0921 18:10:04.301466   26075 cni.go:307] About to add CNI network cni (type=cni)
I0921 18:10:04.392172   26075 kuberuntime_manager.go:680] Created PodSandbox "805dda102e017247685240c2f740295396edcb7071dfe211979215eac0870e0b" for pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)"
I0921 18:10:04.396981   26075 kuberuntime_manager.go:699] Determined the ip "xx.xx.226.17" for pod "opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)" after sandbox changed

14,创建常规容器
I0921 18:10:04.397114   26075 kuberuntime_manager.go:750] Creating container &Container{} in pod opslk1-xxx_lktest01(xxx-3995-11ed-80a8-48df37244930)
I0921 18:10:04.398859   26075 kuberuntime_container.go:108] Generating ref for container opslk: &v1.ObjectReference{Kind:"Pod", Namespace:"lktest01", Name:"opslk1-xxx"}
I0921 18:10:04.398883   26075 kuberuntime_container.go:117] To determine whether to restart the old container. Pod:opslk1-xxx_lktest01 PodIP: PodSandboxId: NameSpace:lktest01
I0921 18:10:04.398888   26075 kuberuntime_container.go:258] pod:opslk1-xxx default KeepRootDirForPod: true
I0921 18:10:04.398935   26075 server.go:471] Event(v1.ObjectReference{Kind:"Pod", Namespace:"lktest01", Name:"opslk1-xxx", UID:"xxx-3995-11ed-80a8-48df37244930", APIVersion:"v1", ResourceVersion:"19846024411", FieldPath:"spec.containers{opslk}"})

以上就是详解kubelet 创建pod流程代码图解及日志说明的详细内容,更多关于kubelet创建pod流程的资料请关注我们其它相关文章!

(0)

相关推荐

  • 替代pod update速度慢的lg_pod_plugin安装使用详解

    目录 1. 安装方式 2. 如何使用lg_pod_plugin 3. 工作原理 1. 安装方式 推荐使用bundle 安装lg_pod_plugin , 免去手动安装 gem install lg_pod_plugin , 方便后续升级lg_pod_plugin版本, 适合团队开发, 总不能让所有人在自己电脑上都安装一次 lg_pod_plugin吧. 创建 Gemfile 文件 bundle init #初始化一个bundle 环境, 类似于pod init 编写Gemfile 文件, 类似于

  • cordon节点drain驱逐节点delete节点详解

    目录 一.系统环境 二.前言 三.cordon节点 3.1 cordon节点概览 3.2 cordon节点 3.3 uncordon节点 四.drain节点 4.1 drain节点概览 4.2 drain 节点 4.3 uncordon节点 五.delete 节点 5.1 delete节点概览 5.2 delete节点 一.系统环境 服务器版本 docker软件版本 Kubernetes(k8s)集群版本 CPU架构 CentOS Linux release 7.4.1708 (Core) Do

  • 静态pod 创建使用示例详解

    目录 一.系统环境 二.前言 三.静态pod 3.1 何为静态pod 3.2 创建静态pod 3.2.1 使用--pod-manifest-path指定静态pod目录 3.2.2 静态pod默认目录/etc/kubernetes/manifests 一.系统环境 服务器版本 docker软件版本 Kubernetes(k8s)集群版本 CPU架构 CentOS Linux release 7.4.1708 (Core) Docker version 20.10.12 v1.21.9 x86_64

  • pod污点taint 与容忍度tolerations详解

    目录 一.系统环境 二.前言 三.污点taint 3.1 污点taint概览 3.2 给节点添加污点taint 四.容忍度tolerations 4.1 容忍度tolerations概览 4.2 设置容忍度tolerations 一.系统环境 服务器版本 docker软件版本 Kubernetes(k8s)集群版本 CPU架构 CentOS Linux release 7.4.1708 (Core) Docker version 20.10.12 v1.21.9 x86_64 Kubernete

  • k8s 中的 service 如何找到绑定的 Pod 及实现 Pod 负载均衡的方法

    目录 k8s 中的 service 如何找到绑定的 Pod 以及如何实现 Pod 负载均衡 前言 endpoint kube-proxy userspace 模式 iptables ipvs kernelspace 服务发现 环境变量 DNS 总结 参考 k8s 中的 service 如何找到绑定的 Pod 以及如何实现 Pod 负载均衡 前言 Service 资源主要用于为 Pod 对象提供一个固定.统一的访问接口及负载均衡的能力. service 是一组具有相同 label pod 集合的抽

  • pod调度将 Pod 指派给节点

    目录 一.系统环境 二.前言 三.pod的调度 3.1 pod的调度概述 3.2 pod自动调度 3.2.1 创建3个主机端口为80的pod 3.3 使用nodeName 字段指定pod运行在哪个节点 3.4 使用节点标签nodeSelector指定pod运行在哪个节点 3.4.1 查看标签 3.4.2 创建标签 3.4.3 通过标签控制pod在哪个节点运行 3.5 使用亲和性与反亲和性调度pod 3.5.1 使用硬策略requiredDuringSchedulingIgnoredDuringE

  • 详解kubelet 创建pod流程代码图解及日志说明

    目录 正文 kubernetes调度pod简介 kubelet 创建pod代码及图解说明 kubelet 简介 kubelet创建及启动pod流程 kubelet 创建pod代码调用图解 kubelet 创建pod详细说明 kubelet 调用cri说明 kubelet创建pod整体架构图 kubelet创建pod日志说明 正文 本文将从如下方面介绍kubelet创建pod的过程 kubernetes调度pod简介 kubelet 创建pod代码图解说明 (本文重点) kubelet 调用cri

  • 详解Spring 拦截器流程及多个拦截器的执行顺序

    拦截器是 Spring MVC 中的组件,它可以在进入请求方法前做一些操作,也可以在请求方法后和渲染视图后做一些事情. 拦截器的定义 SpringMVC 的拦截器只需要实现 HandlerInterceptor 接口,并进行配置即可.HandlerInterceptor 接口的定义如下: public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletRe

  • Spring详解使用注解开发流程

    目录 在Spring4之后 要使用注解开发 必须保证aop包导入了 使用注解需要导入context约束 增加 注解的支持 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance

  • 详解Go 创建命令行工具的方法

    前言 最近因为项目需要写了一段时间的 Go ,相对于 Java 来说语法简单同时又有着一些 Python 之类的语法糖,让人大呼"真香". 但现阶段相对来说还是 Python 写的多一些,偶尔还得回炉写点 Java :自然对 Go 也谈不上多熟悉. 于是便利用周末时间自己做个小项目来加深一些使用经验.于是我便想到了之前利用 Java 写的一个博客小工具. 那段时间正值微博图床大量图片禁止外链,导致许多个人博客中的图片都不能查看.这个工具可以将文章中的图片备份到本地,还能将图片直接替换到

  • 详解如何创建Python元类

    什么是Python元类? Python元类是与Python的面向对象编程概念相关的高级功能之一.它确定类的行为,并进一步帮助其修改. 用Python创建的每个类都有一个基础的Metaclass.因此,在创建类时,您将间接使用元类.它隐式发生,您无需指定任何内容. 与元编程相关联的元类决定了程序对其自身进行操作的能力. 学习元类可能看起来很复杂,但是让我们先从一些类和对象的概念入手,以便于理解. Python中的类和对象 类是一个蓝图,是具有对象的逻辑实体. 一个简单的类在声明时没有分配任何内存,

  • 详解Java中的流程控制

    1.分支结构的概念 当需要进行条件判断并做出选择时,使用分支结构 2.if分支结构 格式: if(条件表达式){ 语句块; } package com.lagou.Day04; import java.util.Scanner; /** * 编程使用if分支结构模拟网吧上网的过程 */ public class Demo01 { public static void main(String[] args) { //1.提示用户输入年龄信息并使用变量记录 System.out.println("请

  • 详解在VScode中添加代码块(含C++指令生成代码)

    有神马用? 能够填充预设的代码 也就是当你输入一些语句时,能够自动补全一堆代码 如图: 这就可以补全一些你的模板之类的了例如当我输入MST,我希望得到一大块最小生成树的模板.简直是竞赛党必备啊hhh 步骤如何? 首先你要有VScode 在哪创建 看图 C++是世界上最好的语言,所以我选择C++ 其他语言一个道理 接着不出意外你会看到这个页面 怎么创建 具体原理就是在行头行尾加上一些符号,中间的逃逸字符和引号转义 下面给出代码,自行创建 注意*.in文件应该和下面的代码放在同一目录之下 根据需求改

  • 详解_beginthreadex()创建线程

    目录 一.使用_beginthreadex() 二._beginthreadex()与代CreateThread()区别 一.使用_beginthreadex() 需要的头文件支持#include         // for _beginthread()需要的设置:ProjectàSetting-->C/C++-->User run-time library 选择Debug Multithreaded 或者Multithreaded.即使用: MT或MTD. 代码如下:      #incl

  • 详解Java创建线程的五种常见方式

    目录 Java中如何创建线程呢? 1.显示继承Thread,重写run来指定现成的执行代码. 2.匿名内部类继承Thread,重写run来执行线程执行的代码. 3.显示实现Runnable接口,重写run方法. 4.匿名内部类实现Runnable接口,重写run方法 5.通过lambda表达式来描述线程执行的代码 [面试题]:Thread的run和start之间的区别? Thread类的具体用法 Thread类常见的一些属性 中断一个线程 1.方法一:让线程run完 2.方法二:调用interr

  • 详解Java中KMP算法的图解与实现

    目录 图解 代码实现 图解 kmp算法跟之前讲的bm算法思想有一定的相似性.之前提到过,bm算法中有个好后缀的概念,而在kmp中有个好前缀的概念,什么是好前缀,我们先来看下面这个例子. 观察上面这个例子,已经匹配的abcde称为好前缀,a与之后的bcde都不匹配,所以没有必要再比一次,直接滑动到e之后即可. 那如果好前缀中有互相匹配的字符呢? 观察上面这个例子,这个时候如果我们直接滑到好前缀之后,则会过度滑动,错失匹配子串.那我们如何根据好前缀来进行合理滑动? 其实就是看当前的好前缀的前缀和后缀

随机推荐