说实话,我见过太多人 Kubernetes 的 YAML 写得比谁都溜,各种 Workload、Ingress、ConfigMap 一套一套的,但一遇到集群 NotReady、Pod 一直 Pending、或者 apiserver 突然抽风,就彻底抓瞎,只能靠重启节点赌运气。原因基本都出在同一个地方:架构层面的运行机制没吃透,只知道"怎么用",不知道"为什么"。
这次我想认真啃一遍 Kubernetes 源码,于是把架构和核心组件的梳理当作整个系列的第一块地基。我始终觉得,源码这东西,如果对整个系统的运行骨架没有清晰的认知,读起来就是走马观花,看到哪算哪,根本串不起来。反过来,只要架构模型在脑子里立住了,再去看源码,你会发现每个组件、每个线程、每个接口都不是凭空设计的,全都能对上号。
这篇文章,我会从一个一线运维工程师的视角,把 Kubernetes 的架构模型、控制面和数据面各组件的职责、组件之间的通信机制,以及最常见的故障排查思路,用大白话讲透。同时会穿插一些源码目录结构和设计意图的解读,给后面真正啃源码铺一条路。适合刚接触 Kubernetes 的运维和开发,也适合那些已经用了一段时间、但总觉得哪里隔着一层纱的同行。
1. Kubernetes整体架构:先建立"控制面+数据面"的全局认知
1.1 为什么Kubernetes要设计成"脑子"和"手脚"分离
Kubernetes 整个系统的设计,说白了就是把"做决策"和"干实事"这两件事彻底拆开。你可以把控制面想象成公司的管理层:负责接单、排期、下达指令、监督执行,但管理层的人自己不下车间干活。而工作节点就是一线车间,里边的 kubelet、kube-proxy 这些组件是真正的执行者,负责把管理层下发的指令落实成一个个运行中的容器。
这种"脑子"和"手脚"分离的设计,最大的好处就是任何一个节点的故障都不会影响整个集群的决策能力。你想想,如果控制逻辑分散在每个节点上,节点一挂,它身上那些应用的调度决策、状态管理就全乱了。而集中式控制面里,apiserver、scheduler、controller-manager 都是可以多副本部署的无状态服务,单个节点炸了,其他副本马上顶上,集群的核心决策能力完全不受影响。
另一个关键设计是声明式 API。用户不直接告诉 Kubernetes"你要在这台机器上运行这个容器",而是提交一份期望状态:"我要 3 个副本,镜像版本是 v1.2.3,端口 8080"。然后控制面的各个控制器会不断对比"期望状态"和"当前状态",一旦发现偏差,就通过一系列操作把现实往期望状态收敛。这套"声明式 + 控制器循环"的模型是理解 Kubernetes 一切行为的钥匙,后面讲 controller-manager 时我会再展开。
1.2 控制面和数据面到底包含哪些组件
标准 Kubernetes 集群分两部分,各自的组件如下:
| 部分 | 组件 | 一句话职责 |
|---|---|---|
| 控制面 | kube-apiserver | 集群唯一 API 入口,所有组件和用户的请求都走这里 |
| 控制面 | etcd | 集群所有状态的存储后端,唯一的数据源 |
| 控制面 | kube-controller-manager | 运行各种控制器,负责把状态收敛到期望值 |
| 控制面 | kube-scheduler | 为新创建的 Pod 选择合适的节点 |
| 数据面 | kubelet | 每个节点上的"Pod 管家",负责本节点容器的全生命周期 |
| 数据面 | kube-proxy | 实现 Service 的访问规则,主要管网络转发 |
| 数据面 | 容器运行时 | 真正创建和运行容器,常见的有 containerd、CRI-O |
注意,控制面和数据面并不是物理上严格隔开的。普通集群里,控制面组件可以跑在专门的 master 节点上,但在轻量级的部署里,控制面和数据面组件也可能混跑在同一批机器上。只是架构逻辑上,它们的职责是严格区分的。
1.3 组件之间怎么通信:一切都要过apiserver
这是很多初学者特别容易懵的地方。Kubernetes 组件之间几乎所有通信都要经过 kube-apiserver,而不是两个组件直接点对点连接。scheduler 不会直接告诉 kubelet"我帮你选好节点了",而是把调度结果写入 apiserver 的 etcd;kubelet 也不是 scheduler 打电话通知的,而是通过监听 apiserver 的 watch 接口,发现自己负责的节点上新增了一个 Pod,才开始动手创建。
这样设计的优点非常明显:所有组件都不需要知道彼此的存在,只要和 apiserver 打交道就行。你可以把 apiserver 想象成一个消息中心,大家都在这里"贴公告"和"看公告",谁都不需要私下拉群。这大大降低了组件之间的耦合度,也方便扩展——你想加一个新控制器,只要按规则跟 apiserver 通信就行,完全不用改其他组件的代码。
组件的端口和通信方向也值得记住,排查网络问题时经常要看:
| 组件 | 端口 | 协议 | 谁连谁 |
|---|---|---|---|
| kube-apiserver | 6443 | HTTPS | 所有客户端、kubectl、各组件 |
| etcd | 2379 | HTTPS/gRPC | kube-apiserver 作为唯一客户端 |
| kube-scheduler | 10259 | HTTPS | 健康检查 |
| kube-controller-manager | 10257 | HTTPS | 健康检查 |
| kubelet | 10250 | HTTPS | apiserver 主动连它,用于 exec/logs |
| kube-proxy | 10249 | HTTP | 自身 metrics |
我第一次搭集群的时候就在这儿踩过坑:ControlPlane 节点的安全组忘了放行 kubelet 的 10250 端口,结果集群怎么都起不来,kubectl exec 也一直报 connection refused。后来才反应过来,apiserver 处理 exec/logs 这类子资源请求时,是会主动去连目标节点上 kubelet 的,所以这个端口必须对控制面开放。
2. 控制面核心组件:apiserver、etcd、controller-manager、scheduler逐一拆解
2.1 kube-apiserver:集群的中枢神经,但业务逻辑最少
如果你去翻 kube-apiserver 的源码,会发现一个很反直觉的现象:这么重要的一个组件,反而几乎不包含业务逻辑。它做的事情非常纯粹:对外提供 RESTful API,处理认证、授权、准入控制,把对象状态读写到 etcd,同时向所有感兴趣的客户端广播变更事件。
源码入口在cmd/kube-apiserver,真正的核心在staging/src/k8s.io/apiserver。注意这个路径是放在 staging 目录下的,意味着它是一套可以独立复用的通用 API 服务器框架,Kubernetes 自己的各种资源类型只是基于这套框架的"插件"。理解了这一点,你再看后面那些自定义 API 扩展、聚合 API Server 之类的概念,就会觉得非常顺理成章。
apiserver 的高可用部署有一个关键点:它本身是无状态的,状态全在 etcd 里。所以你可以启动多个 apiserver 副本,前面挂个负载均衡器就行。不过这里有个小坑——如果多个 apiserver 都开启了写缓存之类的特性,需要确保它们共享同一个 etcd,否则会出现数据不一致。我见过有人图省事,给每个 apiserver 配了单独的 etcd,结果整个集群状态全乱套了。
2.2 etcd:整个集群的"档案室",也是唯一的可信数据源
etcd 是一个分布式键值存储,用 Raft 共识算法保证多副本数据一致。在 Kubernetes 里,etcd 存了所有的集群状态:Pod、Service、ConfigMap、Secret、Deployment,以及它们的期望状态和当前状态,全在这里。
这里有一个比较容易混淆的点:etcd 里存的是"期望状态 + 当前状态",但控制器的工作是让两者趋于一致。比如 Deployment 期望 3 个副本,当前只有 2 个,这本"账"就记在 etcd 里。ReplicaSet 控制器通过 watch 发现这个差异,然后创建第 3 个 Pod。整个系统就像一台永不停歇的"状态修正机"。
从源码角度,kube-apiserver 访问 etcd 走的是staging/src/k8s.io/apiserver/pkg/storage/etcd3这个包,API 版本使用 v3。如果你看到集群里还残留着 v2 的数据目录,那多半是从老版本升级过来的。v2 性能差、watch 机制也弱,早就被移除了。
对运维来说,etcd 最需要关注的是磁盘 IO 性能和网络延迟,因为 Raft 要求每次写操作都要多数节点确认,IO 一慢,整个集群的写入都跟着卡。我有个生产集群曾经因为共享存储的 IO 抖动,etcd leader 频繁切换,apiserver 各种超时,最后排查发现是宿主机上的其他虚拟机在跑批量任务把磁盘带宽打满了。所以如果条件允许,etcd 最好单独一台机器或者至少用独立的 SSD。
2.3 kube-controller-manager:一堆控制器的集合体,核心循环逻辑
controller-manager 不是一个控制器,而是一堆控制器的集合。打开源码cmd/kube-controller-manager里的 app 包,你能看到一长串控制器列表:DeploymentController、ReplicaSetController、NodeLifecycleController、EndpointController、ServiceAccountController、NamespaceController……每个控制器负责一种资源的状态收敛。
控制器的核心模式是控制回路,代码上最核心的函数就是processNextWorkItem,这个死循环的逻辑大概是:
反复执行以下步骤: 1. 从队列里取出一个待处理对象 2. 比较对象的期望状态和当前状态 3. 如果一致,跳过;如果不一致,执行操作让现实往期望靠拢 4. 无论成功失败,把结果记录到事件里这个"队列"从哪来?答案是Informer 机制。每个控制器都会通过 apiserver 的 watch 接口订阅自己关心的资源变化,本地维护一份缓存(indexer),并把变化放进工作队列。所以控制器的本地缓存是实时同步的,处理时根本不用频繁回源查询 apiserver,大大减轻了 apiserver 的压力。
踩坑提醒:controller-manager 多副本部署时,同一时刻只有一个实例在工作,其他实例处于 standby 状态,选举机制用的是 etcd 里的 lease。你可能会在日志里看到"attempting to acquire leader lease"之类的信息,这是正常的,不要慌。
2.4 kube-scheduler:为 Pod 找家,讲究的是"过滤"和"打分"
scheduler 的职责一句话就能说完:给还没分配节点的 Pod 选一个最合适的节点。但怎么选,里面花活很多。
调度过程分两步:过滤和打分。过滤阶段(Predicates)把不满足硬性条件的节点剔除掉,比如资源不够、端口冲突、节点有污点且 Pod 不容忍。打分阶段(Priorities)对剩余节点打分,比如 CPU/内存余量大的节点分数高、Pod 分散在不同节点上的分数高,最后选分数最高的那个。
源码入口在pkg/scheduler,核心接口是Schedule,里面调用了RunFilterPlugins和RunScorePlugins。新版还用到了"调度框架"(Scheduling Framework),允许通过插件扩展调度逻辑。如果你想实现自定义调度策略,不需要改 scheduler 的代码,写个插件注册进去就行。
调度完成后,scheduler 并不会自己去创建 Pod,而是通过 apiserver 写入一个Binding对象。这个细节非常关键:scheduler 只是"建议",最终执行权在 kubelet 手上。如果 kubelet 因为某些原因(比如镜像拉取失败)没能成功创建容器,集群不会自动把 Pod 重新调度到别的节点,因为 Pod 的 nodeName 一旦设置,除非有人手动删掉重建,否则它就绑死在那台机器上了。
3. 工作节点核心组件:kubelet、kube-proxy、容器运行时到底在做什么
3.1 kubelet:节点上的"全能管家",也是唯一代表 Kubernetes 和容器运行时对话的组件
kubelet 是数据面最重要的组件,没有之一。它跑在每一个工作节点上,职责包括:
- 监听 apiserver 的 watch 接口,发现自己节点上应该运行哪些 Pod
- 创建、启动、停止容器,执行容器的生命周期钩子
- 定期上报节点的资源使用情况和状态(心跳)
- 执行 liveness 和 readiness 探针
- 管理挂载卷、网络命名空间
源码入口在cmd/kubelet,核心循环是syncLoop。这个名字起得非常直白——同步循环。它不断从各种来源(apiserver、本地静态 Pod 目录、HTTP 请求)接收 Pod 配置的变更,然后调用后面的容器运行时把实际状态调整到期望状态。
这里有一个非常重要的设计:kubelet 自己并不直接操作 Docker 或者 containerd 的接口,而是通过CRI(Container Runtime Interface)这个抽象层。"我的宿主要换容器运行时了,kubelet 的代码要改吗?"不用,只要新的运行时实现 CRI 接口就行。这个思路跟操作系统的驱动设计如出一辙,你可以把 CRI 理解成"容器运行时领域的 USB 接口"。
3.2 kube-proxy:Service 网络的隐形功臣,三种模式怎么选
kube-proxy 解决的核心问题很简单:让用户可以通过一个稳定的虚拟 IP(ClusterIP)访问一组动态变化的 Pod。Pod 的 IP 会随着重建而变化,但 Service 的 ClusterIP 是不变的。kube-proxy 做的就是维护 ClusterIP 到后端 Pod IP 的转发规则。
实现方式有三种:userspace(老古董,基本淘汰了)、iptables(最常见)、IPVS(大规模集群推荐)。iptables 模式好理解但有个问题:规则多了以后,线性遍历的性能会下降,而且更新规则时是整体刷新。IPVS 模式使用哈希表,性能好、支持更丰富的负载均衡算法,所以生产环境 100 个 Pod 以上的 Service 建议用 IPVS。
看源码的时候有个印象深刻的点:kube-proxy 本身并不负责转发数据包,它只负责"写规则",真正的数据转发是内核里的 iptables/ipvs 在干活。所以 kube-proxy 挂了,已有规则通常还在,新 Service 可能没法访问,但老的还能撑一段时间。这也是为什么排障时要先看内核规则,而不是一上来就重启 kube-proxy。
3.3 kubelet如何调用containerd:从原理到实体调用链路
这个是很多人的知识盲区,也是看源码时最应该弄清的一条链路。我在查 Kubernetes 和 containerd 交互的时候,花了很长时间才把整条调用链理顺,这里给你完整讲一遍。
kubelet 内部有一个模块叫kubeGenericRuntimeManager,它是 CRI 的实现方,把所有和运行时相关的操作封装成RuntimeService和ImageService两类接口。它并不直接创建容器,而是通过 gRPC 客户端向容器运行时发请求。
默认情况下,kubelet 通过本地 Unix Socket 和 containerd 通信,路径是:
/run/containerd/containerd.sock调用链大致是这样的:
kubelet 的 syncLoop 发现需要创建一个新 Pod -> kubeGenericRuntimeManager.SyncPod() -> 调用 remoteRuntimeService.RunPodSandbox() -> 这是 gRPC 请求,通过 Unix Socket 发给 containerd -> containerd 的 CRI 插件(cri plugin)收到请求 -> 创建沙箱(sandbox),也就是 pause 容器 -> 再调用 RunContainer 创建真正的业务容器 -> 底层通过 containerd 的 task 管理接口,最终由 runc 执行容器进程注意这里面每个环节的关键点:
- kubelet 用
--container-runtime-endpoint指定 socket,默认就是 containerd 的地址 - containerd 里跑了一个 CRI plugin,负责把 CRI 请求翻译成 containerd 自己的 API 调用
- runc 是 OCI 规范的实现者,负责真正干活的:用 Linux 内核的 namespace、cgroup、chroot 等机制启动进程
所以你可以理解成:kubelet 是项目经理,CRI 是翻译官,containerd 是车间主管,runc 是干活的工人。整个链路设计得很清晰,每一层都只管自己的事。
我在现场排查的时候,经常用下面这几个命令看容器运行时的状态:
# 更推荐用 crictl,它是 CRI 兼容的命令行工具 crictl ps -a # 查看所有容器,包括已退出的 crictl inspect <container-id> # 查看容器详细信息 crictl logs <container-id> # 直接拉容器日志,比 kubectl logs 更底层 ctr -n k8s.io containers list # 用 containerd 原生工具查看 k8s 命名空间下的容器这里有个坑:用crictl之前,确保--runtime-endpoint和 kubelet 指向同一个 socket,不然你会看到"connect: no such file or directory",以为 containerd 挂了,其实只是工具没找到 socket。
3.4 CNI和CSI:网络和存储的"插槽"
除了 CRI,kubelet 还通过两个类似的接口管理网络和存储:
- CNI(Container Network Interface):解决 Pod 网络 IP 分配、网络打通的问题,常见实现有 Calico、Cilium、Flannel
- CSI(Container Storage Interface):解决持久化存储的问题,常见实现有 Ceph CSI、云厂商的云盘 CSI
kubelet 在创建 Pod 的时候,会先通过 CNI 插件给 Pod 的网络命名空间创建虚拟网卡、分配 IP;业务容器启动后再把存储卷挂载进去。所以你在排障时看到ContainerCreating卡住时,八成是 CNI 出了问题或者存储挂载不了,这两个方向比 kubelet 本身的问题优先级更高。
4. 一次Pod创建全流程:从kubectl到containerd的完整链路
4.1 从提交YAML到etcd落盘
要真正理解架构,光看组件还不够,把一条完整的请求链路走一遍是最好的方式。我们就拿一个最简单的场景举例:kubectl apply -f deployment.yaml,从提交到跑起来,中间到底经过哪些环节。
第一步,kubectl 把 Deployment 的 YAML 解析成 API 对象,然后通过 HTTPS 发给 kube-apiserver 的/apis/apps/v1/deployments接口。注意这里每次请求都要经过 TLS 认证,而且 kubectl 会读取~/.kube/config里的 kubeconfig 配置来确认 apiserver 地址和证书。
apiserver 收到请求后,会依次经过:
- 认证:确认你是谁。常见有客户端证书、Bearer Token、OIDC 等方式
- 授权:确认你有没有权限做这个操作。RBAC 里的 Role、RoleBinding 就是管这个的
- 准入控制:一堆插件按顺序执行,比如验证资源配额、注入默认值等
通过了这三关,apiserver 才会把对象序列化,写入 etcd,然后向客户端返回"创建成功"。
4.2 控制器的连锁反应与调度器的介入
Deployment 对象在 etcd 落盘后,真正的连锁反应才开始。
DeploymentController(运行在 kube-controller-manager 里)通过 watch 接口收到了新 Deployment 的事件,发现它的副本数期望是 3,但当前对应的 ReplicaSet 还不存在。于是 DeploymentController 创建了一个 ReplicaSet 对象。
ReplicaSetController 又通过 watch 收到了新 ReplicaSet,发现期望 3 个副本但当前 Pod 数为 0,于是创建了 3 个 Pod 对象。这些 Pod 此时的nodeName字段是空的,它们处于 Pending 状态。
接下来轮到 scheduler。scheduler 通过 watch 发现了这些没有 nodeName 的 Pod,进入调度流程:先过滤,剔除资源不够的节点;再打分,选出最合适的一个;最后通过 apiserver 写入一个 Binding 对象,指定这个 Pod 应该跑在哪个节点上。写入之后,apiserver 会更新 Pod 的nodeName字段,并再次向所有 watch 的客户端广播变更。
4.3 kubelet真正的执行与容器运行时的协作
目标节点上的 kubelet 一直通过 watch 接口盯着 apiserver,当它发现某个 Pod 的nodeName是自己时,就正式开始执行。
kubelet 会先做一些前置检查,比如拉取 Pod 需要的镜像、准备数据卷。然后它通过 CRI 调用 containerd,执行我在 3.3 节讲的那条链路:
- 先创建 Pod 的沙箱(sandbox),也就是 pause 容器。pause 容器的唯一职责是"占住"这个 Pod 的 network namespace、PID namespace、IPC namespace 等,让同一 Pod 的其他容器共享这些命名空间
- 沙箱建好后,再调用 CNI 插件,给沙箱的网络命名空间分配 IP 地址。这个 IP 就是 Pod 的 IP,所有容器共享
- 拿着沙箱的 ID,再逐个创建业务容器,把它们加入到同一个命名空间中
- 容器启动后,kubelet 开始执行 readinessProbe 探针,检查服务是否真正就绪。就绪后,Pod 的 IP 会通过 EndpointSlice 对象注册到 Service 上,kube-proxy 再把这些信息转换成 iptables/ipvs 规则
到这里,一个 Deployment 才算真正跑起来。
4.4 流量访问怎么走:从Service到Pod的完整路径
用户访问一个 Service 的流程也值得走一遍。假设你访问的是 ClusterIP,请求会先被节点上的 iptables 规则拦截,规则会做 DNAT 把目的地址从 ClusterIP 改成某个 Pod IP,然后再送到对应 Pod 的容器里。
kube-proxy 实时 watch Service 和 EndpointSlice 的变化,一有变动就更新节点上的转发规则。所以当你 kill 掉一个 Pod,新的 Pod 起来后,大概率在几秒内就能自动纳入 Service 的负载均衡池。这也是为什么你在排查故障时,kubectl get endpoints的输出能直接反映后端的接入情况。
5. 架构层面的故障排查:从Pod到集群的10个典型问题
5.1 排查三板斧:事件、日志、状态
搞懂了架构,故障排查就变得有章可循了。我的经验是永远按"从外到内、从高到低"的顺序排查:先看集群事件,再看组件日志,最后才深入到容器内部。
第一板斧,查事件和整体状态:
kubectl get events --sort-by=.lastTimestamp kubectl describe pod <pod-name> kubectl get nodes -o wide kubectl get cs # 查看控制面组件健康状态,新版里可能不推荐了第二板斧,看控制面和节点日志:
# 控制面组件如果是 static pod 或者 systemd 服务的,直接看日志 journalctl -u kubelet -f tail -f /var/log/containers/kube-apiserver-*.log第三板斧,深入到容器运行时内部确认:
crictl ps -a crictl inspect <container-id> crictl logs <container-id>这套方法基本能帮你定位 80% 的问题。剩下的,就是靠架构知识去推断了。
5.2 常见问题速查表和生产踩坑经验
下面这张表是我这些年遇到的高频问题整理出来的,每个都对应了架构里的某一环:
| 症状 | 大概率原因 | 排查命令或手段 |
|---|---|---|
| Pod 一直 Pending | 调度器没选中节点,资源不足或节点有污点 | kubectl describe pod看 Events |
| Pod 一直 ContainerCreating | 镜像拉取慢、CNI 网络异常、存储卷挂载失败 | crictl ps -a看容器状态 |
| CrashLoopBackOff | 启动命令错误、依赖缺失、探针配置太激进 | crictl logs看真实日志 |
| 节点 NotReady | kubelet 心跳断了,或 Kubelet 卡死/磁盘满 | journalctl -u kubelet -f |
| 访问 Service 超时 | Endpoint 没注册、kube-proxy 规则没生成 | kubectl get endpoints+iptables -L -t nat |
| apiserver 变慢 | etcd 慢、大量 watch 客户端重连、大对象频繁更新 | 看 etcd 监控指标 |
| 证书过期 | 某组件证书有效期到了 | 检查各组件 TLS 证书过期时间 |
| Pod 删不掉 | 最终器没退出、有 finalizer 卡住、kubelet 失联 | 看 Pod 的 finalizers 字段 |
| 集群 DNS 解析失败 | coreDNS 副本挂了,或节点 DNS 配置被改 | kubectl -n kube-system get pods |
| 容器一直重启但报成功 | liveness 探针误伤,或容器进程 fork 了子进程 | 调大 initialDelaySeconds 观察 |
我在生产环境踩过最深的坑有两个。第一个是节点 NotReady,一开始以为是 kubelet 挂了,但 ssh 上去发现 kubelet 进程活着,状态也正常。最后查了半天,发现是磁盘满了。kubelet 在磁盘压力超过阈值时会上报 DiskPressure,节点被标记为 NotReady,但它不会像常见的"服务挂了"那样有明显的进程错误。所以排查节点问题,df -h永远是第一步。
第二个是Pod 一直 ContainerCreating,describe 出来下面是 SandBox 创建失败。我一开始以为是容器运行时的问题,结果发现是 Calico 的 BGP 会话断了,Pod 网络不通,CNI 插件分配 IP 的时候卡住。所以看到 ContainerCreating 时,第一反应应该是网络或存储,而不是急着重启 containerd。
5.3 定位问题的延伸思路:从现象反推组件链路
当故障比较复杂、三板斧不够用时,你可以用"链路反推法"来定位。比如一个 Deployment 迟迟不创建 Pod,你就顺着链路走:
- 看 Deployment 状态:
kubectl describe deployment,是否有 ReplicaSet 被创建 - 看 ReplicaSet 状态:
kubectl get rs - 看 Pod 状态:
kubectl get pods -l app=xxx - 看 Pod 事件里的 WARNING 消息
每一步的产物都对应架构里的一个组件:Deployment 归 DeploymentController 管,ReplicaSet 归 ReplicaSetController 管,Pod 调度归 scheduler 管,Pod 创建归 kubelet 和 CRI 管。哪一步断了,你就知道问题出在哪个环节。这个方法比瞎猜高效一万倍,也是"架构思维"在生产里最大的价值。
6. 源码阅读路径建议和我的几点体会
最后按惯例分享点个人经验。如果你打算深入阅读 Kubernetes 源码,我建议按这个顺序来:
- 先读
cmd/kube-apiserver的启动入口,搞清楚一个 API 服务器是怎么一步步起来的,特别是NewServerRunOptions和CreateServerChain这两个函数 - 然后读
cmd/kube-controller-manager里的 app 包,看你日常工作里最常用的 DeploymentController 是怎么实现"期望状态-当前状态"循环的 - 再读
staging/src/k8s.io/client-go/tools/cache里的 Informer 机制,这是所有控制器的基石,理解了 informer,你就理解了 Kubernetes 的"事件驱动"核心 - 接着读
pkg/kubelet里的kubelet.go,顺着syncLoop往下走,看 kubelet 怎么把一个 Pod 配置变成容器运行时的一套调用
我在实际读源码过程中的体会是:架构理解到位了,源码就是注释;架构理解不到位,源码就是天书。你不要试图一上来就通读所有代码,这不现实,也没必要。挑一条你最熟悉的请求链路——比如 Deployment 的创建过程——从 kubectl 一路跟踪到 runc,把这一条线吃透,你对整个系统的理解就会产生质变。
还有个小技巧:读源码时开两个窗口,一个看代码,一个开kubectl get --watch实时观察资源变化。一边看代码,一边看实际集群里的对象状态变化,代码里的每个逻辑分支在现实里都能对上号,那种"哦,原来这里就是这个意思"的瞬间,是读源码最爽的时刻。
这篇就当是系列的起手式,后面我会继续拆解 informer 机制、控制器模型、调度器的插件框架这些更深入的主题。有想让我优先写哪块的,可以在评论区聊聊,我按频次安排。