很多新手拿到 Kubernetes 源码之后,第一反应都是打开 GitHub 仓库,从cmd/kube-apiserver一路往下读,结果没看几天就被各种接口、cacher、informer 绕得头晕,最后无奈放弃。这个现象太普遍了。我当初啃 K8s 源码的时候也踩过同样的坑,整整浪费了一个多月才摸到门路。
Kubernetes(简称 K8s)的源码量极其庞大,单是kubernetes/kubernetes主仓库就有两百多万行 Go 代码。如果你的目标是理解核心设计思想、模块架构和运行机制,那直接逐行读源码是效率最低的方式。正确的做法是分层推进:先建立模块地图,再沿着一条关键业务链路穿透源码,最后再回头补细节。这篇博客就沿着这条思路,把 K8s 源码的主线给你捋清楚。
1. 内容整体设计与思路拆解
1.1 为什么新手直接啃源码会陷入细节
先聊一个根本问题:为什么那么多人都死在直接读源码这件事上?
K8s 源码的复杂度体现在三个维度。第一是代码规模大,vendor目录就占据了仓库体积的大半壁江山,加上真实业务代码,阅读时很容易在依赖关系里迷失方向。第二是抽象层级深,一个简单的 Pod 创建请求,从 kube-apiserver 接收到 etcd 存储,中间要经过 API Scheme、REST 映射、Admission、Validate、Registry、Storage 等多个抽象层,每一层都是一堆接口,没有全局视野的话根本不知道自己在哪。第三是并发模型复杂,K8s 内部几乎所有机制都建立在 informer、list-watch、worker queue 这套异步模型上,如果你连事件分发的路径都没搞清楚,看哪个模块都会觉得像在看天书。
这三个维度叠加在一起,就形成了一个死循环:不理解模块架构就看不懂单一组件的代码,看不懂单一组件就建不起全局认知。所以正确的策略不是“读源码”,而是“带着架构问题去验证源码”。你脑子里必须先有一张草图——每个组件负责什么、跟谁通信、数据往哪流——然后才谈得上用源码去充实这张草图。
1.2 源码、架构与运行机制的关系
很多人把“看源码”和“理解架构”混为一谈。实际上这两件事的目标完全不同:源码告诉你“是什么”,架构告诉你“为什么这样设计”,运行机制告诉你“运行时到底发生了什么”。
一个特别好的类比是拆一台汽车发动机。只盯着螺丝和齿轮看,你看到的是上千个零件的物理存在;看维修手册上的系统分解图,你才知道曲轴、活塞、气门是怎么配合的;再把这台发动机装到车上跑一圈,踩油门听声音,才算真正理解它的工作逻辑。K8s 源码对应的就是那堆零件,模块架构是维修手册,运行机制就是那台运转的发动机。三者缺一不可,顺序还不能乱。
所以我建议的路径是:先从架构入手建立全局图,再从图里挑一条主链路去读源码,最后通过实际操作(比如部署一个集群、跑一个应用)去验证源码里的逻辑。这篇博客主体部分就按这个路径展开。
2. 核心模块架构解析
2.1 控制面与数据面的划分逻辑
K8s 源码顶层目录结构已经暗示了它的架构分层。打开cmd/目录,你会看到一堆组件入口目录,但真正的主干只有 7 个:kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、kubectl和kubeadm。这里面前三个属于控制面组件,后两者是节点组件(如果按传统分布式系统术语讲,kubelet 和 kube-proxy 就是数据面的一部分,但 K8s 的数据面实际上由 Pod 网络完成,kubelet 更准确地说是“节点代理”)。
先理解这个划分,因为整个 K8s 设计的第一性原理就是:控制面负责做决策,节点面负责执行决策。控制面里的 kube-apiserver 是所有组件通信的中枢,它既不创建 Pod,也不调度 Pod,它只做一件事——提供数据读写和校验的唯一入口。kube-controller-manager 是一堆控制器集合,它负责不断把“当前状态”调向“期望状态”。kube-scheduler 则专门负责“新 Pod 应该放到哪个节点”这类决策。而 kubelet 是节点上的“大脑”,它只跟 kube-apiserver 打交道,接收 Pod 配置,调用容器运行时真正把容器跑起来。
这个划分带来一个重要的设计结果:控制面与节点面是通过 API 松耦合的。控制面从不直接 SSH 到节点上发指令,它只把期望状态写入 API Server 的存储(etcd),节点上的 kubelet 自己通过 watch 机制去感知变化。这就像老板和员工的协作模式:老板只把任务写进共享任务板,员工自己盯着任务板接活,而不是老板挨个打电话指挥。理解了这一点,后面所有的源码阅读都会顺很多。
2.2 模块间通信的基石:List-Watch 机制
如果你只能从 K8s 源码里理解一个机制,我强烈建议优先选List-Watch——它是窥探整个 K8s 运行机制的第一把钥匙。
简单说,一个组件如果想获取某种资源(比如 Pod)的变化信息,流程是:
- 先调用 List 接口,把当前所有 Pod 数据全量拉下来,本地建缓存。
- 再调用 Watch 接口,建立长连接,服务端(kube-apiserver)会持续推送增量变化事件(如 Pod Added、Modified、Deleted)。
在源码层面,这个机制的实现在k8s.io/client-go/tools/cache包里,核心有两个关键对象:Reflector和Informer。Reflector 负责上面说的“List 一次 + Watch 增量”,它把收到的变化事件放进一个 DeltaFIFO 队列;Informer 则从队列里取出事件,通过 ResourceEventHandler 回调分发给你注册的处理器,同时把你的资源更新到本地Indexer缓存里。
这个设计解决了一个分布式系统最头疼的问题:多个组件同时监听同一份数据,如何保证一致性和实时性。List 解决全量同步问题,Watch 解决增量实时问题,本地缓存解决读多写少的性能问题。后面你看任何控制器的代码,注意力放在“它注册了哪些事件的回调函数”上,就能快速理解这个控制器的触发条件是什么。
2.3 各组件源码入口与核心目录指引
给大家整理一份源码地图,方便你有目标地进入对应目录。不要试图把整个仓库读完,只挑这些关键文件看就足够建立主线。
| 组件 | 源码入口目录 | 推荐优先阅读的文件 | 核心职责一句话 |
|---|---|---|---|
| kube-apiserver | cmd/kube-apiserver | pkg/controlplane/apiserver.go | 所有 API 请求的入口、鉴权、校验、存储 |
| kube-controller-manager | cmd/kube-controller-manager | pkg/controller/namespace、pkg/controller/deployment | 持续调谐“当前状态”到“期望状态” |
| kube-scheduler | cmd/kube-scheduler | pkg/scheduler/core/generic_scheduler.go | 为新 Pod 选择合适的节点 |
| kubelet | cmd/kubelet | pkg/kubelet/kubelet.go | 节点代理:驱动容器运行时执行 Pod 声明 |
| kube-proxy | cmd/kube-proxy | pkg/proxy/iptables/proxier.go | 维护节点网络规则,实现 Service 转发 |
| kubectl | cmd/kubectl | pkg/kubectl/cmd/run.go | 命令行客户端,本质是请求 API 的封装 |
这里特别提醒一点:这些组件虽然代码量巨大,但真正核心的逻辑往往集中在少数几个文件里。比如 kube-scheduler 的核心调度逻辑其实只有generic_scheduler.go里的Schedule()函数,重点看它的预选(Predicate)和优选(Priority)两个阶段就够了,其他都可以先略过。
3. 核心运行机制实现详解
3.1 从一条 Pod 创建链路看源码的串联
架构了解之后,我强烈建议新手沿着一条Pod 创建完整链路去读源码。因为这条链路几乎穿过了所有核心组件,读完之后你对 K8s 运行机制的认识就不再是零散的点了。
这条链路大致是这样的:
- 你执行
kubectl apply -f pod.yaml,这在底层就是 kubectl 向kube-apiserver发送了一个 POST 请求,路径是/api/v1/namespaces/{namespace}/pods。 - kube-apiserver 收到请求后,通过
pkg/registry/rest中的 REST 框架处理,经过认证(Authentication)、授权(Authorization)、准入(Admission)和资源校验(Validation)四道关卡后,最终把 Pod 对象写入 etcd。 - kube-scheduler 通过 informer 监听到这个新 Pod 的事件(因为它的
schedule队列收到了这个 Pod),尝试为它寻找一个合适的节点,找到后将PodSpec.NodeName字段更新回 API Server。 - 目标节点上的 kubelet 同样通过 informer watch 到 Pod 被调度到了自己节点,于是调用容器运行时(CRI,如 containerd)拉取镜像、启动容器。
- 容器启动成功后,kubelet 再把 Pod 状态通过 API Server 更新为 Running。
这条链路每一步都能在源码里找到对应实现。比如第 2 步,重点是k8s.io/apiserver/pkg/endpoints/handlers里的CreateHandler;第 3 步,重点是pkg/scheduler里的调度流程;第 4 步,重点是pkg/kubelet/kuberuntime里对 CRI 的调用。
顺着这条链路走一遍,你就明白了一个关键问题:组件之间没有直接的 RPC 调用,所有状态变更都是通过“写 API Server + watch API Server”这一种方式完成的。这一点是整个 K8s 架构最精妙也最反直觉的地方,很多从微服务架构转过来的人一开始都想不通“怎么没有服务注册发现”、“怎么不直接调用接口”。
3.2 控制器模式与水平调谐
只要你打算深入 K8s 源码,就绕不开“控制器模式”这个概念。整个kube-controller-manager里的每个控制器,本质上执行的都是一个无限循环:
for { 期望状态 := 从 API 获取 (spec) 当前状态 := 从 API/外部系统获取 (status/实际资源) 对比期望和当前,计算差异 执行操作消除差异 等待下一个周期 }这个模式还有名字,叫调谐循环(Reconcile Loop)。比如 ReplicaSet 控制器做的事,就是保证集群里某个 Deployment 的 Pod 副本数维持在期望值。它通过 informer 监听 ReplicaSet 和 Pod 的事件,一旦发现实际 Pod 数少于期望数,就调用 API Server 创建新的 Pod;反之则删除多余的 Pod。
源码层面,这个模式的核心在pkg/controller/controller_utils.go和各个控制器的syncHandler函数。以pkg/controller/deployment/deployment_controller.go为例,你会发现它结构非常统一:一个NewDeploymentController初始化 informer 和 worker queue,一个processNextWorkItem从队列里取任务,一个syncDeployment执行真正的调谐逻辑。所有控制器几乎都是这个骨架,看熟一个,再看其他控制器就是分分钟的事。
我个人的体会是,理解控制器模式之后,你就掌握了读 K8s 源码的“心法”。因为整个系统里大量机制 —— 不管是大到 Deployment、StatefulSet,还是小到 Node Lifecycle、垃圾回收 —— 都是这个模式的不同变体。
3.3 API Server 的请求处理管线
API Server 是 K8s 的大脑,也是所有请求的必经之路,它的代码结构很值得单独拆开讲。
一个请求到kube-apiserver之后,会经历一条清晰的管线:
- POST 请求到达后,先由
pkg/apiserver/server.go里的 HTTPS 监听器接收。 - 接着进入
k8s.io/apiserver/pkg/endpoints/filters里的一连串过滤器,包括 RequestTimeout、Authentication、Audit、Authorization、Impersonation 等,这部分其实就是 Go 中间件链。 - 过滤器通过后,请求会进入
mux路由层,根据请求路径找到对应的资源 handler。这个 handler 在k8s.io/apiserver/pkg/registry/generic/registry里,核心是一个Store结构体。 Store.Create()会先做默认补全(defaulting)、合法性校验(validation),然后经过 Admission 链,最后调用底层的Storage接口把数据写入 etcd。
源码阅读建议按这个顺序走,先看中间件链(filters),再看资源 handler(registry),最后看存储层(storage/etcd3)。特别关注一下 etcd3 的存储实现,它有一套比较复杂的编码、序列化、版本控制逻辑,但核心就一件事:把任意 K8s 资源对象转成 KV 格式存进 etcd。
这里我给大家一个实用贴士:如果觉得直接看源码断点太难,可以先开一个单节点的kubeadm集群,然后用kubectl apply手动创建资源,同时把 kube-apiserver 的--v=6日志打开,你能看到完整的请求处理记录,配合源码看效率翻倍。
3.4 kubelet 与节点运行时
kubelet 是唯一一个每个节点上都跑、且在源码里管理“真正的容器进程”的组件。很多人读到这里就开始卡壳,因为 kubelet 内部有太多 goroutine、channel、cache,看半天不明白它在干嘛。
其实 kubelet 的核心逻辑可以抽成三步:
- 同步 Pod:通过 informer/pleg 感知本节点需要运行的 Pod 列表,跟当前实际运行的容器对比。
- 计算差异:针对每个 Pod,计算需要创建、重启、停止的容器。
- 调用 CRI 执行:通过
internalapi.Runtime调用 containerd/CRI-O 等运行时,执行 Sandbox/Container 的生命周期操作。
读 kubelet 源码时,建议重点看两条路径:一是pkg/kubelet/kubelet.go里的syncLoop主循环(这是 kubelet 所有动作的驱动源头);二是pkg/kubelet/kuberuntime/kuberuntime_manager.go里的syncPod函数(这是真正“干活的”地方)。
特别提醒一句:kubelet 是最容易出现“过度读源码”的组件,因为里面涉及 pod workers、housekeeping、probe manager、image manager 等一大堆并发模块,新手一头扎进去很容易失去主线。我的建议是,只保留上面说的主循环和 syncPod 一条主线路,其他模块等以后有具体问题再看。
4. 新手进阶之路:从源码到实战验证
4.1 一套循序渐进的三阶段学习路径
说了这么多,到底应该按照什么顺序学?我根据自己带团队的经验,整理了一套非常适合新手的路径,总共三个阶段。
第一阶段,架构与机制认知(1-2 周)。不必读任何源码,只看文档和架构图,把上面提到过的控制面/节点面划分、List-Watch、控制器模式、调谐循环四件事彻底搞懂。这个阶段你可以结合实验操作:用 kind 或者 kubeadm 部署一个集群,创建 Deployment、Service、Pod,观察各个组件日志,建立直观感受。
第二阶段,主干链路源码阅读(3-4 周)。按照第 3 节那条“Pod 创建链路”走一遍源码,从 kubectl 到 kube-apiserver,再到 etcd,再到 scheduler,最后到 kubelet。读的时候要带着问题去读,在源码里找对应答案,不要逐行念经。这期间最好用调试工具(如 GoLand 断点,或者--v=6日志)边跑边看。
第三阶段,专题深入研究(持续)。当你主线通了之后,就按需去研究扩展点:网络(CNI 实现)、存储(CSI 实现)、调度扩展、自定义 CRD/Controller 等。这阶段的重点已经不是“读懂 K8s”,而是“基于 K8s 做二次开发或深度排障”。
这三阶段的设计思路,本质上就是不断在“全局-局部-全局”之间切换。我个人特别不建议一上来就研究某个边缘模块(比如 apiserver 里的某个 storage encryption),那会严重破坏初期的成就感。
4.2 工具与资料推荐
源码阅读工具上,我的建议很简单:本地克隆kubernetes仓库,选一个稳定版本(比如 v1.26.0),然后用 GoLand 打开。GoLand 的代码跳转、断点调试、调用层级对读源码帮助极大,IDEA 这类的工具在符号查找上远比命令行高效。
一个很多人不知道的小技巧是:K8s 源码编译虽然重,但你可以只编译单个组件。比如想看 scheduler 逻辑,在仓库根目录跑:
make kube-scheduler然后在本地跑一个 fake cluster,配合kind load加载这个二进制到集群里,就能用真实集群环境测试你修改过的 scheduler 行为。这是把“读源码”变成“调试源码”的最有效方式。
另外推荐两个在线资源:k8s.io/community里的 KEP(Kubernetes Enhancement Proposal)文档,非常有助于理解某个功能的设计背景;还有client-go库里的tools/cache和workqueue,这是理解 informer 建模的关键依赖,值得提前花点时间单独精读。
4.3 常见问题与排查技巧实录
读 K8s 源码过程中我几乎踩遍了新手会遇到的坑,整理几个最常见的,你们遇到直接对照处理。
问题一:While compiling with Go modules,本地仓库报错没有 vendor 目录。
K8s 从某个版本开始使用 Go modules,但主仓库仍然依赖vendor/目录。如果你 clone 之后发现没有 vendor,多半是用的--depth=1浅克隆导致的。解决办法是删掉仓库完整重新 clone。
问题二:读 informer 源码,不知道 DeltaFIFO 和 Indexer 各自的作用。
这两个对象是 informer 机制里的核心,也是新手最懵的地方。一句话区分:DeltaFIFO 负责“排队等待处理的事件”,Indexer 负责“已经处理完毕的资源的本地缓存”。读代码时先区分这两个,思路会清晰很多。
问题三:集群部署使用 kubeadm 初始化时报 preflight 检查失败。
[init] using kubernetes version: v1.26.0 [preflight] running pre-flight checks虽然这条日志看着很吓人,但绝大部分 preflight 失败都不是 K8s 自身的问题,而是主机环境不满足,常见有 swap 未关闭、端口被占用、CRI socket 未配置、内核参数缺失。在单机测试场景,建议直接跑:
kubeadm init --ignore-preflight-errors=Swap把这个检查绕过,优先把集群搭起来跑通主干链路。
问题四:断点调试时,多个组件同时运行,不知道断点该打在哪。
建议不要同时调试多个组件。先从最单一的 kube-scheduler 入手,只跑 scheduler,用kubectl create pod触发调度,断点打在generic_scheduler.go的Schedule()入口。这样可以完全隔离变量,对新手友好得多。
4.4 我个人实操中的两点体会
最后聊两个我用一次次失败换来的经验。
第一,千万别把源码当小说一样从第一行读到最后一行。源码是给编译器看的,不是给人从头读到尾的。正确做法是——先找一个具体的“锚点问题”(比如“scheduler 是怎么把我的 Pod 绑定到节点的”),然后顺着这个问题的调用链反向展开。每读一个函数,都要问自己一句:这个函数解决了哪个环节的问题?跟我的锚点问题有什么关系?用这种方式,读到的每一行代码都是有用的,而且记忆特别牢。
第二,要珍惜折腾环境的时间。搭建本地调试环境、跑通 kubeadm、打断点看堆栈,这些过程看起来“没在读书”,但事实上是在把抽象机制变成肌肉记忆。我自己带过的不少新人,读源码读得昏昏欲睡,一旦亲手把 K8s 组件改坏、再定位原因、再修好,马上就开窍了。这个项目的核心从来不是看多少行代码,而是建立一套“出问题时知道去哪定位”的直觉。等到你面对一个陌生模块能说出——啊,这个模块在调谐这个状态,它监听的是那几个资源,状态变更会推到这个队列,这个队列又由哪几个 worker 消费——K8s 源码这关,就算真正过去了。