课程目录(第 2 章 / 共 33 章)
课程/认识 Kubernetes

集群架构:控制平面与工作节点

2 章 / 共 33·14 分钟·入门概念架构控制平面

拆开一个 Kubernetes 集群,看清控制平面与工作节点上各跑着什么,以及一条 kubectl apply 之后请求如何一步步变成运行中的容器。

学完这一章,你将能够

  • 说出控制平面四个核心组件各自负责什么
  • 用 kubectl 在自己集群里找到这些系统组件
  • 讲清一条 kubectl apply 从提交到容器启动的完整链路

集群不是一台机器

上一章我们说,Kubernetes 的职责是「让现实不断向期望靠拢」。真正干这件事的不是一个程序,而是一组组件,分布在两类机器上:控制平面(control plane)负责做决定,工作节点(worker node)负责干活。

你敲的 kubectl 命令既不直接操作容器,也不直接跟节点说话,它只是把 YAML 交给集群的入口,剩下的由内部组件接力完成。

理解这套分工很实用:以后遇到节点 NotReady、Pod 卡在 Pending、Service 访问不通,你能立刻判断该去看哪一层,而不是盲目重启所有东西。

text
kubectl / CI / 其他客户端
        │ HTTPS

  kube-apiserver ──────────► etcd(唯一持久化存储)

        │ watch / 读写
        ├── kube-scheduler
        ├── kube-controller-manager
        └── cloud-controller-manager(可选,本地集群没有)

        │ 节点上的 kubelet 通过 apiserver 领任务

   kubelet ──► containerd ──► 容器
   kube-proxy ──► Service 的转发规则

注意这张图里所有箭头都指向 apiserver:没有组件会绕过它直接改 etcd

控制平面:做决定的那一层

组件核心职责一句话记忆
kube-apiserver集群唯一入口,负责认证、鉴权、准入校验,读写 etcd前台接待
etcd分布式键值存储,保存全部集群对象唯一真相来源
kube-scheduler为还没分配节点的 Pod 挑选节点派单员
kube-controller-manager运行一批控制器,把现实拉回期望纠偏工
cloud-controller-manager对接云厂商的负载均衡、节点与路由(可选)云适配层

kube-apiserver 是所有客户端唯一的对话对象。它本身几乎不保存状态,只做三件事:认证(你是谁)、鉴权(你能不能做)、准入控制(这个对象合不合法),然后把对象写进 etcd。正因为它无状态,生产环境可以同时跑多个实例,前面挂一个负载均衡。

etcd 是集群的「唯一真相来源」。你创建的每一个对象——Pod、Service、ConfigMap——最终都序列化存在这里;apiserver 通过 watch 机制把变化推送给其他组件。etcd 一旦不可用,集群就失去了记忆:已经在跑的容器还在跑,但任何查询、调度和变更都会失败。生产上 etcd 通常单独部署在低延迟 SSD 上,并盯住磁盘 fsync 延迟,因为所有写请求都要落盘才返回。

kube-scheduler 只干一件事:给 spec.nodeName 为空的 Pod 选一个节点。它先过滤掉资源不足、污点不匹配、亲和性不符的节点,再给剩下的节点打分,最后把绑定结果写回 apiserver。它不负责启动容器,只是写下「这个 Pod 应该去哪个节点」。

kube-controller-manager 把一批控制器打包在一个进程里运行:Deployment 控制器保证副本数、ReplicaSet 控制器创建 Pod、Node 控制器处理节点失联、EndpointSlice 控制器维护 Service 的后端列表。上一章提到的「控制器」,绝大多数跑在这个进程里。每个控制器都是同一个套路:watch 一类对象,比较期望与实际,然后采取动作。

cloud-controller-manager 是可选的,把与云厂商耦合的逻辑拆出去,比如创建云负载均衡、给节点打标签、管理云上路由。本地 kind / minikube 集群没有这个组件。

组件之间怎么「知道」发生了变化

