☰
一次 kubectl apply 到底发生了什么?六阶段全链路拆解(底座认知 08·讲解篇)
2026/11/5 1:53:49 网站建设 项目流程
我们每天都在敲kubectl apply -f xxx.yaml,然后看到一个 Pod 从Pending变成Running。中间那几秒到底发生了什么?搞清楚这条链路,你排障的思路会从"猜"变成"顺着链条定位"。这篇文章按时间顺序把它拆成六段。

目录

  • 一、先看全景:六阶段链路
  • 二、阶段①:kubectl 不是"下发命令",而是"提交期望"
  • 三、阶段②:apiserver 的三道关卡
  • 四、阶段③:控制器接力,把 Deployment 变成 Pod
  • 五、阶段④:调度器挑一台机器
  • 六、阶段⑤:kubelet 让容器真正跑起来
  • 七、阶段⑥:kube-proxy 让服务可访问
  • 八、Pod 卡住了?按阶段查
  • 九、为什么说这是"收敛",不是"一次性命令"
  • 十、小结

一、先看全景:六阶段链路

阶段主角干了什么
①kubectl读 YAML,发 HTTP 请求
②kube-apiserver认证、授权、准入,写入 etcd
③controller-managerDeployment 创建 ReplicaSet,ReplicaSet 创建 Pod
④kube-scheduler选节点,写绑定关系
⑤kubelet+ containerd拉镜像、建容器、探针检查
⑥kube-proxy更新 iptables/IPVS 规则,Service 可访问

下面逐段拆。

二、阶段①:kubectl 不是"下发命令",而是"提交期望"

kubectl做的事比想象中简单:把 YAML 转成 JSON,向 apiserver 发一个 HTTP 请求。

但这里有一个常被忽略的细节——apply 之前,kubectl 会先 GET 一次现有对象。如果对象不存在,就发POST(创建);如果存在,就计算差异,发PATCH(更新)。

apply → GET(存在吗?) → 不存在:POST → 存在: PATCH(带 last-applied-configuration 做三方合并)

这就解释了两个常见现象:

  • create重复执行会报AlreadyExists,apply不会
  • 手动kubectl edit改过的字段,可能在下一次apply时被改回去,或者产生冲突——因为 kubectl 按"我上次提交的"和"集群现在的"做三方合并

另外别忘了,kubectl 需要知道自己连哪个集群、用什么身份:

kubectl config view --minify # 看当前 context、server、证书 kubectl auth whoami # 看当前身份(1.27+)

第一步错误通常出在这里:连错集群、证书过期、context 不对。"kubectl 连不上"和"集群坏了"是两件完全不同的事,先分清。

三、阶段②:apiserver 的三道关卡

apiserver 是唯一能读写 etcd 的组件,所有请求都要过它。它像一道安检门,设了三道关卡:

关卡解决什么问题失败表现
认证 Authentication你是谁Unauthorized (401)
授权 Authorization你能不能做这件事Forbidden (403),提示 RBAC 拒绝
准入 Admission这个对象合不合规明确报错,如超出配额、镜像不在白名单

认证

常见方式两种:

  • X.509 客户端证书:kubeconfig 里常见的client-certificate-data,证书里的 CN 是用户名,O 是用户组
  • Bearer Token:ServiceAccount 的 token,或云厂商的临时凭证

授权

默认是RBAC:用户/ServiceAccount → RoleBinding → Role/ClusterRole → 允许的动词和资源。

排查 403 的标准动作:

kubectl auth can-i create pods --namespace default kubectl auth can-i --list --namespace default

准入

准入控制器在对象真正落库前做校验或改写,常见的有:

  • ResourceQuota:命名空间的总资源配额
  • LimitRange:没写 resources 时给默认值,或强制要求写
  • Pod Security Admission:限制 privileged、hostPath 等危险配置
  • Mutating/Validating Webhook:各种自定义校验

到这一步,对象终于被写入etcd——集群唯一的事实来源。写入成功,kubectl就返回了deployment.apps/xxx created。

注意:此时容器一个都还没启动。kubectl说的"成功"只是"期望状态已记录"。

四、阶段③:控制器接力,把 Deployment 变成 Pod

etcd 里多了一个 Deployment 对象,但 Deployment 本身不产生 Pod。

kube-controller-manager里的 Deployment 控制器一直在watch这类对象。它发现"期望 3 个副本,实际 0 个",于是:

  1. 创建(或更新)一个ReplicaSet,把 Pod 模板交给它
  2. ReplicaSet 控制器接着 watch,发现"期望 3 个,实际 0 个",创建 3 个Pod对象

为什么中间要夹一层 ReplicaSet?因为版本管理:每次改镜像,Deployment 会新建一个 ReplicaSet,旧的保留(revisionHistoryLimit控制保留几个),滚动更新就是两个 RS 此消彼长的过程,回滚就是把旧 RS 扩回去。

kubectl get rs -o wide # 看每个 RS 的 DESIRED / CURRENT kubectl rollout history deploy/xxx kubectl rollout undo deploy/xxx --to-revision=1

到这里,3 个 Pod 对象存在于 etcd 里,但nodeName是空的——它们还没有归属任何节点,状态是Pending。

五、阶段④:调度器挑一台机器

kube-schedulerwatch 到"有 Pod 没有 nodeName",开始为它选节点,分三步:

步骤目的典型约束
Filter(预选)筛掉不可能运行的节点资源不足、taint 不容忍、nodeSelector/affinity 不匹配、端口冲突
Score(优选)在可行节点里打分排序资源均衡、亲和性权重、镜像本地已存在
Bind(绑定)把结果写回 Pod更新spec.nodeName

