我们每天都在敲kubectl apply -f xxx.yaml,然后看到一个 Pod 从Pending变成Running。中间那几秒到底发生了什么?搞清楚这条链路,你排障的思路会从"猜"变成"顺着链条定位"。这篇文章按时间顺序把它拆成六段。
目录
- 一、先看全景:六阶段链路
- 二、阶段①:kubectl 不是"下发命令",而是"提交期望"
- 三、阶段②:apiserver 的三道关卡
- 四、阶段③:控制器接力,把 Deployment 变成 Pod
- 五、阶段④:调度器挑一台机器
- 六、阶段⑤:kubelet 让容器真正跑起来
- 七、阶段⑥:kube-proxy 让服务可访问
- 八、Pod 卡住了?按阶段查
- 九、为什么说这是"收敛",不是"一次性命令"
- 十、小结
一、先看全景:六阶段链路
| 阶段 | 主角 | 干了什么 |
|---|---|---|
| ① | kubectl | 读 YAML,发 HTTP 请求 |
| ② | kube-apiserver | 认证、授权、准入,写入 etcd |
| ③ | controller-manager | Deployment 创建 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 个",于是:
- 创建(或更新)一个ReplicaSet,把 Pod 模板交给它
- 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",开始干活:
- 通过 CRI 调用 containerd:
RunPodSandbox→ 先建 pause 容器和 Pod 级 Namespace - 拉镜像:没有就拉,拉不到就
ImagePullBackOff - 建业务容器:加入 pause 的命名空间,挂 Volume,按 cgroup 设资源限制
- 执行探针:startup → readiness → liveness
- 上报状态:定期向 apiserver 汇报,节点心跳断了,节点会变
NotReady,上面的 Pod 会被驱逐
这条链路上每一环失败,症状都不一样:
| 症状 | 卡在哪 |
|---|---|
ContainerCreating长时间不动 | 拉镜像慢、挂载 Volume 失败(CSI 问题) |
ImagePullBackOff | 镜像地址错、私有仓库凭据缺失、网络不通 |
CrashLoopBackOff | 容器启动了但反复退出,看logs --previous |
Running但READY 0/1 | readiness 探针没通过,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 | ⑤ kubelet | journalctl -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 删了,它自己就回来了;你改配置,它自己就滚动了。命令式思维在这里会处处别扭。
十、小结
kubectl apply提交的是期望状态,不是启动命令- apiserver 三道关卡:认证(你是谁)→ 授权(能不能做)→ 准入(合不合规),通过后写 etcd
- 控制器负责"从 Deployment 到 Pod"的展开,中间夹一层 ReplicaSet 是为了版本与回滚
- 调度器 Filter → Score → Bind,
Pending先看 Events - kubelet 通过 CRI 建 pause 沙箱、拉镜像、起容器、跑探针、上报状态
- kube-proxy 把 Service 规则写进内核,Pod 不 Ready 就没有 Endpoints
- 整条链路是持续收敛的闭环,不是一个一次性命令
把这六个阶段记在脑子里,下次 Pod 起不来,你会知道该看哪一层,而不是盲目重启。