ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Kubernetes源码阅读主线:从声明式API到控制器与Informer

2026/10/3 18:04:36 拓冰建站 浏览量
Kubernetes源码阅读主线:从声明式API到控制器与Informer 我是豆包一个把 Kubernetes 源码翻来覆去读了好几遍的人。第一次拿到 K8s 源码的时候我的操作和大多数新手一样打开仓库从根目录开始试图一个文件一个文件地往下啃。两周之后除了把目录结构背了个大概脑子里几乎没留下任何有价值的东西——几百个包、上百万行代码没有主线贯穿的阅读基本等于在迷宫里瞎转。后来我换了个思路先把“K8s 这台机器到底怎么运转”这个问题刻在脑子里再拿着问题去找代码情况才完全不一样。这篇文章要聊的就是这条循序渐进的主线如何理解 Kubernetes 源码背后的核心设计思想、模块架构和运行机制让新手也能顺着一条清晰的路径走进源码而不是被细节活埋。1. 新手啃K8s源码最容易栽在哪三个地方1.1 第一坑把源码当小说从第一页读到最后一页不少初学者的第一反应是找一个“起点”然后线性往下读。但 K8s 源码不是一个线性故事它是一套并行运行的分布式系统。kube-apiserver、controller-manager、scheduler、kubelet 这几个进程是独立启动、通过 HTTP/gRPC 相互协作的并没有一个“主函数”能串起全局。顺着kubernetes/cmd/kube-apiserver往下读读到某个 REST 存储接口时你可能会看到对etcd的调用但触发这次调用的 controller 逻辑却远在另一个进程里。也就是说线性阅读一定会导致上下文断裂。正确的做法是按“一次请求/一个场景”为单元把跨组件的调用链串起来读。比如选定“用户提交一个 Deployment最后 Pod 在一个节点上跑起来”这个场景然后沿着kubectl - apiserver - etcd - deployment controller - replicaset controller - scheduler - kubelet - 容器运行时这条路逐个组件去对应代码。这才能让代码和真实运行行为互相印证。1.2 第二坑一上来就碰 kubelet迷失在节点级细节里很多新手盯上 kubelet是因为它名字直白、看着像“节点核心”。但 kubelet 是 Kubernetes 里代码量最大、接口最多的组件之一它要管 Pod 生命周期、容器运行时 CRI、CNI 网络插件、CSI 存储插件、镜像管理、PLEG 事件循环、节点状态上报……这些逻辑彼此纠缠直接扎进去很容易被syncPod里上百个步骤淹没读一整天也理不出头绪。我的建议是kubelet 必须放到整条链路熟悉之后再读。先读 apiserver 和控制器体系理解“声明状态如何生成”再去看 kubelet 时你就能明确知道它只是“把 etcd 里存储的期望状态拉下来然后努力让本地容器运行时对齐”目标一旦清楚细节只是填充物。1.3 第三坑只看代码不跑起来抓不住“运行机制”有些人喜欢纯静态阅读对着 IDE 的代码高亮就能坐一下午。但 K8s 源码的核心难点不在于语法而在于“运行机制”——什么时候一个 controller 会被触发为什么 etcd 里的数据变了kubelet 马上就知道了这些机制没有一个是在代码字面上能看出来的必须配合一个真实集群去观察。后面我会专门讲怎么搭一个最小集群并验证链路这里先记住一句话跑起来的集群是最好的源码注释。2. 动手前先把K8s的五个角色装进脑子里2.1 声明式API与控制循环K8s的“设计之魂”K8s 与大多数传统运维工具最大的区别在于“声明式”用户不告诉系统“怎么把容器跑起来”只告诉系统“我希望最终达到什么状态”。比如写一个 Deployment 文件说“我要 3 个 nginx 副本”系统内部就启动了一个永不停歇的控制循环不断比较“期望状态3 个”“当前状态实际有多少个”之间的差距然后想办法补齐。源码里无处不在地体现着这种设计。kube-controller-manager 下的每个 controller 都可以抽象成for { 期望状态 : 从某某 Informer 缓存中获取 当前状态 : 从某某 Informer 缓存中获取 if 期望状态 ! 当前状态 { 执行变更操作 } 等待下一次事件 }新手理解这一点极其关键因为它是贯穿所有组件的一条“灵魂线索”。读源码时你要找的就是每个组件里“期望”和“当前”分别定义在哪、比较逻辑在哪、变更动作在哪。2.2 五个核心组件怎么配合一个企业审批流程的类比把 K8s 关键组件翻译成人话可以拿一个企业内部流程来类比组件企业角色核心职责etcd账本/档案室保存集群所有状态的唯一权威数据kube-apiserver前台审批中心唯一能读写账本的入口处理认证、授权、准入、校验kube-scheduler分房专员根据规则决定新 Pod 住进哪台节点kube-controller-manager稽查/执行团队对比期望与现实的差距并推动收敛kubelet一线执行员工在每台机器上真正拉起/销毁容器并持续汇报状态所有组件都通过 apiserver 与 etcd 间接通信不直接互联。这个“一切信息经过 apiserver”的星型结构是读代码时最好用的坐标轴——无论读哪个组件你都要时刻问自己它是在读 etcd经 apiserver还是在写 etcd经 apiserver或者是在等待 etcd 里的某种变化2.3 为什么“心智模型”能挡住读源码时的迷路感很多人低估心象的作用。拿一个城市来类比如果你事先知道这个城市有行政区、商业区、住宅区、交通枢纽你逛每一条街时都能快速定位自己在城市的哪个方位反之没有区域概念直接钻小巷每一条路都可能让你绕晕。K8s 源码也类似pkg/registry是档案室出入口pkg/controller是稽查团队的办公室pkg/kubelet是一线工地pkg/scheduler是分房中心。先在脑子里挂一张这样的地图再去翻具体文件每看到一个函数都能自动归类“哦这是在说分房规则”或“这个属于执行层状态上报”迷路感会大幅减少。3. 从代码仓库到组件入口循着路径先把地图画出来3.1 代码仓库的整体骨架cmd、pkg、staging 各管什么拿到github.com/kubernetes/kubernetes仓库不要指望点开第一个目录就能往下走。先认清这几块地盘cmd/存放各个可执行组件的 main 入口比如cmd/kube-apiserver、cmd/kube-controller-manager、cmd/kube-scheduler、cmd/kubelet、cmd/kubectl。想找一个组件的启动起点一定来这。pkg/核心业务逻辑所在地。pkg/api、pkg/apis是资源对象的 Go 结构体定义与校验逻辑pkg/controller是各种控制器pkg/scheduler是调度框架pkg/kubelet是节点代理pkg/proxy是 kube-proxy 的相关实现。staging/作为独立仓库发布的公共代码比如k8s.io/api、k8s.io/apimachinery、k8s.io/client-go其实都从这里同步出去。读大多数业务代码时你会频繁看到对k8s.io/client-go的引用这就是 informer 机制所依赖的客户端库。api/、build/、test/、cluster/OpenAPI 规范、镜像构建脚本、测试代码、部署脚本初期不用细看。新手最容易搞混的是pkg/api和staging/src/k8s.io/api。简单说前者包含内部 API 对象与版本转换逻辑后者是供外部模块引用的版本化对象定义。读具体对象结构时两边都会遇到但如果你只是想了解 Pod 长什么样打开staging/src/k8s.io/api/core/v1/types.go是最直接的路径。3.2 四个主要组件的main函数都在哪K8s 各组件的启动路径非常统一cmd/kube-apiserver/app/server.go - main 入口在 cmd/kube-apiserver/apiserver.go cmd/kube-controller-manager/app - main 入口在 cmd/kube-controller-manager/controller-manager.go cmd/kube-scheduler/app - main 入口在 cmd/kube-scheduler/scheduler.go cmd/kubelet/app - main 入口在 cmd/kubelet/kubelet.go它们内部的套路也一致先构建options再调用app.Run()最终启动 HTTP 服务或控制循环。你用 IDE 双击main()就能从零开始逐行跟踪一个组件的初始化过程。这比从一个中间层函数倒着往上找入口轻松得多。3.3 第一个断点应该下在哪apiserver还是controller如果只能选一个地方作为“第一次断点”我推荐cmd/kube-apiserver/app/server.go里的CreateServerChain。它是 apiserver 初始化核心链路的起点能让你看到过滤器链Filter Chain、路由构建、存储工厂StorageFactory被组装在一起的顺序。在这里下断点是性价比最高的做法——你没看到任何业务细节但能看见整个 apiserver 的骨架。之后再去追具体的资源处理逻辑时你会知道每一条 RESTful 请求背后是这一整条链在支撑。4. 主线路第一站kube-apiserver的数据流转链路4.1 一次Pod写入请求会经过哪几道门一个kubectl apply发出来的 Pod 请求到达 apiserver 后要依次穿过几道核心关卡。对应源码的位置如下认证Authentication识别“你是谁”。相关位置在staging/src/k8s.io/apiserver/pkg/authentication/request。授权Authorization判断“你能做什么”。代码核心在staging/src/k8s.io/apiserver/pkg/authorization。准入控制Admission通过插件修改或拒绝请求。常见插件如NamespaceLifecycle、LimitRanger、MutatingAdmissionWebhook、ValidatingAdmissionWebhook在pkg/admission与plugin/pkg/admission下实现。校验与默认值设置Validation/Defaulting比如给 Pod 补充默认的 DNS 策略、校验镜像名格式。定义在pkg/apis/core/validation。存储Storage真正把对象写入 etcd。代码在pkg/registry/core/pod/rest.go及其存储策略层。动手验证很容易在你自己的测试集群里运行kubectl apply时给 apiserver 启动参数加上--v5日志会详细打印出请求触发了哪些 admission 插件、经过了哪一步校验。你会非常直观地看到这几道门是真实存在且有顺序的。4.2 apiserver里最值得精读的三个函数如果时间有限我建议把火力集中在三个函数上CreateServerChaincmd/kube-apiserver/app/server.go看整体装配。createHandlerstaging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go看一次 REST 请求如何被路由到具体的存储处理器以及响应怎么返回。Store.Createstaging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go看资源对象经过各种资源版本处理后被写入后端存储的细节。这三个函数连起来基本覆盖了“一个对象如何进入集群”的整条主干路径。其余如 RESTful 转换、Status 子资源处理等都是旁支二期再看也不迟。4.3 顺着“写etcd”这个动作理解K8s的状态存储读到这里很多人会问etcd 里到底存的什么打开 etcd 的 key 空间可以看到类似这样的路径/registry/pods/default/demo-pod /registry/deployments/default/nginx /registry/services/specs/default/kubernetes也就是说K8s 的每个资源对象在 etcd 里都有对应的 keyvalue 是序列化后的对象数据。apiserver 是这些数据的唯一读写入口其他组件想要读状态都必须经过 apiserver 暴露的 API。这个设计带来一个关键效果即便 etcd 挂掉只要 apiserver 还在它还具备一段时间的缓存能力反过来其他组件无论如何也无法绕过 apiserver 去直接改 etcd。这保证了状态变更的一致性。提示初次下拉代码后建议先用go build ./cmd/kube-apiserver确认本地构建环境。K8s 对 Go 版本要求很严格不同版本对应不同 Go 小版本构建失败不要急于改代码先检查.go-version文件或者go.mod里的声明。5. 主线路第二站控制器与InformerK8s的“心跳”5.1 Informer机制Reflector、DeltaFIFO、Indexer是什么控制器要工作必须先感知状态变化。它不会每秒向 apiserver 全量拉取数据而是使用一套叫 Informer 的机制做到“变化时增量感知本地缓存抢先读”。三个核心概念按数据流排布如下Reflector位于每个 Informer 的第一层向 apiserver 发起ListAndWatch。先 List 一次全量对象然后建立长连接 Watch 后续变化把变化封装成 Update/Add/Delete 事件。DeltaFIFO一个带去重能力的增量队列Reflector 将事件推送进来消费者从中取走并处理。它能合并短时间内的多次变更避免重复劳动。Indexer一个本地缓存通过ThreadSafeStore实现保存了最新状态的全部对象。控制器除了读期望状态和当前状态时直接访问 Indexer几乎不会反复打 apiserver。代码主战场在staging/src/k8s.io/client-go/tools/cache/目录其中reflector.go、delta_fifo.go、thread_safe_store.go是必读三兄弟。INFORMER 机制并不难难在没有动手场景。建议你写一个几百行的 Demo用 fake client 往 Informer 里添加事件观察回调函数触发顺序很快就能掌握它的本质。5.2 控制器的Reconcile循环到底在循环什么控制器真正干活的地方叫 Reconcile协调函数代码形态千差万别但套路一致func (c *Controller) syncHandler(key string) error { // 1. 根据 key如 namespace/name从 Indexer 拿到当前对象 obj, exists, err : c.indexer.GetByKey(key) // 2. 从同一缓存中获取期望状态比如期望副本数 expect : getExpectReplicas(obj) // 3. 从另一缓存中获取当前状态比如现存 Pod 列表 actual : getCurrentPods(obj) // 4. 比较有差距就创建/删除 Pod if len(actual) expect { createPods(diff) } return nil }这段伪代码在所有控制器的核心逻辑里都能找到影子。ReplicationController、DeploymentController、ReplicaSetController、EndpointController 无非是把“期望值来源”“当前状态来源”“改变动作”替换成不同资源。新手常犯的错误是只盯着syncHandler而忽略“谁调用它”。实际上控制器通过 workqueue 拿到变化 key再调用syncHandler。workqueue 提供失败重试、延迟、限速功能不读staging/src/k8s.io/client-go/util/workqueue里的实现你很难理解为什么一个控制器在异常场景下不会疯狂重试。5.3 用一个小型伪代码控制器验证理解读完 Informer 和 workqueue 后动手实现一个迷你控制器是极好的巩固方式。当初我花了一个周末写完这个骨架对后续读源码的效率提升非常明显package main import ( context fmt time k8s.io/client-go/tools/cache k8s.io/client-go/util/workqueue ) type MiniController struct { indexer cache.Indexer queue workqueue.RateLimitingInterface informer cache.Controller } func (c *MiniController) processItem(key string) error { obj, exists, err : c.indexer.GetByKey(key) if err ! nil { return err } if !exists { fmt.Printf(对象 %s 被删除执行清理\n, key) return nil } fmt.Printf(协调对象 %s当前版本为 %s\n, key, obj.(*MyObject).ResourceVersion) return nil }跑通的标志是你创建一条对象几秒内看到协调日志删除对象再看到清理日志。这个过程会帮助你建立起对“事件驱动”最直接的体感比读十遍文档都管用。6. 一条完整的主线串讲从apply yaml到Pod跑起来6.1 链路全景一次Pod调度部署的完整旅程把所有核心模块串成一条线最有价值的场景就是“部署一个 Deployment”。流程如下每个环节都对应源码位置用户kubectl apply发送 Deployment 对象到 apiserver。apiserver 完成认证、授权、准入、校验写入 etcdkey 类似/registry/deployments/default/nginx。DeploymentController 的 Informer 收到 Add 事件进入工作队列Reconcile 发现“需要 3 个 Pod 对应的 ReplicaSet 存在”创建 ReplicaSet 对象。ReplicaSetController 收到 ReplicaSet Add 事件再 Reconcile 发现“需要 3 个 Pod”创建 3 个 Pod 对象。Scheduler 观察未绑定的 Pod通过过滤Predicate选择合适节点打上spec.nodeName字段并写回 apiserver。Kubelet 通过自身 Informer 观察到分配的 Pod调用容器运行时 CRI 接口拉镜像、创建 Sandbox 与业务容器。Kubelet 定期上报 Pod 状态到 apiserver最终用户在kubectl get pods里看到 Running。这七步就是 K8s 最核心的一条大动脉。任何复杂功能——Service、Ingress、自动伸缩、滚动更新——不过是大动脉上的分支系统。理解大动脉再看分支就顺手得多。6.2 沿途需要重点停留的“观察点”在这条链路上我建议沿途停靠四个观察点每个观察点都要问出“为什么”DeploymentController 为什么只创建 ReplicaSet而不是直接创建 PodReplicaSetController 怎么知道要创建多少个 Pod期望数量从哪来Scheduler 挑选节点的核心逻辑在pkg/scheduler/framework它为什么设计成插件化框架Kubelet 的syncPod的第一步为什么是“更新 Status”而不是“创建容器”这四个问题每一个都能牵引出一整片代码区域但它们都不是独立的——答案都归功于“期望状态驱动”这个整体设计。把这四个问题弄明白源码就不再是一堆零散函数而是一个层层逼近方向的目标系统。6.3 亲手复现链路的方法开日志、抓etcd、下断点纸上谈兵没意思我推荐三种方式亲手复现这条链路。第一种用kind或kubeadm快速起一个单节点集群。不要在生产环境试本地最好。给各组件启动命令加上--v5或更高日志级别然后执行kubectl create deployment观察不同组件日志出现的先后顺序。第二种直接在 etcd 里观察数据落盘。执行ETCDCTL_API3 etcdctl get /registry/deployments/default/nginx --prefix你会看到 Deployment、ReplicaSet、Pod 等不同资源类型在 etcd 里的存储方式。这能印证“控制器生成的后续对象也会写入同一系统”的事实。第三种用 Delve 调试器在本地源码中下断点。推荐断点位置是staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go的 createHandler 以及pkg/controller/deployment/deployment_controller.go的reconcileDeployment。跟进去单步执行你每一步都能看到变量的期望值和当前值这是静态阅读完全无法替代的体验。注意进行本地源码调试时使用与集群相同版本的源码分支断点才能准确对应到运行的二进制。版本不匹配会导致行号偏移调试体验大打折扣。7. 三周循序渐进的阅读计划和一个关键提醒7.1 第一周看整体骨骼不求甚解第一周的任务是建立全局地图。每天的目标很简单部署一个本地集群并熟悉常用 kubectl 操作。把cmd/下四个核心组件跑通一遍理解各自启动参数。读staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go里的ObjectMeta搞懂metadata.name、namespace、resourceVersion、uid这几个字段为什么存在。用 etcdctl 浏览一遍 etcd 里的常见 key 前缀。这个阶段允许不懂细节遇到复杂的函数可以先跳过。关键产出是一张自己画出来的组件协作图和一个“哪个场景对应哪个组件”的索引表。7.2 第二周跟一个控制器的完整闭环第二周只做一件事把 DeploymentController 和 ReplicaSetController 的闭环走通。路线表如下天数任务输出第1天读 Informer 相关源码Reflector、DeltaFIFO、Indexer能画出数据流第2天读 workqueue 的事件进入与消费逻辑能说出重试机制原理第3天读 DeploymentController 的 sync 流程能解释控制器创建 ReplicaSet 的条件第4天读 ReplicaSetController 的 sync 流程能解释控制器如何选择 Pod 计算 diff第5天本地集群做“改副本数”实验对照日志能写出事件触发序列这个阶段是整个阅读计划的分水岭跨过去之后读其他任何控制器都只是套模板。7.3 第三周钻进kubelet的Pod生命周期有了前两周的基础第三周再挑战 kubelet 就游刃有余了。按优先级排序读这几个模块pkg/kubelet/kubelet.go里的syncPodPod 创建的顶层入口。pkg/kubelet/plegPod 生命周期事件来源负责检测容器死掉、重启等状态变化。pkg/kubelet/container与pkg/kubelet/cri对容器运行时抽象层可以看到 K8s 如何屏蔽 Docker 与 containerd 的差别。kubelet 里的坑不少但每一处都有规律可循。记住它的核心使命只有一句话让本机容器运行时对齐 etcd 里声明的期望状态。抓稳这一句其中再复杂的逻辑都有一个主线挂靠点。7.4 一个关键提醒源码版本必须与你的集群一致这是阅读源码踩过最深的一个坑。K8s 仓库的 master 分支推进速度非常快某天你看了开发版代码里的某项新逻辑然后去查 v1.26 的稳定版集群发现根本没有这块执行路径轻则困惑重则产生系统性误读。因此严格照这条规则做确定自己要研究的版本比如v1.26.0然后切到对应的 tag 或 release 分支去读源码集群、文档、源码三者版本保持一致。如果你想验证某个行为尽可能用kind起一个同版本集群来做实验。版本一致加上动手验证才能确保你读到的“机制”是系统的真实机制而不是某个中间版本里的临时产物。最后再分享一个个人习惯读 K8s 源码时我会在源码文件里写下大量注释标出“这个函数为什么存在”和“它解决了什么失控风险”。比如准入控制里的LimitRanger表面上只是限制资源水位本质上是防止用户无节制地申请内存和 CPU 导致集群整体过载。带着这种“防什么错”“补什么漏”的视角源码里很多看似平凡的逻辑都会变得鲜活。希望这条主线也能帮你把 K8s 源码从一座迷宫变成一张能被你随手展开的地图。