Filter 阶段全军覆没,Pod 就一直是Pending,事件里会写原因,例如:

0/3 nodes are available: 3 Insufficient memory. 0/3 nodes are available: 3 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }.

看到 Pending 的第一反应应该是kubectl describe pod看 Events 最后一行,而不是重启集群。

还要注意:调度只是"分配",不等于"启动"。绑定完成后 Pod 状态可能还是 Pending,因为节点上还没开始拉镜像。

六、阶段⑤:kubelet 让容器真正跑起来

节点上的 kubelet watch 到"分给我的 Pod",开始干活:

  1. 通过 CRI 调用 containerd:RunPodSandbox→ 先建 pause 容器和 Pod 级 Namespace
  2. 拉镜像:没有就拉,拉不到就ImagePullBackOff
  3. 建业务容器:加入 pause 的命名空间,挂 Volume,按 cgroup 设资源限制
  4. 执行探针:startup → readiness → liveness
  5. 上报状态:定期向 apiserver 汇报,节点心跳断了,节点会变NotReady,上面的 Pod 会被驱逐

这条链路上每一环失败,症状都不一样:

症状卡在哪
ContainerCreating长时间不动拉镜像慢、挂载 Volume 失败(CSI 问题)
ImagePullBackOff镜像地址错、私有仓库凭据缺失、网络不通
CrashLoopBackOff容器启动了但反复退出,看logs --previous
Running但READY 0/1readiness 探针没通过,Service 不会把流量给它

kubelet 与 apiserver 之间是双向的:kubelet 主动 watch 分配给自己节点的 Pod,也主动上报状态。apiserver 不会主动连 kubelet——这一点决定了排查方向:节点上的 kubelet 日志往往比 apiserver 日志更有用。

七、阶段⑥:kube-proxy 让服务可访问

Pod 起来了,但 Pod IP 会变,没人能直接依赖它。于是有了 Service。

kube-proxy在每个节点上 watch Service 和 EndpointSlice,把"访问 Service IP:Port → 转发到某个 Pod IP:Port"的规则写进内核:

模式实现适用
iptables(默认)规则链 + DNAT通用,Pod 数量中等
IPVS内核四层负载均衡,哈希表大规模、高并发,规则多时性能更好

Service 四种类型:

类型作用
ClusterIP集群内虚拟 IP,默认类型
NodePort每个节点开一个端口,外部可通过节点IP:端口访问
LoadBalancer云厂商提供外部负载均衡器
ExternalName不做转发,只做 DNS CNAME

排查"服务访问不通",顺序是:

kubectl get endpointslices -l kubernetes.io/service-name=xxx # 有没有后端 kubectl get pods -o wide # Pod 是否 Ready kubectl exec -it <pod> -- curl http://<service-ip>:<port> # 集群内通不通

最常见的坑:Pod 没 Ready,Endpoints 是空的,Service 自然转发不到任何地方。这时候别去翻 iptables,先看探针。

八、Pod 卡住了?按阶段查

把上面六个阶段变成一张排障表:

现象大概率阶段第一条命令
kubectl超时、连不上①kubectl cluster-info、检查 kubeconfig
401 Unauthorized② 认证检查证书是否过期
403 Forbidden② 授权kubectl auth can-i ...
创建报配额/校验错误② 准入看错误详情里的 quota/webhook 名
Pod 一直Pending,无节点④ 调度kubectl describe pod看 Events
Pending但有 nodeName⑤ kubeletjournalctl -u kubelet -f
ImagePullBackOff⑤kubectl describe pod看拉取错误
CrashLoopBackOff⑤kubectl logs --previous
Running但访问不通⑥kubectl get endpointslice

一句话总结排障心法:先确定 Pod 卡在哪个状态,再对应到哪个阶段,最后只看那个阶段的日志。不要一上来就kubectl logs,那时候容器可能根本还没建。

九、为什么说这是"收敛",不是"一次性命令"

上面整条链路的驱动力,是controller-manager 的 Reconcile 循环:

for { 期望状态 = 从 etcd 读到的 spec 实际状态 = 从集群观察到的 status if 期望 != 实际 { 执行动作,让实际逼近期望 } }

这个循环永不停歇。所以:

  • 你删掉一个 Pod,ReplicaSet 发现副本数不足,立刻补一个
  • 你把节点关机,控制器发现 Pod 不在,重新创建并调度到别的节点
  • 你改了镜像版本,Deployment 创建新 RS,滚动替换

这就是"声明式"的本质:你只描述想要什么,系统自己想办法达到,并持续保持。

也正因为如此,K8s 里没有"重启服务"这个说法——你把 Pod 删了,它自己就回来了;你改配置,它自己就滚动了。命令式思维在这里会处处别扭。

十、小结

  1. kubectl apply提交的是期望状态,不是启动命令
  2. apiserver 三道关卡:认证(你是谁)→ 授权(能不能做)→ 准入(合不合规),通过后写 etcd
  3. 控制器负责"从 Deployment 到 Pod"的展开,中间夹一层 ReplicaSet 是为了版本与回滚
  4. 调度器 Filter → Score → Bind,Pending先看 Events
  5. kubelet 通过 CRI 建 pause 沙箱、拉镜像、起容器、跑探针、上报状态
  6. kube-proxy 把 Service 规则写进内核,Pod 不 Ready 就没有 Endpoints
  7. 整条链路是持续收敛的闭环,不是一个一次性命令

把这六个阶段记在脑子里,下次 Pod 起不来,你会知道该看哪一层,而不是盲目重启。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询