从源码级别理解k8s
从源码理解 Kubernetes,最容易踩的坑就是一上来就想啃 kubernetes/kubernetes 这个主仓库——它是一个几百万行代码的巨型 monorepo,没有主线索地硬啃根本读不动。比较高效的路径是反过来,从最小、最纯粹的那块代码入手,先建立起”控制器到底是怎么写出来的”这个心智模型,再回头去看真正的 scheduler、kubelet 这些大组件,因为你会发现它们的核心循环骨架,跟那个最小示例几乎是同一套模式的放大版,这也正好呼应了我们前面聊 Informer/reconcile loop 的所有内容。
第一步强烈建议先读 kubernetes/sample-controller 这个仓库(github.com/kubernetes/sample-controller),它是官方专门为了教学目的维护的一个”迷你控制器”参考实现,代码量很小(主逻辑就几百行),但完整展示了一个真实控制器该有的全部骨架:怎么用 client-go 建 Informer、怎么注册 AddFunc/UpdateFunc/DeleteFunc 事件回调、事件怎么被塞进一个 workqueue、一个 worker 循环怎么从队列里取出 key 再做 reconcile、失败了怎么退避重试。这套模式不是 sample-controller 自己独创的,而是整个 Kubernetes 生态(包括 kube-controller-manager 内部那几十个控制器、包括我们这几天一直在用的 Crossplane)共用的同一套骨架,读懂这一份代码,相当于同时读懂了后面几十个更复杂组件的”通用语法”,性价比最高,一定要作为起点。
读完 sample-controller,再往底层看一层,去翻 client-go 这个仓库里 Informer 和 workqueue 的具体实现(在主仓库里的物理路径是 staging/src/k8s.io/client-go/tools/cache/ 和 staging/src/k8s.io/client-go/util/workqueue/)。这里要理解一个仓库结构上的关键点:kubernetes/kubernetes 这个主仓库其实不是一堆互相独立的东西堆在一起,client-go、apimachinery、api 这些你在 GitHub 上单独能搜到的仓库,源头其实都在主仓库的 staging/ 目录下面,是通过一个机器人自动同步发布出去的只读镜像,真正开发是在主仓库的 staging 目录里做的,这一点知道了能省你不少”我该去哪个仓库看”的困惑。cache 包里重点看 SharedIndexInformer 的实现,能看到它内部其实是 Reflector(负责真正发起 List-Watch)+ DeltaFIFO(缓冲增量事件)+ 本地 Indexer(缓存)三层组合,这比我们之前口头描述的”Informer 机制”细致得多,能看到具体是怎么把 etcd/apiserver 的 watch 流转换成本地事件回调的。
有了这个通用骨架垫底,再去看具体某个大组件就会轻松很多,我建议直接挑 kube-scheduler,因为我们已经从概念上把它的 Filter/Score/Bind 流程讲得很细了,这时候去对照源码验证会特别有成就感。入口在 cmd/kube-scheduler/scheduler.go,往里跟会走到 pkg/scheduler/scheduler.go,这个文件里的 Run(ctx context.Context) 方法就是调度器真正启动主循环的地方(我刚查了一下当前 master 分支代码,这个方法里会用 wait.UntilWithContext(ctx, sched.ScheduleOne, 0) 不断循环调用 ScheduleOne,也就是我们之前讲的”每次从队列取一个 Pod、跑完一整个调度周期”这个函数,内部实际执行调度逻辑委托给 sched.SchedulePod)。想看 Filter/Score 插件体系具体怎么定义的,去看 pkg/scheduler/framework/interface.go,里面能直接找到 FilterPlugin、ScorePlugin、PreFilterPlugin 这些接口定义,读接口比读具体某个插件的实现更划算,因为接口方法集直接告诉你”这一层扩展点到底能做什么”,不需要把每个插件的实现都看一遍。同样的路数,kubelet 可以从 pkg/kubelet/kubelet.go 里的 syncLoop、syncPod 入手,对照我们之前讲的”kubelet 怎么把 Pod 落地成容器”那一段。
读大型 Go 项目源码有几个通用技巧,不只适用于 Kubernetes:优先搜接口定义(interface)而不是直接一头扎进某个具体实现,接口往往就是这一层设计意图的浓缩;不要试图线性通读一整个包,而是挑一条你已经有具体问题的”竖切面”去追(比如”一个 Pod 从创建到被 Bind,中间到底调用了哪几个函数”),带着已有的问题去读代码,比漫无目的地扫代码有效得多,这也是为什么我建议你先把这几天我们聊的这些概念性问题都想清楚,再去对照源码,而不是反过来;另外善用代码搜索工具(GitHub 自带的搜索、或者本地 clone 下来用 grep/ripgrep)按函数名或者类型名精确定位,比按目录一层层点进去快得多。
还有一个很实用、而且你现在已经具备条件用的技巧:拿你自己的 kind 集群做实验田。给 kube-scheduler 或者 kubelet 这些组件的 Pod 加上更高的 --v 日志级别(比如 --v=4 甚至更高),Kubernetes 各组件内部在很多关键函数入口和分支判断的地方都埋了日志(用的是 klog 这套日志库),级别调高之后几乎能把内部执行路径实时打印出来,你一边看源码一边看实时日志,能把”代码里这一行”和”实际运行时真的走没走到这里”对应起来,这种”读代码+看真实运行日志”的组合,比单纯静态读代码理解速度快很多,而且你已经很熟悉怎么改 kind 集群的组件配置了,操作上完全没有障碍。
最后如果你想理解某个设计”为什么这么做”而不只是”具体怎么做的”,可以去看 KEP(Kubernetes Enhancement Proposal,仓库是 kubernetes/enhancements),这里面存的是每个重大特性从提案、讨论到最终落地的完整设计文档,能看到当初权衡了哪些方案、放弃了哪些方案,这部分背景信息是纯读代码永远补不出来的,配合着看能省下很多”这里为什么要这么设计”的困惑。
总结一下路径:sample-controller 建立骨架心智模型 → client-go 的 Informer/workqueue 看清底层机制 → 挑一个你已经理解概念的具体组件(推荐 kube-scheduler)对照源码验证 → 用 kind 集群 + 调高日志级别做实时印证 → 遇到”为什么这么设计”就去翻 KEP。不要试图通读,Kubernetes 源码量太大,带着具体问题去挖一条线,比系统性通读现实得多,而且效果也更好。
Sources:
