深入理解 Kubernetes:架构、组件、资源与控制器
引言
Kubernetes(以下简称 K8s)本质上是一个"声明式的分布式操作系统":用户描述"我想要什么样的最终状态",K8s 内部一整套组件协同工作,持续把集群从"当前状态"驱动到"期望状态"。理解 K8s,最重要的不是记住某个资源怎么写 YAML,而是理解这套系统的设计哲学——一切皆状态,一切靠协调(reconcile)。本文从架构、组件、资源模型、部署链路、控制器原理五个层面,尽量抛开命令与代码,讲清楚"为什么是这样设计的"。
一、整体架构:控制平面与工作节点
K8s 集群在物理/逻辑上分为两类角色:
- 控制平面(Control Plane,也就是常说的 Master/Server 节点):负责决策——调度谁去哪个节点、维护集群状态、暴露 API 入口。
- 工作节点(Worker Node):负责执行——真正跑容器、上报状态。
这个划分背后的设计动机很直接:决策与执行分离。控制平面不直接操作容器,它只操心"应该是什么样子";节点上的 agent 才负责"具体怎么落地"。这样即使控制平面短暂不可用,已经运行的工作负载也不会立刻受影响,系统具备了一定的容错弹性。
在 K3s 这种轻量发行版里,这个划分被大幅简化和打包(单二进制内嵌 etcd/SQLite、去掉云厂商相关组件等),但核心的角色划分逻辑没有变。
二、Server(控制平面)组件详解
1. kube-apiserver —— 唯一的"前门"
apiserver 是整个集群唯一对外暴露的入口,所有组件(包括 kubelet
、scheduler、controller-manager,甚至 kubectl)都只和 apiserver 打交道,从不直接互相通信,也不直接读写 etcd。
这样设计的原因:
- 统一鉴权与准入控制:所有请求经过同一层做认证(Authentication)、鉴权(Authorization,如 RBAC)、准入控制(Admission Controller,如资源配额校验、Webhook 校验/变更)。
- 解耦:其他组件不需要知道 etcd 长什么样、怎么存储,只需要面向一套统一的 REST/gRPC(实际是 HTTP + Protobuf/JSON)API 编程。
- 可扩展:CRD(自定义资源)能够无缝接入,就是因为大家统一走 apiserver 这一层抽象。
2. etcd —— 集群的"记忆"
etcd 是一个分布式一致性键值存储(基于 Raft 协议),是 K8s 集群里唯一的"真相来源"(Single Source of Truth)。所有资源对象最终都以键值对形式存进 etcd。
为什么必须是强一致性存储?因为调度、控制器协调都依赖"读到的状态是准确的",如果存储层本身数据不一致,整个集群的协调逻辑就会建立在流沙上。这也是为什么 etcd 节点数一般是奇数(3、5),以满足 Raft 多数派选举的要求。
3. kube-scheduler —— "红娘"
scheduler 只做一件事:把还没有被绑定节点的 Pod,挑一个"最合适"的节点安排上去。这个过程分两个阶段:
- 过滤(Filtering / Predicates):排除资源不够、亲和性不满足、有污点容忍不了等等的节点。
- 打分(Scoring / Priorities):对剩下候选节点打分,选择得分最高的。
值得注意的是,scheduler 本身不负责实际启动容器,它只是把"这个 Pod 应该在哪个节点上运行"这个决定写回 apiserver(更新 Pod 的 nodeName 字段)。真正的执行是节点上的 kubelet 发现自己"被分配了任务"后去落地的。这种"决策者不执行、执行者不决策"的分工,同样是为了解耦和可替换性——你完全可以写一个自定义 scheduler 替换掉默认的。
4. kube-controller-manager —— "协调器合集"
这是一个进程,内部跑着一堆彼此独立的控制器(Controller),例如 Deployment 控制器、ReplicaSet 控制器、Node 控制器、Endpoint 控制器、Job 控制器等。每个控制器都遵循同一套模式——Reconciliation Loop(协调循环):不断对比"期望状态"和"实际状态",如果不一致就采取行动让二者趋同。控制器的详细工作机制放在第五节展开。
把这么多控制器打包进一个进程,是工程上的取舍:减少需要独立部署和维护的组件数量,同时通过 leader election(领导者选举)保证同一时刻只有一个 controller-manager 实例在真正工作,其余作为热备。
5. cloud-controller-manager —— 云厂商相关逻辑的"隔离层"
这个组件把强依赖于具体云厂商 API 的逻辑(比如创建负载均衡器、给节点打云厂商特有的标签、管理云盘)从核心逻辑中剥离出来。这样 K8s 核心代码保持"厂商中立",各云厂商只需要实现这一层插件即可接入。自建 K3s 集群如果没有对接云厂商,这一组件常常是缺省或裁剪掉的。
三、工作节点(Node)组件详解
1. kubelet —— 节点上的"监工"
kubelet 是运行在每个节点上的 agent,持续 watch apiserver,看有没有分配给"自己节点"的 Pod。一旦发现,它负责:
- 通过 CRI(Container Runtime Interface) 调用容器运行时(如 containerd)去真正创建/销毁容器;
- 持续检查容器健康状态(liveness/readiness probe),并把结果上报回 apiserver;
- 管理 Pod 的存储卷挂载(通过 CSI)、网络配置(通过 CNI,但网络配置通常由 CNI 插件在 Pod 创建时触发)。
kubelet 从不直接和 scheduler 或 controller-manager 通信,它眼里只有 apiserver——这再次体现了"一切通过 apiserver"的架构原则。
2. kube-proxy —— 服务发现与负载均衡的"数据面"
Service 这个抽象需要有人把"虚拟 IP"翻译成"真实 Pod IP 之间的流量转发规则",kube-proxy 就是干这个的。它 watch Service 和 Endpoint(或 EndpointSlice)的变化,动态维护节点上的转发规则(常见实现有 iptables 模式、IPVS 模式)。这也是为什么 Service 的 ClusterIP 其实"不存在"于任何网卡上——它只是一条条转发规则的集合。
3. Container Runtime(如 containerd)—— 真正"跑容器"的那个
通过 CRI 接口与 kubelet 解耦。K8s 早期直接绑定 Docker,后来抽出 CRI 标准接口,任何符合 CRI 规范的运行时都能接入,这就是"可插拔"设计思想在运行时层的体现。
三大插件接口小结
| 接口 | 解决什么问题 | 典型实现 |
|---|---|---|
| CRI(Container Runtime Interface) | 容器怎么创建/销毁/管理 | containerd, CRI-O |
| CNI(Container Network Interface) | Pod 怎么获得网络、怎么互通 | Flannel, Calico, Cilium |
| CSI(Container Storage Interface) | Pod 怎么挂载持久化存储 | 各云盘/存储厂商 CSI 驱动 |
这三个接口的共同设计动机是一样的:把易变的、厂商特定的实现细节从核心系统剥离,核心系统只面向标准接口编程,具体实现留给生态自由竞争。
四、资源模型:为什么会有这么多种资源对象
理解 K8s 资源,最好按照"问题驱动"的思路,而不是死记硬背种类。每一种资源的出现,都是为了解决前一层抽象没解决好的问题。
1. Pod —— 最小调度/部署单元
为什么不是"容器"作为最小单元,而是 Pod(一组容器)?因为现实中经常有"强耦合、必须同生共死、需要共享网络和存储"的容器组合(比如主容器 + 日志采集 sidecar)。Pod 把这类容器打包成一个原子调度单位,共享同一个网络命名空间和存储卷。
但 Pod 有个致命问题:它不具备自愈能力。Pod 所在节点挂了,或者 Pod 自身崩溃退出,没有人会自动帮你再拉起一个新的。
2. ReplicaSet —— 解决"Pod 数量恒定"的问题
ReplicaSet 的存在就是为了解决上面这个问题:声明"我要恒定跑 N 个某规格的 Pod",控制器持续监控实际数量,少了就补,多了就删。
3. Deployment —— 解决"如何安全地发布新版本"的问题
ReplicaSet 能维持数量,但不擅长处理"版本升级"——如果你直接改 ReplicaSet 的镜像版本,所有 Pod 会被同时替换,服务会中断。Deployment 在 ReplicaSet 之上又包了一层:它通过管理多个 ReplicaSet(新旧各一个)来实现滚动更新、版本回滚、暂停/恢复发布。这是一个非常典型的"分层抽象,每层只解决一个问题"的例子。
4. StatefulSet —— 解决"有状态服务"的问题
Deployment 假设 Pod 之间是完全对等、可随意替换的(无状态)。但数据库、消息队列这类服务,每个实例都有自己独立的身份(固定的网络标识、独立的持久化存储,不能随便换)。StatefulSet 就是为了给 Pod 提供稳定的编号、稳定的网络标识(配合 Headless Service)、以及一一对应的持久化存储卷。
5. DaemonSet —— 解决"每个节点必须跑一份"的问题
日志采集器、监控 agent、网络插件这类组件的需求不是"跑 N 个",而是"每个节点必须恰好跑一个"。DaemonSet 就是专门表达这种诉求的资源,新节点加入集群时会自动补齐。
6. Job / CronJob —— 解决"一次性/周期性任务"的问题
前面几种资源都假设 Pod 是"长期运行"的服务。但批处理任务跑完就该结束,不该被当成"崩溃"重启。Job 负责保证任务跑到成功为止(可控制重试与并行度),CronJob 则在 Job 之上加了"按 cron 表达式周期触发"的能力。
7. Service —— 解决"Pod IP 不稳定"的问题
Pod 会被频繁创建销毁,IP 一直在变,如果消费方直接依赖 Pod IP,几乎没法用。Service 提供一个稳定的虚拟 IP 和 DNS 名字,背后自动关联一组匹配 label 的 Pod,并做负载均衡。它解决的是"服务发现 + 负载均衡"这个经典分布式系统问题。
8. Ingress / Gateway API —— 解决"七层路由"的问题
Service 主要工作在四层(TCP/UDP),当你需要基于域名、路径做 HTTP 路由、TLS 终止的时候,Service 就不够用了。Ingress(以及更新的 Gateway API)在此之上提供七层路由规则的声明方式,具体转发能力由 Ingress Controller(如 Traefik、Nginx Ingress)实现——这也是"资源只是声明,控制器负责落地"这一模式的又一次体现。
9. ConfigMap / Secret —— 解决"配置与代码分离"的问题
把配置(ConfigMap)和敏感信息(Secret)从镜像中剥离出来,使同一个镜像可以在不同环境(SIT/UAT/PRD)用不同配置运行,而不需要重新构建镜像。Secret 与 ConfigMap 的区别主要在于:apiserver 对其访问权限、编码方式(并非加密,只是 base64 编码)和展示方式做了额外限制。
10. Namespace —— 解决"多租户/环境隔离"的问题
Namespace 提供了资源命名和权限管理的逻辑边界,让同一个集群可以承载多个团队、多个环境。
11. PV / PVC / StorageClass —— 解决"存储生命周期与解耦"的问题
容器本身是无状态、易失的,但业务数据需要持久化。这里又分了三层抽象:
- PV(PersistentVolume):代表一块实际存在的存储资源(比如一块云盘、一个 NFS 目录),由运维/存储系统提供。
- PVC(PersistentVolumeClaim):应用方发出的"我需要多大、什么访问模式的存储"的请求,与具体存储实现无关。
- StorageClass:把"动态创建 PV"这件事模板化、参数化,避免每次都要人工预先创建 PV。
这一整套分层的动机同样是解耦:应用开发者只关心 PVC,不需要知道底层是哪种存储介质。
12. CRD(CustomResourceDefinition)—— 解决"K8s 原生资源不够用"的问题
当你需要表达一个 K8s 原生没有的概念,CRD 允许你在不修改 K8s 源码的前提下扩展出全新的资源类型,再配合自定义控制器(Operator)实现"声明 + 协调"的整套模式。CRD 的存在,本质上说明了 K8s 的核心价值不是"容器编排",而是它这套"声明式 API + 控制器协调"的通用框架,容器只是它管理的第一类资源而已。
五、一次部署的完整链路:从提交到运行
把上面所有组件串起来看一次典型的"发布一个 Deployment"的过程,能帮助理解各组件如何协作:
- 提交:用户提交一个 Deployment 的期望状态描述给 apiserver。
- 鉴权与准入:apiserver 做身份认证、RBAC 鉴权,再经过一系列准入控制器(可能包括默认值填充、合法性校验、Webhook 校验),确认这个请求合法且完整。
- 落盘:apiserver 把这个对象写入 etcd,这是这个资源"正式存在"的时刻。
- Deployment 控制器介入:controller-manager 里的 Deployment 控制器 watch 到新对象,发现"期望的 ReplicaSet 不存在",于是创建一个新的 ReplicaSet 对象(同样通过 apiserver 写入 etcd)。
- ReplicaSet 控制器介入:ReplicaSet 控制器发现"期望数量的 Pod 不存在",于是创建对应数量的 Pod 对象(此时 Pod 还没有绑定节点)。
- 调度:scheduler watch 到"未绑定节点的 Pod",执行过滤+打分,选出目标节点,把结果写回 apiserver(更新 Pod 的 nodeName)。
- 节点侧执行:目标节点上的 kubelet watch 到"分配给自己的新 Pod",通过 CRI 调用容器运行时拉镜像、创建容器;如果需要网络,CNI 插件配置网络;如果需要存储,CSI 驱动完成挂载。
- 状态上报:kubelet 持续监测容器状态(存活/就绪探针),把 Pod 的实际状态(Running/CrashLoopBackOff 等)写回 apiserver。
- 服务发现联动:如果这个 Pod 被某个 Service 的 selector 匹配到,Endpoint/EndpointSlice 控制器会更新对应的端点列表,kube-proxy watch 到变化后更新节点上的转发规则,流量开始能够正确路由到新 Pod。
整个链路里,没有任何一个组件直接命令另一个组件——全都是"写状态到 apiserver / etcd,其他组件自己 watch 到变化后各自反应"。这正是下一节要讲的核心机制。
六、Controller 是如何运作的:协调循环的本质
1. 核心模式:List-Watch + Reconcile
几乎所有 K8s 控制器(无论是内置的还是自定义 Operator)都遵循同一套编程模型:
- Informer(通过 List-Watch 机制):控制器启动时先做一次 List(全量拉取当前某类资源的所有对象),之后转为 Watch(订阅增量变更事件)。这样既保证了状态的完整性,又避免了持续轮询 apiserver 带来的压力。Informer 内部通常还维护一份本地缓存(Local Cache/Store),后续读取直接查本地缓存,减轻 apiserver 负担。
- 事件入队:当 Informer 感知到资源变化(新增/更新/删除),并不会立刻处理业务逻辑,而是把这个对象的 key 丢进一个工作队列(Workqueue)。这一步的意义在于削峰、去重、限流——短时间内同一个对象反复变化,只需要处理一次最新状态即可,同时可以做失败重试(带指数退避)。
- Reconcile 函数:工作协程不断从队列里取出 key,执行"协调逻辑":读取这个对象当前的期望状态(Spec)和集群中的实际状态(可能来自本地缓存,也可能需要再查询实际资源,比如真实的 Pod 是否存在),如果二者不一致,就采取"最小化的修正动作"让实际状态逐渐趋近期望状态。
2. 为什么是"协调"而不是"事件驱动的一次性处理"
这里有一个非常关键的设计哲学:Reconcile 函数被设计成幂等的、面向最终状态的,而不是"针对某个具体事件做针对性处理"。也就是说,无论触发 Reconcile 的原因是"Pod 被删除了"还是"Node 重启了"还是"纯粹的周期性重新同步(resync)",Reconcile 函数内部的逻辑都是一样的:重新审视一遍"现在期望是什么、实际是什么、该做什么"。
这样设计的好处是极强的容错性:即使某次事件因为网络抖动、组件重启而丢失,只要之后触发了任意一次 Reconcile(包括定期的全量 resync),状态最终都会被修正过来。这就是所谓"最终一致性"在 K8s 里的具体体现,也是为什么 K8s 控制器普遍能够"自愈"——不是因为它监听得多全面,而是因为它压根不依赖"不丢事件"这个假设。
3. 多控制器协作时如何避免冲突
一个资源的状态变化,可能会触发好几层控制器连锁反应(比如上一节的 Deployment → ReplicaSet → Pod)。每一层控制器只负责"自己那一层的期望 vs 实际",不会跨层直接操作。比如 Deployment 控制器只关心"ReplicaSet 数量和版本对不对",具体某个 Pod 是否健康,它并不直接插手,而是交给更下层的 ReplicaSet 控制器去处理。这种"层层委托、各司其职"的分工,使得整个系统即使有几十种控制器同时运行,也不会互相踩踏。
4. Leader Election:保证协调逻辑不会被重复执行
controller-manager、scheduler 这类组件通常会部署多个副本做高可用,但同一时刻只能有一个实例真正在执行协调逻辑(否则多个实例同时对同一个对象做决策,可能产生冲突或者重复扩缩容之类的问题)。这是通过在 etcd(或者通过 apiserver 提供的 Lease 资源)上竞争一把分布式锁实现的——抢到锁的实例成为 leader,负责实际工作,其余实例持续尝试抢锁,一旦 leader 失联(租约到期未续约),其他实例中会有一个接管,保证控制器逻辑始终"恰好一个在跑",而不会因为单点故障而彻底停摆。
七、小结:一以贯之的设计哲学
回过头看,K8s 看似复杂的组件和资源体系,其实反复运用着几条很朴素的设计原则:
- 声明式而非命令式:用户只描述"想要什么",不描述"怎么做到",具体路径交给系统自己摸索。
- 状态存储与协调逻辑分离:etcd 只管存,所有"让状态趋同"的逻辑都在无状态的控制器里,控制器可以随时重启、重新计算,不影响正确性。
- 分层抽象,每层只解决一个问题:Pod 解决容器编组,ReplicaSet 解决数量,Deployment 解决发布,StatefulSet 解决身份,Service 解决发现——没有一个资源试图"包打天下"。
- 插件化边界:CRI/CNI/CSI/Admission Webhook/CRD,凡是容易变化、容易有厂商差异的部分,都被抽成标准接口,交给生态实现。
- 协调优于响应:控制器不依赖"完美接收每一个事件",而是靠周期性重新核对状态,换取极强的容错能力。
理解了这五条,再去看任何一个具体资源或者组件的行为,基本都能推导出"它为什么会这样设计",而不需要死记硬背。

浙公网安备 33010602011771号