所有组件都不去轮询 etcd,而是向 apiserver 发起 watch 请求,维持一条长连接。对象一有变化,apiserver 就把增量事件推给订阅者。这也是为什么控制平面组件大多是无状态的:它们的状态都在 etcd 里,重启后重新 watch 一遍就能恢复。

哪个组件挂了会怎样

这个表格能帮你在出问题时快速缩小范围。学习环境里不用背,但排错时非常省时间:

挂掉的组件你观察到的现象
kube-apiserverkubectl 完全连不上,所有控制器停止工作
etcd查询可能超时,新对象写不进去
kube-scheduler新 Pod 一直 Pending,已运行的 Pod 不受影响
kube-controller-managerPod 挂了不会重建,副本数不再被保证
kubelet该节点变成 NotReady,上面的 Pod 被驱逐
CoreDNS新建的 Pod 无法解析 Service 域名

注意最后两行:节点和 DNS 属于「Pod 已经跑起来之后」才暴露的问题,很容易被误判成应用故障。判断时先看 kubectl get nodes,再看 kubectl get pods -A,顺序不要颠倒。

单节点与高可用控制面

学习用的 kind、minikube 默认是单节点集群:一个节点同时承担控制平面和工作节点的角色。它足够学完本课程,但控制平面一挂,整个集群就无法调度和变更了。

生产集群的做法不同:控制平面至少 3 个节点,etcd 用 3 或 5 个成员(奇数是为了在出现网络分区时还能选出多数派),多个 apiserver 实例前面挂一个负载均衡 VIP,kubelet 和 kubectl 都连这个 VIP。工作节点可以随时增删,控制平面则要谨慎维护。

一句话总结:控制平面里只有 etcd 是有状态的,apiserver、scheduler、controller-manager 都可以多副本;所以要小心对待的是 etcd,而不是 apiserver。

工作节点:真正跑容器的那一层

节点上只有三个常驻角色,但它们是容器真正跑起来的地方。

kubelet 是节点上的工头。它通过 apiserver 领到自己节点上「应该跑哪些 Pod」,调用容器运行时把容器拉起来,按 livenessProbe / readinessProbe 执行健康检查,并持续上报节点与 Pod 状态。kubelet 不参与调度决策,它只执行「分配给我」的结果。

容器运行时 负责拉镜像、创建容器、管理命名空间和 cgroups。现在主流是 containerd,kubelet 通过 CRI(Container Runtime Interface)跟它说话。Docker 本身不再直接参与,它只是能构建出符合 OCI 标准的镜像。

kube-proxy 把 Service 的虚拟 IP 翻译成节点上的转发规则(iptables 或 IPVS 模式),让访问 Service 的流量能落到某个 Pod 上。它只做转发,不做负载均衡决策之外的复杂逻辑。现在很多集群用 Cilium 这类基于 eBPF 的方案直接替代它,把 Service 转发和网络策略一起接管。

kubelet 还会定期向 apiserver 上报节点心跳。默认情况下,超过约 40 秒没有心跳,Node 控制器就把节点标记为 NotReady;再持续一段时间(默认约 5 分钟),就会开始驱逐上面的 Pod,把它们重新调度到健康节点上。你不需要手动搬运 Pod,这套机制是自动的。

一套记忆顺序

把控制平面想成一家公司:apiserver 是前台,所有事都经过它;etcd 是档案室,唯一存底;scheduler 是派单员,决定这活派给谁;controller-manager 是监工,发现人数不对就补人。工作节点上,kubelet 是工头,containerd 是真正干活的工人,kube-proxy 负责把客人的电话转到工位上。

集群附加组件

控制平面和节点组件只保证「Pod 能跑起来」,要让应用好用,还需要几个附加组件。它们通常装在 kube-system 命名空间,或者由你自己部署到别的命名空间。

附加组件解决什么问题怎么确认它在跑
CoreDNS集群内 DNS 解析,让 web.demo.svc.cluster.local 能用kube-system 里有 coredns 的 Pod
CNI 插件给 Pod 分配 IP、打通跨节点网络(Calico、Cilium、Flannel)kind 默认用 kindnet
metrics-server提供 CPU / 内存指标,支撑 kubectl top 与 HPAkind 默认没有kubectl top 会报错
Ingress Controller把外部 HTTP 流量按域名和路径转发给 Service需要自己装,第 8 章 Ingress 会用 ingress-nginx

CoreDNS 是必装项:没有它,Pod 之间只能靠 IP 互相访问。CNI 也是必装项:没有它,Pod 连 IP 都拿不到,会一直卡在 ContainerCreating

动手:看清你的集群里跑着什么

观察系统组件

先看节点,再看每个节点上跑着哪些系统 Pod:

bash
kubectl get nodes
kubectl get nodes -o wide
kubectl get pods -n kube-system -o wide

判断成功的特征:

  • kubectl get nodesSTATUS 列是 ReadyROLES 列显示 control-plane<none>
  • kube-system 里能看到 kube-apiserver-<节点名>etcd-<节点名>kube-scheduler-<节点名>kube-controller-manager-<节点名>,以及 corednskube-proxy、CNI 插件的 Pod。
  • 控制平面组件在本集群里是静态 Pod:kubelet 直接按节点上 /etc/kubernetes/manifests/ 目录里的文件启动它们,apiserver 里的对象只是镜像。

再按组件类型筛一次,练习用标签选择器:

bash
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl get daemonsets -n kube-system

第一条应该只返回 CoreDNS 的 Pod;第二条列出 kube-proxy 和 CNI 插件,因为它们是「每个节点都要跑一份」的 DaemonSet。

一条 kubectl apply 之后发生了什么

这是本章最值得记住的链路。假设你提交了一个 Pod 清单:

  1. kubectl 把 YAML 转成 JSON,向 kube-apiserver 发起 HTTPS 请求,例如 POST /api/v1/namespaces/demo/pods
  2. apiserver 完成认证、鉴权、准入校验,补全默认值(比如 restartPolicy: Always),把对象写入 etcd。
  3. etcd 持久化成功并返回,apiserver 告诉 kubectl「创建成功」。此刻容器还没有开始跑。
  4. 控制器(如果你提交的是 Deployment,就是 Deployment 控制器)通过 watch 感知到新对象,发现期望副本数与实际不一致,创建 ReplicaSet,再由 ReplicaSet 控制器创建 Pod 对象。
  5. kube-scheduler 看到这个 Pod 的 spec.nodeName 是空的,经过过滤和打分选出节点,把绑定结果写回 apiserver。
  6. 该节点上的 kubelet watch 到「有一个 Pod 分配给我」,调用容器运行时(containerd)拉镜像、创建并启动容器。
  7. kubelet 把 Pod 状态(Running、Pod IP、就绪情况)上报给 apiserver 并写入 etcd,你才能从 kubectl get pods 里看到 Running

如果链路在第 6 步断掉,通常是镜像拉不下来,Pod 会停在 ImagePullBackOff;如果第 4、5 步出问题,Pod 会长时间停在 Pending。后面几章的排错,基本都在定位这条链路的哪一段卡住了。

把这条链路画成时序图,看的是「谁在什么时候动」:

text
kubectl apply -f pod.yaml
   │ ① HTTPS 请求

kube-apiserver ──② 写入──► etcd
   │ ③ etcd 写入成功,apiserver 返回 created(此刻容器还没跑)

   │ ④ watch 事件推送给控制器

控制器(Deployment → ReplicaSet → Pod)


kube-scheduler ⑤ 过滤 + 打分,写回 spec.nodeName


kubelet ⑥ 调 containerd 拉镜像、创建并启动容器


kubelet ⑦ 上报 Pod 状态 ──► apiserver ──► etcd


kubectl get pods 里终于看到 Running

