1. 先搞清楚这套系统在解决什么问题
1.1 从"手动管理容器"到"声明式编排"
接触 Kubernetes 之前,我用了很长一段时间的 Docker Compose。几十个容器靠脚本和手工在管,部署一次要写一堆启动命令,节点一挂就得人工介入迁移,扩容基本靠复制粘贴再改端口。那会儿最痛苦的不是容器本身,而是"怎么让一堆容器像一个整体一样运转"。
Kubernetes 干的事情,核心就是这套"编排"逻辑。它不关心你单个容器内部是什么样的,它关心的是整个集群里同时运行着成百上千个容器时,怎么让它们各就各位、故障自愈、按需伸缩。
这里要特别注意一个概念:声明式。大多数刚开始接触的人都会在这里绕弯。Docker 的方式是命令式——你告诉它"现在给我跑一个 nginx 容器";Kubernetes 是声明式——你告诉它"我需要 3 个 nginx 副本",至于这 3 个副本怎么创建、分布到哪台机器上、某个副本挂了怎么办,Kubernetes 自己会想办法搞定。
理解"声明式"是理解整个 Kubernetes 组件设计的钥匙。整套系统的核心循环就一句话:持续比对"期望状态"和"实际状态",发现不一致就去纠正,直到两者对齐。这个循环由不同组件接力完成,也就是标题里说的"组件"各司其职。
另外,Kubernetes 里的"组件"这个词容易让人混淆。一种是指系统级的组件,也就是本文介绍的控制平面和工作节点上的这 7 个核心进程;另一种是指你业务里的各种应用组件(比如微服务拆分出来的各个服务)。本文讨论的是前者——那些支撑 Kubernetes 本身运行的基础设施。
1.2 控制平面与工作节点的分工逻辑
Kubernetes 的架构本质上就是"大脑 + 四肢"的模型。
- 控制平面(Control Plane):负责所有决策,比如谁该运行、哪个副本挂掉了、新版 Desired State 是什么。控制平面通常不跑业务容器。
- 工作节点(Worker Node):真正跑业务容器的地方,它做的事情就是把控制平面的"指令"落地,然后把执行结果汇报回去。
这种分工不是拍脑袋定的,而是有实打实的工程意义。控制平面集中管理,方便做统一的鉴权、审计和状态存储;工作节点只做执行,节点之间没有耦合关系,任何一个节点宕机都不会影响控制面的决策。而节点的故障,正好由控制面里的 Controller 负责感知和处理。
用一句俗话概括:控制平面负责“想”,工作节点负责“做”。所有"想"的结果都会落在一个共享的、强一致的状态存储里,所有"做"的过程也都通过这个状态存储来反馈,这样整个集群在任何时间点,都能对齐到同一个事实。
下面这张表,先给你一个整体地图,后面再逐一展开:
| 组件 | 所在位置 | 核心职责 |
|---|---|---|
| kube-apiserver | 控制平面 | 所有 API 请求的唯一入口,负责鉴权、准入控制、读写状态 |
| etcd | 控制平面 | 键值存储,保存整个集群的期望状态和实际状态 |
| kube-scheduler | 控制平面 | 为新创建的 Pod 挑选一个合适的节点 |
| kube-controller-manager | 控制平面 | 运行各类控制器,把系统状态收敛到期望状态 |
| kubelet | 工作节点 | 节点上的代理,负责创建和管理本节点上的 Pod |
| kube-proxy | 工作节点 | 维护 Service 的转发规则,实现服务发现与负载均衡 |
| 容器运行时 | 工作节点 | 真正执行容器进程,常见的有 containerd、CRI-O |
2. 控制面四大件:API Server、etcd、Scheduler、Controller Manager
2.1 API Server:一切请求的唯一入口
如果说 Kubernetes 是一个国家,API Server 就是它的政务大厅。所有外部请求——不管是 kubectl 命令、CI/CD 系统、还是其他组件的内部通信——都必须先经过 API Server,没有任何例外。
为什么非要这么"绕"?因为 API Server 干的不只是转发,它承担了三层非常关键的关卡:
- 认证(Authentication):先确认你是谁,常见方式有客户端证书、Token、ServiceAccount。
- 授权(Authorization):确认你是否有权限做这个操作,默认走 RBAC。
- 准入控制(Admission Control):在请求真正落库之前,做最后的校验和修改,比如自动给命名空间打标签、注入 Sidecar、校验资源配额。
三层关卡走完,API Server 才会把你的请求写入 etcd,并返回结果。注意,API Server 是唯一直接读写 etcd 的组件,其他组件想获取集群状态,都是通过 API Server 提供的 Watch 机制来订阅,而不是各自直连 etcd。这个设计避免了多组件并发写同一份数据导致的混乱,也让 API Server 成为天然的"消息总线"。
我实际排查时的经验:当某个组件出现"连不上集群"或者 "leader election lost" 这类问题时,第一反应不应该是去翻某个业务 Pod 的日志,而是先检查 API Server 是否健康、认证凭证是否过期。很多"诡异问题",最后都出在 API Server 这一层的访问链路上。
2.2 etcd:集群的"真相存储"
etcd 是一个分布式的、强一致性的键值数据库。集群里所有的资源对象——Deployment、Service、Pod、ConfigMap、Secret——都保存在 etcd 里。它就像一个黑匣子,里面装着集群的全部状态。
这里头最关键的是"强一致"三个字。etcd 靠 Raft 共识算法实现,集群内部所有数据变更需要大多数节点(例如 3 节点集群里至少 2 个节点)确认,才算真正写入成功。这也是为什么生产环境 etcd 节点的推荐数量是 3、5、7 等奇数——必须保证在少数节点故障时,仍然能形成多数派继续工作。
在实际操作中,etcd 是控制平面里最容易出性能问题的组件,没有之一。因为它对磁盘的 fsync 延迟极其敏感。当你发现 API Server 响应偶发超时、或者 kubectl 操作要卡好几秒时,很大概率是 etcd 所在的盘太慢,或者 etcd 的数据目录快满了。
这里有几个实操经验值得记下:
- 定期做快照备份:
etcdctl snapshot save,并把这个快照文件异地留存,这是集群灾难恢复的唯一救命稻草。 - 别把 etcd 和业务容器混在同一个磁盘上,尽量让 etcd 独占 SSD。
- 默认端口 2379 是客户端通信端口,2380 是节点间通信端口,网络策略里不要乱封。
2.3 Scheduler:怎么决定 Pod 落在哪个节点
Scheduler 的职责只有一个:给新创建的 Pod 找到一个最适合运行的节点。听起来简单,但"最适合"三个字,背后是一套完整的调度算法。
调度过程分为两步,业内常叫"过滤"和"打分"。
过滤阶段,把所有不能满足 Pod 需求的节点剔除掉。比如 Pod 要求 4 核 8G,那么只剩余 2 核的节点会被过滤掉;Pod 要求绑定在某个可用区的节点上,那其他可用区的节点也会被过滤掉;节点上有污点(Taint)而 Pod 没有对应的容忍(Toleration),也过不了这一关。
打分阶段,对过了过滤的节点按优先级打分。比如资源使用率更均衡的节点会得高分,Pod 与已运行 Pod 在同一节点或相邻节点(亲和性)也会影响分数。最后 Scheduler 选出分数最高的节点,把结果写回 API Server——具体来说,是给 Pod 的spec.nodeName字段赋值。
关键点在于:Scheduler 并不直接给节点下指令。它只是"建议"Pod 应该跑在哪个节点上,这个建议写进 API Server 之后,目标节点上的 kubelet 才会感知到并真正动手创建容器。这个"间接通信"模式贯穿整个 Kubernetes 设计,所有组件之间都通过 API Server 沟通,谁都不直接碰谁的接口。
我见过的常见误区是:有人以为 Pod 一直 Pending 是 kubelet 没启动。其实 Pending 说明调度器还没选好节点,大概率是资源不足、端口冲突或污点不匹配。先去看节点状态和 Pod 事件,而不是去重启节点上的 kubelet。
2.4 Controller Manager:把期望状态变成实际状态
Controller Manager 是控制平面里最"机械"又最核心的一个组件。它内部跑着一堆控制器,比如:
- ReplicaSet 控制器:保证一个 ReplicaSet 指定数量的 Pod 一定存在,少了就补建,多了就删掉。
- Deployment 控制器:管理 ReplicaSet 的发布和回滚。
- Node 控制器:节点失联后,负责给节点打 NotReady 状态,并清理该节点上的 Pod。
- EndpointSlice 控制器:维护 Service 与后端 Pod 的关联关系。
每个控制器的干活模式都一样:Observe(观察当前状态)→ Diff(和期望状态比一比)→ Act(执行纠正动作)。这就是著名的"控制循环"(Control Loop)。
你可以把它想象成一个恒温器:你设定了 26 度(期望状态),温度传感器发现现在是 24 度(实际状态),于是恒温器启动加热(Act),直到温度回到 26 度。Controller Manager 里的每个控制器,都是一个专注于某一类资源的"恒温器"。
为什么不让一个大控制器统一管理所有资源?因为关注点分离更好维护、更容易扩展。今天你只需要自定义一种新资源,写一个专门的控制器去管它就行,没必要重写整个控制面。这也是 Kubernetes 扩展性这么强的根本原因之一。
3. 工作节点三件套:kubelet、kube-proxy、容器运行时
3.1 kubelet:节点上的"监工"
如果说控制平面是大脑,那 kubelet 就是大脑派到每个节点上的监工。它负责三件大事:
第一,向 API Server 注册节点,并且定期汇报心跳和节点状态(CPU、内存、磁盘压力、容器运行状态),这些数据会更新到 Node 对象上。
第二,监听 API Server 分配给本节点的 Pod 任务。每当一个 Pod 被调度到本节点,kubelet 就会去拉镜像、创建容器、维护容器生命周期。它不关心节点选择,只关心"分到我头上的活,我要干好"。
第三,执行探针(Probe)检查,比如 LivenessProbe(判断容器是否活着,挂了就重启)和 ReadinessProbe(判断容器是否就绪,就绪了才把流量打过来),然后把结果汇报给 API Server。
kubelet 本身并不直接创建容器,它通过 CRI(Container Runtime Interface)调用容器运行时。这里有个概念要分清:Pod 只是逻辑上的调度单元,实际容器运行起来之前,kubelet 会先创建一个叫"pause"的沙箱容器,它负责持有整个 Pod 的网络命名空间和共享存储卷,业务容器再逐一加入这个沙箱。这个 pause 容器就是为什么你在节点上用crictl ps能看到一堆pause容器的原因,别当成异常进程去处理。
实际运维中,kubelet 出问题最典型的现象是节点显示 NotReady。这时候先别急着重启节点,按顺序排查:检查 kubelet 服务状态、看/var/log/messages或 journald 里 kubelet 的日志、确认节点上的容器运行时是否健康、确认证书是否过期。大多数 NotReady 都不是真正的"节点坏了",而是 kubelet 和运行时之间的断链。
3.2 kube-proxy:服务的网络转发
业务 Pod 的 IP 是随时变化的——节点故障、扩缩容、重新调度,都会导致 Pod IP 改变。你不可能让上游服务去记这些飘忽不定的地址,所以就有了 Service 这个抽象,以及负责让 Service 生效的 kube-proxy。
kube-proxy 的工作本质上就是维护一组转发规则:当访问 Service 的 ClusterIP 或 NodePort 时,把流量转发到某个后端 Pod 上。它有两种主流的实现模式:
- iptables 模式:默认且最常见,利用 Linux 内核的 NAT 规则做转发,随机挑选后端 Pod。
- IPVS 模式:基于内核的 IPVS 模块,支持更丰富的负载均衡算法(如加权轮询、最小连接数),性能更好,规则量大时也更有优势。
注意,kube-proxy 只是个"规则维护者",它不是代理进程本身,真正的数据包转发是在内核态完成的。所以不必担心 kube-proxy 成为性能瓶颈,它的瓶颈只在于规则数量过大时的更新延迟。
排障时,如果遇到"Pod 能 ping 通但 Service 访问不通",优先怀疑 kube-proxy 的规则没有生效。用iptables -t nat -L查看规则,确认是否生成了对应 Service 的 DNAT 条目;如果用的是 IPVS 模式,就用ipvsadm -ln查看。很多时候不是 kube-proxy 崩溃,而是它监听的 EndpointSlice 没有及时更新,底层对象变化没被同步过来。
3.3 容器运行时:真正的执行层
容器运行时是整个链路里的"最后一公里"——真正把镜像拉下来、把进程跑起来的组件。
历史上 Kubernetes 长期直接调用 Docker,后来因为维护成本和解耦需求,演进出 CRI 标准接口,并在 v1.24 版本移除了内置的 dockershim。现在主流的运行时是 containerd 和 CRI-O,Docker 本身依然能通过外部 shim 使用,但已经不是默认路线了。
为什么非要搞 CRI 这个标准?因为 Kubernetes 不想被某个具体的运行时绑架。只要运行时实现了 CRI 的接口,无论底层是 containerd、CRI-O 还是其他兼容实现,kubelet 都能用同样的方式驱动它。这套"标准接口"的思想,和 CNI(容器网络接口)、CSI(容器存储接口)一脉相承,都是 Kubernetes 扩展性的根基。
在看节点上的容器时,我建议直接用crictl而不是docker命令。比如:
crictl ps # 查看本节点所有容器 crictl logs <container-id> # 查看容器日志 crictl inspect <container-id> # 查看容器详细信息它们输出的信息维度类似,但crictl更贴近 Kubernetes 的视角,能直接看到 Pod 相关的沙箱和容器。许多新手在节点上用 docker 命令看容器,发现和kubectl get pods对不上,就是因为没意识到有 pause 沙箱层和业务容器层的区分。
4. 一次 Pod 创建请求的完整旅程
理解组件之间的协作,最好的方式不是背架构图,而是跟一次完整的"事件流"走一遍。下面以kubectl apply -f deployment.yaml为例,看看一个 Deployment 从提交到 Pod 运行,到底经历了什么。
4.1 从 kubectl 到 API Server
你敲下命令的那一刻,kubectl 会把 YAML 解析成 JSON,然后向 API Server 发起一个 POST 请求,路径类似/apis/apps/v1/namespaces/default/deployments。
这个请求先过认证:kubectl 读取你的 kubeconfig 里配置的客户端证书或 Token,确认你的身份。再过授权:你在 RBAC 规则里有没有权限在这个命名空间创建 Deployment。最后走进准入控制:比如系统检查你有没有超出资源配额、命名空间是否存在、有没有需要注入的默认值。三层关卡全部通过,API Server 才把这份"期望状态"写入 etcd,然后返回一个 201 Created。
这里有个细节:这个时刻系统里还没有任何 Pod,只是 etcd 里多了一个 Deployment 对象。后续所有动作,都是从这次写入之后开始被感知的。
4.2 控制器与调度器的接力
Deployment 控制器一直在监听 Deployment 对象的变化。它发现 etcd 里多了一个新 Deployment,就会创建对应的 ReplicaSet 对象。ReplicaSet 控制器接着登场:它计算发现"期望副本数是 3,当前 Pod 数是 0",于是开始创建 3 个 Pod 对象。
这 3 个 Pod 对象会被写入 etcd,但它们的spec.nodeName还是空的,状态是 Pending。
Scheduler 此时正在持续监听所有 Pending 的 Pod。它发现了这 3 个没有 nodeName 的 Pod,便开始执行上文提到的"过滤 + 打分"流程,最终选出节点 node-a、node-b、node-c。然后 Scheduler 通过 API Server 把 Pod 的spec.nodeName字段更新为对应节点。
这里重申一次:Scheduler 没有直接联系 node-a 上的 kubelet,它只是把这个决策结果更新到了 API Server 上的 Pod 对象里。kubelet 是"自己"通过 Watch 机制发现这个变化的。
4.3 节点上的执行与反馈
node-a 的 kubelet 通过 API Server 的 Watch 接口,发现刚才那个 Pod 被绑定到了自己身上。接下来是真正的"落地"过程:
- kubelet 调用 CRI,让容器运行时先创建一个 Pod 沙箱(即 pause 容器),把网络命名空间、共享卷建好。
- 容器运行时按顺序拉取镜像。如果 Pod 里有 InitContainer,会先串行跑完初始化容器。
- 创建业务容器,加入沙箱,挂载卷,配置网络。
- 容器启动后,kubelet 开始执行 LivenessProbe 和 ReadinessProbe,同时持续采集容器日志和指标。
- kubelet 把 Pod 的运行状态不断写回 API Server,最终
kubectl get pods里能看到 Pod 进入 Running 状态,并且 READY 列变为1/1。
如果这一步里哪一环出问题——镜像拉不下来、探针连续失败、CNI 网络配置报错——Pod 会卡在 ContainerCreating 或 CrashLoopBackOff,你需要通过kubectl describe pod看事件,去定位具体是运行时、网络还是探针的问题。
处理完这条完整链路,你对组件的理解才算真正从"背清单"变成了"懂协作"。以后再遇到任何集群问题,都可以对照这条链路逐层排查。
5. 用 kubeadm 搭集群后的排障实战:组件协作中的典型坑
5.1 读懂 kubeadm init 的 pre-flight 报错
很多人的第一个 Kubernetes 集群是用 kubeadm 搭的。执行kubeadm init时,首先看到的一段输出就是:
[init] Using Kubernetes version: v1.26.0 [preflight] Running pre-flight checks这段 pre-flight 检查,其实是 kubeadm 在帮你验证"这套环境能不能安全地装上控制平面"。它包含很多细项,但失败高频的大概有这几类:
- 检测到 swap 未关闭。kubelet 在大多数部署方式下不支持 swap,所以提示先执行
swapoff -a并注释掉/etc/fstab里的 swap 行。 - 端口被占用。比如 6443 被别的进程占用、或 10250 端口起不来,此时
ss -lntp看一下就能定位,先停掉占用进程再重试。 - CRI 版本不匹配。kubeadm 会去连容器运行时的 socket,比如
/run/containerd/containerd.sock,如果 containerd 没启动或 socket 路径配置不对,会直接卡在这里。
还有一个非常经典的坑:kubelet 用的 cgroup 驱动和容器运行时的 cgroup 驱动不一致。比如 kubelet 配置成了systemd,但 containerd 还用的cgroupfs,节点起来后 kubelet 一直报错,Pod 也起不来。解决方法是让两者对齐,一般在 containerd 的配置文件里加上:
SystemdCgroup = true5.2 etcd 的备份恢复与性能隐患
生产环境里,etcd 的坑多数不在"坏了,怎么修",而在"没备份,坏不起"。有一次我把某个测试集群的 etcd 数据目录误删,才发现手里的快照还是三天前的——中间的应用变更全部丢失。从那时起,我给自己定了个规矩:etcd 快照不仅要做,做完还要拿到集群之外单独的目录存一份。
备份命令很简单:
ETCDCTL_API=3 etcdctl --endpoints=127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ snapshot save /backup/etcd-snapshot-$(date +%Y%m%d).db恢复时,如果你只有一个节点的单 etcd,需要先把集群停掉,清空数据目录,再用快照文件恢复。多节点集群恢复的流程更复杂,建议先在测试环境完整演练一遍,别等到线上出事才来翻命令。
另一个容易被忽略的是 etcd 的磁盘健康度。etcd 对磁盘 fsync 的延迟非常敏感,普通机械盘在高负载下会直接把 API Server 拖到超时。如果你的 etcd 节点和业务节点混部在同一台机器且磁盘 IO 被跑满,基本可以预期整个集群开始"瘫痪"。有条件的话,给 etcd 单独挂一块 SSD。
5.3 kubelet 与容器运行时的断链排查
kubelet 和容器运行时之间是通过 CRI 通信的。它们如果失联,表现就是节点 NotReady,kubectl describe node 里会看到类似failed to connect to containerd的报错。
排查顺序我通常这样走:
- 确认 containerd 服务状态:
systemctl status containerd。 - 用
crictl info测试运行时接口是否能正常响应。 - 看 kubelet 日志,通常在 journald 里:
journalctl -u kubelet -f。 - 检查容器运行时 socket 路径和 kubelet 启动参数里的
--container-runtime-endpoint是否一致。
还有一类隐蔽情况:节点上磁盘压满,导致容器运行时无法创建临时文件或拉取镜像,kubelet 也报 Eviction 相关错误。这时候df -h一看便知,清理镜像缓存或日志文件就能恢复,不需要重启任何组件。
5.4 CNI 网络组件导致的 Pod 网络异常
最后聊一个非常高频的问题:Pod 卡在 ContainerCreating,describe 一看,事件里写着类似failed to set up pod network的报错。这通常不是 Kubernetes 组件本身的问题,而是 CNI 网络插件(比如 Calico、Flannel)没配好。
排查链路建议按这个顺序:
- 先看 CNI Pod 是否 Running。
kubectl get pods -n kube-system里如果 calico-node 或 flannel 不是 Running,优先解决它。 - 看事件的具体报错:
kubectl describe pod <pod-name>最末尾的 Events 区块,信息量很大,能指明是网卡创建失败还是 IP 分配失败。 - 再看 kubelet 日志里 CNI 相关输出,以及 CNI 插件的日志。
- 最后检查节点间的网络互通,比如底层防火墙是否放行了 VXLAN 或 BGP 所需端口。
另外,当整个集群的 Service DNS 解析都异常时,链路排查顺序大概是:CoreDNS Pod 是否存活 → kube-proxy 的规则是否生成 → 是否被网络策略拦截 → 底层节点是否有多路由冲突。只要按这条链走,基本都能在几分钟内把问题定位到具体组件上。
我在实际部署集群时,最深的体会是:K8s 排障别靠猜,要按照组件通信链路一层层查。上文那个 Pod 创建旅程,其实就是一个天然排查地图——先确认请求到了 API Server,再看 Controller 有没有生成对应对象,再看 Scheduler 有没有做调度决策,再看 kubelet 有没有认领任务,最后才看容器运行时的日志。按这个顺序走,绝大多数问题十分钟内能定位。这套组件架构设计得这么"绕"不是没理由的,每个角色单一职责、统一通过 API Server 通信,虽然多跳了几层网络,换来的是整个集群的稳定和解耦。如果你刚开始搭集群,建议把这篇文章当成一份对照清单,配合 kubeadm init 的输出一步步理解,会比死记组件清单有用得多。