ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kubernetes源码怎么读?从模块架构到运行机制的实战路线

Kubernetes源码怎么读?从模块架构到运行机制的实战路线 聊 KubernetesK8s源码之前我先泼一盆冷水直接打开 GitHub 仓库从头啃大概率一周之内就会劝退。K8s 的源码仓库非常庞大核心模块包括 API Server、Controller Manager、Scheduler、kubelet、kube-proxy再加上各类接口抽象与插件体系代码总量早就超过百万行。它不像普通开源项目那样只有一个程序入口而是一套分布式系统在协作运行。如果没先建立模块架构和运行机制的主线很容易一头扎进某个细节里出不来。这篇文章是我自己从源码小白一路摸到能定位问题、能改核心逻辑的历程总结重点讲怎么循序渐进地读 K8s 源码、从哪些入口看、按什么顺序看帮新手少踩几个坑。1. 先搞清楚K8s 源码为什么那么难啃1.1 它不是一个程序而是一套分布式系统很多人第一次打开 Kubernetes 仓库时会下意识找 main 函数找到一个cmd/kube-apiserver/main.go就从头开始读。读了几百行还在解析 flag、初始化日志、装配配置好不容易看到 Run 函数又立刻被里面一大串NewXxx的依赖注入搞晕。这种挫败感不是你的问题而是 K8s 本身的设计使然。它并不是一个单体程序而是多个独立的二进制协作kube-apiserver控制面的入口所有请求和数据都从这里过。kube-controller-manager控制面的大脑跑着一堆 controller。kube-scheduler负责给 Pod 找节点。kubelet每个节点上的管家真正把容器拉起来。kube-proxy负责 Service 的访问规则。仓库根目录下cmd/放着这些二进制的入口pkg/放着核心实现staging/放着对外部暴露的库。也就是说你要理解的不只是某一段代码而是这几条消息怎么在这个分布式系统里流转。如果只盯着一个函数看丢掉了协作维度自然读不出东西。1.2 新手抓不住主线的三个典型症状我在社区里看到不少人和当年的我一样卡在同一个地方。典型症状有这么几种症状实际表现本质原因从头啃入口从 main 开始逐行读到 Run 就被初始化逻辑淹没把源码当成单进程程序来读不理解分布式协作被接口套娃带跑看到NewInformerFactory就追进去追完忘了自己原本想干什么缺少“带问题找答案”的意识遇到新概念就被带偏随机挑模块想读 Controller Manager却从一堆 controller 列表随机挑一个没先建立整体架构图缺乏主线引导这三个问题背后的本质是还没有建立“顶层设计图”就急着看底层实现。代码本身并不算难难的是你不知道它在整条链路里扮演什么角色。1.3 正确的学习顺序先建路线图再动手读代码我自己折了不少跟头之后总结出一个比较稳妥的顺序先概念、再链路、后源码、最后扩展。第一步不看源码先把官方文档里 Concepts 部分过一遍。搞清楚 Deployment、ReplicaSet、Service、Pod 这些资源对象之间的关系以及“期望状态、当前状态、调谐”这几个基本概念。第二步跑一个本地集群通过一次kubectl apply观察事件流转。哪里卡住了就去查对应组件的日志在真实世界里感受一下模块协作。第三步选一个小而完整的 controller 源码精读例如 ReplicaSet controller把调谐循环读透。第四步再按需扩展到 Scheduler、kubelet、API Server 等大模块。循序渐进不是谦词而是这类大型项目唯一现实的读法。上来就想全模块通读结果往往是什么都读不深。2. 模块架构拆解K8s 这台机器都有哪些部件2.1 控制面大脑与决策层控制面是一套系统的“管理层”负责接收请求、做决策、维持全局状态。把控制面比作一家公司的话各模块的职责非常清楚API Server前台接待唯一入口。所有kubectl apply、外部 API 调用、各组件之间的通信都要经过它。它负责认证、鉴权、准入校验最后把数据写入 etcd并向所有监听者推送事件。Controller Manager中层经理里面跑着几十个 controller什么 ReplicaSet、Deployment、Service、Namespace、Endpoint 都有对应的 controller。它们通过 informer 监听资源变化然后不断让当前状态向期望状态靠拢。SchedulerHR 兼工位分配只关注 Pod 该落在哪个节点。它 watch 那些还没绑定节点的 Pod经过过滤和打分选出一个最合适的节点。etcd档案室严格来说不是 K8s 自身的组件但它是整个控制面的数据底座所有资源对象都存在这里。API Server 是唯一直接读写 etcd 的模块这样设计是为了保证一致性和安全边界。控制面组件核心职责关键特性kube-apiserver所有请求入口、统一数据读写认证、鉴权、准入、watch 推送kube-controller-manager维护期望状态多 controller 共存于一个进程kube-scheduler为 Pod 分配节点预选 优选 扩展点etcd持久化存储分布式强一致键值数据库控制面这些组件可以部署成多副本它们干的是“决策”的活不直接碰容器。2.2 数据面执行与落地层控制面做出决策之后真正动手干活的是数据面。如果把 K8s 比作一个装修公司控制面是设计师和项目经理数据面就是工地上的施工队。kubelet车间主任每个节点上都有一个负责管理本节点的 Pod 生命周期。它 watch 到本节点上新增了某个 Pod就要确保容器真正跑起来过程中还要维护网络、存储等附属资源。kube-proxy门卫负责实现 Service 的访问规则把发往 Service VIP 的流量转发到对应的后端 Pod常见实现是 iptables 或 ipvs。容器运行时通过 CRI 接口对接常见的是 containerd真正完成拉镜像、创建沙箱、启动容器等操作。网络插件通过 CNI 接口实现 Pod 网络比如 Calico、Flannel。存储插件通过 CSI 接口实现持久化存储比如各种云盘驱动。数据面插件的存在正是 K8s 生态能那么繁荣的关键。它不关心你底层用什么容器运行时、什么网络方案只要实现了对应接口就能被纳入体系。2.3 三个核心设计思想声明式、调谐循环、接口抽象光知道模块有哪些还不够你还要理解 K8s 为什么这么设计。读源码的时候你会反复遇到下面三个思想。声明式用户描述“我要什么”而不是“你怎么做”。比如 YAML 里写replicas: 3表达的是“我要三个副本”而不是“帮我创建一个副本、再创建第二个、再创建第三个”。这相当于你给空调设定 26 度而不是每隔十分钟喊一次“上调一度”。调谐循环控制器的核心逻辑就是不断执行“观察当前状态、对比期望状态、消除差异”的操作。理想状态下它就是个无限循环每次循环做很小的修正。这个模式在源码里的体现就是 controller 里面那个“取 key、做 sync”的循环。接口抽象CRI、CNI、CSI 这些接口本质是把具体实现挡在外面。API Server 不关心你网络怎么打通kubelet 也不关心你用哪种运行时。它定义一个契约让不同厂商各自实现谁都可以插进来。这些思想读明白了再去看源码你会发现自己能猜到一个 controller 下一步要干什么。看不懂设计思想的人看代码只是看“每一步做了什么”看懂设计思想的人看代码是在验证“它是不是按这套逻辑做的”。3. 运行机制核心从一次 Pod 创建看系统协作3.1 第一棒API Server 与 etcd所有请求的总入口假设你执行了kubectl apply -f pod.yaml一个 Pod 的生命周期就这样开始了。首先kubectl 会把 YAML 解析成 Pod 对象然后通过 REST 请求把对象提交给 API Server。API Server 在这个入口上要做的事非常多先做身份认证再看你有没有权限操作 Pod接着跑一组准入控制器Admission Controller还会做 schema 校验看字段是否合法。这些检查都通过之后它才把 Pod 对象序列化写入 etcd。这一步是关键从此刻起这个 Pod 的“期望状态”就持久化存在了。API Server 随后会通过自身的 watch 机制向所有监听这个资源类型的客户端推送一条 “Pod Added” 事件。很多新手不理解为什么所有组件都要绕道 API Server 而不直接读写 etcd。原因有两个一是安全与授权不能让人人都摸到底层数据二是一致性所有组件看到的都是同一份数据流。etcd 只服务 API Server 一个人其他人只能通过 API Server 的接口和事件来感知变化。3.2 第二棒Scheduler 如何决定 Pod 落在哪个节点Pod 对象已经被 etcd 持久化了但此刻它还没有nodeName字段也就是还没分配节点。Scheduler 会通过 informer 监听这类“待调度 Pod”。接下来是调度核心流程。第一步是预选Filter也就是过滤掉不满足条件的节点资源够不够、端口有没有冲突、nodeSelector 是否匹配、是否有污点等。第二步是优选Score对剩下的节点打分看资源余量、镜像是否已在本地、pod 亲和反亲和等选得分最高的节点。Scheduler 做完了决定做的事情其实很简单通过 API Server 把spec.nodeName写回 Pod 对象。注意关键点它只负责“登记”并不负责真的去节点上启动容器。容器启动是 kubelet 的事调度器写完就完事了。P.S. 我记得第一次看调度代码时总觉得它应该直接发指令给节点结果发现它只是改一个字段。这种“通过 API Server 改状态来间接驱动别人干活”的模式是理解 K8s 运行机制的钥匙。3.3 第三棒kubelet 与 CRI/CNI/CSI 协同落地kubelet watch 到某个 Pod 被绑定到本节点之后就轮到它上场了。它会进入 pod worker 线程最终调用syncPod这个核心入口按顺序处理一堆事如果 Pod 正在删除走终止流程清理容器和资源。确保 Pod 的沙箱sandbox存在这一步通过 CRI 调用容器运行时完成。创建或更新业务容器同样走 CRI。配置 Pod 网络通过 CNI 插件完成。挂载存储卷通过 volume manager 和 CSI 插件完成。这个过程就像车间主任指挥几个工种CRI 负责造容器、CNI 负责拉网线、CSI 负责搬硬盘kubelet 是那个盯进度、协调顺序的人。有同学会问kubelet 怎么知道网络挂载有没有做好答案是它也会 watch 状态、查询状态确保上一步完成再进入下一步。源码里大量这种“查询当前状态 - 决定下一步动作”的逻辑就是调谐思想在单模块里的体现。3.4 隐藏主线List-Watch 与 informer 机制如果你想读懂 K8s 全部模块的通信List-Watch 机制是绕不开的主线。所有组件都不主动轮询 API Server而是先做一次全量 LIST然后保持一条长连接接收增量变化事件WATCH。围绕这条主线源码里有一套非常经典的封装Reflector、DeltaFIFO、Indexer、WorkQueue。Reflector底层干活的负责 LIST 和 WATCH把收到的对象放进 DeltaFIFO。DeltaFIFO一个中间队列存放变更事件保证事件的顺序和去重。Indexer本地缓存并且带索引。controller 通过 Lister 读对象不用每次都请求 API Server。WorkQueue控制器真正消费事件的队列可以延迟重试、限速避免一个错误事件反复爆炸。读任何一个 controller 源码时你会反复看到 informer WorkQueue 的组合。理解这条链路比背下某个具体函数更有用。它解释了 K8s 为什么在大量并发场景下还能保持稳定事件先进队列工作线程慢慢消费出错了一重重试不慌不忙。4. 源码阅读实操路线从哪入手、按什么顺序看4.1 热身仓库结构、Go 基础与版本选择读 K8s 源码之前先把 Go 语言基础补一补。重点不是语法而是 goroutine、channel、interface 的用法因为 K8s 内部到处都是并发模型和接口抽象。函数式、反射、泛型这些反而用得不算多不用一开始就啃。仓库结构要认清cmd/各组件入口。pkg/核心实现平时读最多的地方。staging/对外发布的库比如k8s.io/api、k8s.io/client-go、k8s.io/apimachinery都在这里。test/大量集成测试可以当用法文档看。版本选择上建议选一个稳定的发布分支比如 v1.26 或 v1.28。太老的版本可能还在用一些废弃的 API太新的特性实验性较强文档和源码容易对不上。4.2 入门推荐从 API Server 的资源定义切入我的建议是别从 main 函数开始而从“资源对象长什么样”开始。最简单的入口是staging/src/k8s.io/api/core/v1/types.go这里面定义了 Pod、Service、ConfigMap 等核心对象的 Go 结构体。为什么先读这个因为你读懂PodSpec和PodStatus的差异之后后面所有 controller 的调谐逻辑都好理解了。控制器的目标就是“把 PodStatus 尽量改得符合 PodSpec 的期望”。Spec 是目标Status 是现实所有模块都在想办法让两者对齐。读完结构体可以顺手看对应资源的存储层。API Server 里每个资源都有 registry它负责把资源对象序列化写入 etcd。读到这里你就知道一个 API 对象从请求到落盘大体经过哪些环节。4.3 核心捷径Controller Manager 里的调谐循环想深入运行机制最简单的入口是 ReplicaSet controller路径在pkg/controller/replicaset。为什么选它因为它是“最小可独立工作的控制器”逻辑不多不少刚好覆盖核心套路。打开源码你会看到关键链路NewReplicaSetController创建控制器注册事件回调工作线程不断从 WorkQueue 取 keyprocessNextWorkItem取出一个namespace/name然后调用syncHandler也就是真正的同步逻辑最后通过manageReplicas创建或删除 Pod。读这一套流程你就在实战里理解了什么是我前面提的 Reflector、DeltaFIFO、WorkQueue。读透一个 controller 之后你会惊喜地发现Deployment controller、DaemonSet controller、Job controller 都是类似的套路区别只在于“调谐的时候具体比对什么字段、操作什么资源”。读到这里你已经有能力实现一个简单的自定义 controller 了。K8s 官方提供了 sample-controller 示例照着模板把 ReplicaSet 换成自定义 CRD加上自己的业务逻辑就行。这一步做完你对这套框架的掌握就基本落地了。4.4 扩展路线Scheduler 框架与 kubelet 的同步循环Controller Manager 读懂之后可以往两个方向扩展。第一个方向是读 Scheduler。入口在pkg/scheduler核心是pkg/scheduler/framework里定义的一组扩展点接口PreFilter、Filter、Score、PreBind、Bind 等。理解调度器的关键不是去背某个具体算法而是看懂这些扩展点在哪个阶段执行、由哪些插件实现。内置插件在你机器上用的可能是 NodeResourcesFit、NodeAffinity、TaintTolerations 这些。第二个方向是读 kubelet。核心入口是pkg/kubelet/kubelet.go里的syncPod方法。建议先跟一条完整路径从入口到 CRI 调用、再到 Pod 状态更新中间牵涉到的细节先用“TODO”标记不急着展开。Scheduler 和 kubelet 比 controller 复杂很多没必要第一轮就啃完。把主线走通、知道什么功能在哪个目录以后遇到问题能快速定位就已经值回票价。5. 实际操作中容易踩的坑与排查技巧5.1 interface 套娃怎么快速定位具体实现K8s 源码为了扩展性到处是接口。新手最容易卡在“这个接口到底由谁实现”的问题上。我的经验是不要从接口定义往前追而是从调用方回头看。你在代码里看到iface.NewXxx()这时候真正重要的是找到NewXxx返回的具体类型。IDE 里直接使用“Go to Implementation”GoLand 是 CtrlAltBVS Code 也可以它可以帮你跳到当前接口的实现类。如果 IDE 不好使还有一个土办法全局搜索func NewXxx的所有实现从包的命名基本能判断出调用的到底是谁。实在不行记一条规律K8s 源码里接口名和实现的关系一般很近通常实现类叫xxxManager、xxxProvider之流都在同一个包或子包里。5.2 版本与文档对不上如何保持一致性网上大量 Kubernetes 源码分析文章写于多年之前不少代码路径已经变了。你今天照着 v1.28 的源码搜索某篇文章里提到的函数可能根本搜不到。我建议以 go.mod 或k8s.io/kubernetes的版本号为准。你 clone 下来的仓库切到对应 tag比如v1.28.4然后所有在线阅读的文章都当作参考而不是权威。看代码时优先看当前版本里的同名函数或相邻目录不要强迫自己找到一个已经删掉的老函数。另外要注意 k8s.io 系列库的版本与主仓库保持一致。因为staging/里的库会被独立发布版本但代码内容与主仓库同步你直接在主仓库读staging/src/k8s.io/...就行不用单独去 clone 那些独立 repo。5.3 只看代码没有体感用 Kind 集群对照日志验证只看代码容易产生“虚”的感觉因为你不知道这段代码跑起来是什么样。我强烈建议本地起一个 Kind 集群单节点就行资源开销很小。具体做法是本地起好集群后开三个终端第一个跑kubectl get events --watch第二个跑kubectl apply -f xxx.yaml第三个看对应组件的日志。然后一步步观察事件是怎么产生的再去源码里找对应的处理逻辑。比如你看到一条Scheduled事件就可以去 Scheduler 代码里搜事件名称看看是谁发的、在哪个节点发的。我还有一个习惯读完一个 controller 的调谐循环之后故意改坏一份 YAML比如把镜像名写错然后看 kubelet 日志和 Pod 事件。这时候抽象代码和真实行为会迅速对应起来记忆特别深。5.4 一个从源码视角定位 Pod 问题的真实实例再分享一个实际排查场景。之前有同事遇到 Pod 一直 Pending按经验先看kubectl describe pod发现加了污点但没有对应的容忍。这个现象背后对应的源码路径是 Scheduler 的 TaintTolerations 插件。我当时直接把pkg/scheduler/framework/plugins/tainttoleration的 Filter 代码打开对照事件里报的 “0/3 nodes available” 信息很快确认是 TolerationSeconds 配置没生效——YAML 里写错了缩进导致这一项根本没被解析进去。另一个常见问题是 CrashLoopBackOff。很多人只知道去看容器日志但从源码角度看这个状态是 kubelet 里的pkg/kubelet/container重启策略逻辑计算出来的。它根据容器的退出状态、重启策略和退避算法算出下一次应该隔多久重启。明白这点之后你就知道为什么 “Always” 策略下容器即使成功退出也会无限重启也就知道该怎么调整。把“线上现象”映射到“源码路径”是我觉得最有价值的一项能力。它比单纯读完几万行代码更能体现你对系统运行机制的理解。6. 最后分享一点个人体会很多人问我读 K8s 源码有没有什么捷径。我觉得真正的捷径不是某个秘籍而是“先跑一个真集群 选一个控制器 跟踪一次调谐”这三件事的组合。你不需要读完全部代码哪怕只是把 ReplicaSet controller 从事件监听到副本调整这一条链路读透你对整个 Kubernetes 的理解就已经超过绝大多数只写过 YAML 的人。我至今还记得第一次读完 ReplicaSet controller 之后的那种通透感原来 Deployment 自动修复副本、滚动更新这些高级功能底层都是一个个小控制器在循环里“观察差异、消除差异”。理解了这层再去看什么自定义调度器、自定义控制器基本就是套模板的事。如果你也想开始读源码我给一个特别具体的目标第一周不用贪多就做一件事画出一张“从kubectl apply到容器运行”的时序图标清每个组件、每个关键接口、每类事件。这张图画完你已经比 90% 只背命令的人更懂 K8s 了。后面再想深入哪个模块都只是沿着这张图往下钻而已。
返回列表