注意 ③ 和 ⑦ 之间的时间差:`kubectl apply` 返回 `created` 只代表对象进了 etcd,不代表容器已经在跑。想观察这段过程,用 kubectl get pods -w 盯着状态变化。

常见坑

三个容易看错的地方

  • kubectl get pods -n kube-system 里的 kube-apiserver-xxx 是静态 Pod,删掉后 kubelet 会按 manifest 文件重新创建;想改配置要改文件,而不是改 apiserver 里的对象。
  • 节点 NotReady 不一定是 kubelet 挂了,也可能是容器运行时或 CNI 没起来。先 kubectl describe node <节点名>ConditionsEvents,再决定看谁。
  • 「集群能查询但起不了新 Pod」通常指向 etcd 或调度器;「集群完全连不上」通常是 apiserver 或控制平面网络。分层判断,别一上来就重启所有组件。

自测题

自测:为什么 kubectl apply 已经返回成功,Pod 却可能还是 Pending?(点击展开答案)

因为 apiserver 的职责边界只到「把对象合法地写进 etcd」。它写完就立刻返回,剩下的调度、拉镜像、启动容器都是异步发生的:scheduler 要先 watch 到新 Pod 再挑节点,kubelet 要再 watch 到绑定结果才动手。

所以 created 是「登记成功」,不是「运行成功」。如果对象进了 etcd 却一直没人处理(比如 scheduler 挂了),Pod 就会永远停在 Pending。

自测:kube-scheduler 挂了,为什么已经在跑的 Pod 不受影响?(点击展开答案)

因为调度器只做一件一次性的事:给 spec.nodeName 为空的 Pod 挑一个节点。已经运行中的 Pod 早就完成了这一步,它的生命周期由所在节点的 kubelet 负责——kubelet 是本机进程,拉容器、做健康检查、上报状态都不经过 scheduler。

所以 scheduler 挂掉的现象很典型:新 Pod 全部 Pending,老 Pod 照常服务。看到这个组合,先查 scheduler,而不是去重启应用。

自测:为什么说控制平面里最需要小心的是 etcd,而不是 apiserver?(点击展开答案)

因为 apiserver、scheduler、controller-manager 都是无状态的:状态全在 etcd 里,重启后重新 watch 一遍就能恢复,所以它们可以随时多跑几个副本。etcd 不一样,它保存着集群的全部对象,是唯一的真相来源。

etcd 一挂,已经在跑的容器仍然在跑(kubelet 不需要它也能维持本机容器),但任何查询和变更都会失败,集群等于「失忆」。因此生产上 etcd 的磁盘延迟比 CPU 更值得盯。

小结

  • 控制平面做决定:apiserver 是唯一入口,etcd 存全部状态,scheduler 挑节点,controller-manager 负责把现实拉回期望。
  • 工作节点干活:kubelet 领任务并上报状态,containerd 真正起容器,kube-proxy 维护 Service 的转发规则。
  • 附加组件补齐能力:CoreDNS 做服务发现,CNI 打通网络,metrics-server 提供指标,Ingress Controller 接入外部流量。
  • 一条 kubectl apply 的链路是:apiserver → etcd → 控制器 → 调度器 → kubelet → 容器运行时。
  • 单节点适合学习,生产要 3 个以上控制平面节点,并把 etcd 的磁盘性能当成第一优先级。

相关章节:动手把集群装起来看 第 3 章 kind 与 kubectl 上手;真机上排障的分层判断见 第 15 章 排障手册

练习

  1. 在你的集群上跑 kubectl get pods -n kube-system -o wide,把每个 Pod 归到「控制平面组件」「节点组件」「附加组件」三类里。
  2. kubectl describe node <你的节点名> 找到 Conditions 一节,查清 ReadyMemoryPressureDiskPressure 分别代表什么。
  3. 想一下:如果 etcd 的数据盘写满了,集群会先表现出什么症状?是「能查到对象」还是「写不进去」?

下一章我们把这样一个集群装到自己电脑上,从零开始,直到 kubectl get nodes 返回 Ready