K8S核心组件详解:kube-apiserver、controller-manager、scheduler与kubelet
很多刚接触 K8S 集群的朋友第一次打开架构图时都会被一堆组件名字绕晕。尤其是“四大组件”这个说法不同资料里指代还不完全一样。今天不打算堆架构图而是直接聊清楚其中四个最关键的角色kube-apiserver、kube-controller-manager、kube-scheduler、kubelet。搞懂这四个组件的工作原理K8S 集群里的大部分问题基本都能自己查了。这篇文章适合刚把 K8S 跑起来、但遇到问题还只能靠搜索引擎的运维和开发同学。我会把每个组件“为什么这么设计”“请求进来后到底发生了什么”“常见故障要从哪里查”讲透。内容偏实践尽量说人话。1. 先定个框架四大组件是哪四个它们各管什么1.1 为什么说法不统一如果你把 K8S 官方文档翻一遍会发现“控制平面组件”里除了 apiserver、controller-manager、scheduler还有 etcd。所以有些资料会把 etcd 也算进四大组件。这倒也没错但我们在实际排障时一个请求从发出去到 Pod 真正跑起来真正贯穿始终的是 apiserver、controller-manager、scheduler、kubelet 这四个角色etcd 更像是背后提供持久化的“账本”。把它看作辅助件更容易理解整个流程否则一旦 etcd 出问题四者的职责边界很容易被搅浑。所以我个人习惯把上面四位当成核心闭环。kube-apiserver 是集群对外的唯一入口所有读写请求都要经过它kube-controller-manager 是控制器的集合负责把用户的期望状态转成集群里的对象操作kube-scheduler 负责给新 Pod 选一个最合适的节点kubelet 则是在每个具体节点上执行任务的人。另外一个常被提起的 kube-proxy 管的是 Service 网络转发和这四个组件不在同一条“创建链路”上后面可以单独聊。1.2 四个角色的责任边界用一个公司来打比方你会记得很牢kube-apiserver 是前台兼门卫所有外部请求都要从这里进出所有状态变更都要先记到“台账”里。kube-controller-manager 是后台监督部门它盯着台账里“应该是什么样”一旦发现现实和台账不一致就立刻安排人手去纠偏。kube-scheduler 是人事分配部门新入职的 Pod 到底去哪个节点上班由它拍板。kubelet 是驻在每个业务现场的基层执行者它真正把 Pod 拉起来、盯着它存活、定期向总部汇报。这四个角色能配合起来靠的是 K8S 贯穿始终的“声明式状态”思想。你不需要告诉它具体怎么一步步实现只要描述你期望的结果比如“我要 3 个 nginx 副本”K8S 自己会想办法完成。理解了这个思想后面所有组件的原理都是顺理成章的。2. kube-apiserver集群的唯一入口和状态中枢2.1 一个请求进 apiserver要先过四道关先明确一点kubectl、kube-controller-manager、kube-scheduler、kubelet 访问集群本质上都是在和 apiserver 通信。apiserver 是唯一会直接读写 etcd 的组件所以它天然成为整个集群的“串行点”。这意味着它的稳定性直接决定集群的可用性所有其他组件失去 apiserver 后最多只能维持本地临时状态。当一个请求向 apiserver 发过来比如你执行kubectl create deployment nginx --imagenginx它并不是直接写库而是依次通过四道关卡。第一关是认证Authentication确认“你是谁”。K8S 支持多种方式最常见的是客户端证书、RBAC 用的 ServiceAccount token、以及外部 OIDC token。认证失败时请求会在这一步被拒绝通常返回 401。第二关是授权Authorization确认“你能做什么”。默认使用 RBAC 规则根据用户或 ServiceAccount 绑定的角色判断这次操作是否被允许。这一层返回 403 是最常见的权限问题。第三关是准入控制Admission它会在对象持久化之前拦截并可能修改请求。比如 Namespace 配额检查、PodSecurityPolicy、以及各种动态准入 webhook。很多集群卡在创建资源时没有反应都是被某个 mutating webhook 或 validating webhook 卡住了这点后文会专门讲。第四关才是校验和持久化。apiserver 会检查对象的字段语义是否合法比如镜像名是否合规、字段类型是否正确然后把它序列化写入 etcd。到这里一个对象才真正被集群“承认”。这四个环节里隐藏着很多性能和安全考量。认证、授权、准入都是可以分布式横向扩展的etcd 通常是瓶颈。所以生产集群里etcd 的性能监控和 apiserver 的 QPS 监控必须放在一起看单看 apiserver 不一定能找到问题。2.2 为什么说 apiserver 是状态中枢apiserver 不仅仅是一个 HTTP 服务它还是一个完整的“状态订阅中心”。controller-manager、scheduler、kubelet 都不需要反复轮询整个资源列表而是通过 watch 接口订阅自己关心的对象变更。这有点像公众号消息推送你关注了 Deployment 这个资源类型只要 Deployment 一变apiserver 就把增量事件推给你。为了保证这个机制可靠apiserver 引入了 resourceVersion 的概念。每个资源对象在变更时都会有一个递增的版本号客户端带着上次看到的版本号重新发起 watchapiserver 就能从断点继续推送而不用重新全量拉取。这个设计非常巧妙但实际运行中也有一个常见坑如果 etcd 里的资源变化非常频繁或者集群版本长期不升级watch 连接可能被断开客户端只能被迫做一次全量 list瞬间把 apiserver 和 etcd 的负载推高。下次你看到 apiserver 内存突增但 QPS 也没有明显变化可以先往这个方向想。另外apiserver 对每种资源都实现了严格的版本转换和 OpenAPI 校验这让多个客户端即使版本不同也能正常工作。不过也正因为如此在 apiserver 上做核心配置变更时需要格外小心改一个参数往往会影响所有 client 的兼容性。2.3 实操apiserver 排查常用动作我在排 apiserver 故障时通常不是一上来就翻日志而是先分清楚问题出现在“客户端到 apiserver”还是“apiserver 到 etcd”。如果是第一种我会先执行kubectl get --raw /healthz看 apiserver 是否活着再执行kubectl auth can-i list pods来确认自己的权限是否足够。如果是第二种我会看 etcd 的健康状态和磁盘延迟因为 apiserver 很多超时其实源自 etcd 慢而不是 apiserver 本身忙。真正要看细节时我会这样取日志kubectl -n kube-system logs apiserver-pod --tail200 | grep -E Forbidden|timeout|admission这句命令会过滤出权限拒绝、超时、准入控制相关的日志。如果日志里大量出现Failed to update resource大概率是 watch 或者 etcd 写入链路不稳。此时再看 apiserver 的 metrics比如apiserver_request_total、apiserver_request_duration_seconds等指标比纯靠猜要靠谱得多。记住一点apiserver 不是不能重启但重启只能解决它本身的僵死问题如果根因在 etcd 或网络重启反而会让故障时间更长。3. kube-controller-manager控制循环如何让期望状态变现实3.1 控制循环的三步曲kube-controller-manager 不是一个单独的控制器而是一个控制器集合。里面每一个控制器都遵循同一套“控制循环”模式不断地观察当前状态对比期望状态然后执行动作缩小区间。这个循环通常叫 reconcile loop调谐循环。具体来说它做的是这么三步从本地缓存或者 apiserver 获取期望状态。比如 Deployment 对象里写了replicas: 3。获取当前状态。比如当前实际只有 1 个 Pod。计算差异并执行操作。发现少 2 个 Pod就创建 2 个 Pod发现多了就删除多的。这个循环不是跑一次就结束而是永远在跑。所以即使你手动删掉一个 Pod几秒钟后 controller 会自动补一个新的出来。很多新手第一次看到这种情况会以为遇到了“灵异事件”其实这正是声明式 API 的核心威力。值得注意的是控制器通常不会直接创建最终对象。比如 Deployment controller 做的事是创建一个 ReplicaSet真正负责控制 Pod 副本数的是 ReplicaSet controller。这种分层结构避免了每个控制器都需要理解所有逻辑也方便用户在不同的抽象层级做操作。你改了一个 Deployment 的镜像Deployment controller 会产生一个新的 ReplicaSet旧的 ReplicaSet 再被逐渐缩容这就是滚动更新的本质。3.2 内置控制器很多重点关注这几个kube-controller-manager 里常驻的控制器有几十个但我建议你重点记住下面几类Deployment / ReplicaSet 控制器负责实现副本数、滚动更新、回滚。Node 控制器监控节点心跳发现节点失联后会按照 pod-eviction-timeout 设置的时间窗口把该节点上的 Pod 标记为需要驱逐。Endpoints / EndpointSlice 控制器根据 Service 的后端 Pod 变动动态维护 Service 对应的 endpoint 列表。Namespace 控制器、ServiceAccount 控制器处理 Namespace 生命周期、自动创建 ServiceAccount 等。在排障时你可以反推当某个对象的状态一直和你预期不符先问一句“这个对象的 controller 在哪儿”。比如 Deployment 一直没生成 ReplicaSet那基本是 Deployment controller 没正常工作而不是 scheduler 或 kubelet 的问题。这种“按 controller 定位”的思路比漫无目的地翻日志高效得多。3.3 高可用部署时的 leader election 与脑裂生产环境通常会在两个节点上各部署一个 controller-manager 副本但同一时刻只有一个实例在真正“干活”。这是通过 leader election 机制实现的依赖一个叫 Lease 的锁对象。候选实例启动后都会尝试获取锁拿到锁的成为 leader定期续约另一个实例处于 standby 状态监听锁事件等 leader 失联后接管。这个机制有一个微妙的地方如果出现网络分区旧 leader 可能暂时联系不上 apiserver新 leader 随即抢到锁开始工作但旧 leader 并不一定真的死了它可能在网络恢复后以为自己还是 leader从而出现短暂“双 leader”同时操作。好在 K8S 的控制器逻辑大多做成了幂等操作即使两个 leader 同时处理同一对象最终也只会收敛到期望状态不会像 MySQL 双主那样直接数据冲突。不过实际运维时还是要尽量避免手动干预这些控制器尤其不要用“强杀 leader Pod”的方式来做故障转移正常通过 Deployment 滚动重启即可。另外controller-manager 的日志默认输出到 stdout排查时要记得看它的锁状态相关日志。如果发现集群里所有对象都没反应先kubectl get lease -n kube-system看一下锁的持有者是不是已经失联而不是直接重启所有副本。4. kube-scheduler如何为 Pod 挑一个合适的节点4.1 调度流程过滤、打分、绑定scheduler 的角色是“给未调度的 Pod 找一个最合适的节点”。它监听的是那些spec.nodeName还为空的 Pod并把它们放进内部调度队列。每个 Pod 都会经历过滤、打分、绑定三个阶段。过滤阶段也会被称为 Predicates 或 Filtering。调度器会先把不满足硬性约束的节点排除掉比如节点资源不足、不满足 nodeSelector、节点上有不可容忍的污点等。过滤是“一票否决制”只要有一条不满足节点就被淘汰。打分阶段也叫 Priority 或 Scoring。调度器会对剩下的每个节点打分考量的是更偏“软性偏好”的因素包括资源使用是否均衡、节点亲和性、Pod 亲和性/反亲和性、拓扑分布约束等。分数最高的节点会被选中。最后一个阶段是绑定。调度器调用 apiserver 的 bind 接口把 Pod 的nodeName字段更新为目标节点。这一步之后Pod 就不再属于“待调度”状态后续交给对应节点的 kubelet 处理。如果你用kubectl describe pod pod名字在 Events 区域经常能看到类似Successfully assigned default/nginx-xxx to node-02这样的记录这行字就是调度器绑定成功的证据。Pod 一直 Pending 时去 Events 里翻一翻往往能找到“0/3 nodes are available”这类调度失败原因。4.2 默认策略和容易被忽略的细节默认调度器并不是只看 CPU 和内存的剩余量它还会做很多更细致的判断。我见过不少同学以为内存够就应该调度上去实际上还会被下面这些因素拦住端口冲突如果新 Pod 要使用 hostPort但目标节点上已经有一个 Pod 占用了相同端口调度器会淘汰这个节点。污点与容忍度节点上有NoSchedule污点而 Pod 没有对应容忍度就无法调度上去。Pod 亲和性/反亲和性比如要求两个 Pod 必须同节点或者必须分布在不同的可用区这些约束会直接影响计分。拓扑分布topologySpreadConstraints会把同一副本集的 Pod 尽量打散到不同节点避免某个节点挂掉导致全部不可用。一个容易被忽略的坑是 NodePort Service。调度器打分时不会考虑 Service 的 NodePort 端口但 kubelet 在启动 Pod 时如果发现宿主机端口被占用会直接报错。所以我在规划测试集群时会额外给 NodePort 做一个端口段规划避免因为端口冲突产生诡异现象。另外调度器本身是有状态的它内部的缓存记录了每个节点的资源分配情况。如果某个节点长期上报异常或容量突变scheduler 的缓存可能和实际不一致这时kubectl describe node看到的 Allocated resources 会有参考价值但不一定等于调度器视角。Pod 被卡在 Pending 时先查kubectl get events --all-namespaces再看 scheduler 日志这是最省时间的两步。4.3 自定义调度器什么时候才需要默认调度器能满足绝大多数场景但有些特殊需求它做不到。比如你要按 GPU 卡型、机架位置、甚至节点成本来调度默认策略就不够用。这时候有两条路线一是用调度器扩展器Scheduler Extender机制让默认调度器在过滤和打分时把候选节点转发给你自己的外部服务再由外部服务追加评分。这种方案侵入性小但会额外增加调度器的 HTTP 调用延迟不适合超大规模集群。二是给 Pod 指定schedulerName完全使用自己写的独立调度器。K8S 允许一个集群里同时运行多个调度器只要 Pod 没有明确指定schedulerName就默认用默认调度器。自定义调度器的好处是逻辑完全可控坏处是要自己处理调度队列、失败重试、扩展点等一系列问题。我的建议是先用默认调度器的各种亲和性和拓扑约束看看能不能解决问题。实在不行再考虑 extender最后才考虑独立调度器。一上来就写自定义调度器多半会在调度失败和重试处理上把自己绕进去。5. kubelet节点上真正干活的核心代理5.1 kubelet 到底管哪些事kubelet 是运行在每个节点上的守护进程可以说它是集群里“最后落地”的关键角色。它负责从 apiserver 监听分配到本节点的 Pod并调用容器运行时去创建、启停、删除容器。所谓“监听”还是走 informer 机制而不是每个节点轮询 apiserver。除了 Pod 生命周期管理kubelet 还身兼数职执行三种探针livenessProbe容器是否活着、readinessProbe是否可对外提供服务、startupProbe启动阶段保护。通过 CRI容器运行时接口与 containerd、docker 等运行时通信。上报 Node 状态和 Pod 状态给 apiserver。管理节点资源给容器设置 cgroup 限制执行节点级别的资源驱逐。回收镜像和容器清理不再使用的镜像避免磁盘写满。还要说清楚一点kubelet 只负责“Pod 里的容器生命周期”不负责网络策略和 Service 规则。节点上的网络插件CNI、kube-proxy 是另外的组件它们和 kubelet 配合但不属于 kubelet 内部逻辑。很多人把 IPVS 转发问题归咎于 kubelet其实是找错了对象。5.2 从拿到 Pod 到容器启动经历了什么假设 scheduler 已经把 Pod 绑定到节点 Akubelet 的 informer 会收到这个 Pod 的更新。随后它会进入一个 pod sync 流程先根据 Pod 定义创建 sandbox 容器也就是 pause 容器。pause 容器负责持有 Pod 的网络命名空间和一组 cgroup业务容器通过加入 sandbox 的方式来共享网络和资源。调用 CRI 接口向容器运行时发送创建容器的请求。具体到 containerd 就是创建 pause 之外的业务容器。容器启动后kubelet 会周期性检查容器的状态执行探针。当 readinessProbe 通过后kubelet 会把 Pod 的 Ready 条件更新为 True并同步给 apiserver。这一链路里任何一步出问题现象都会不同。如果是镜像拉不下来容器会一直处于 ContainerCreating事件里会有Failed to pull image如果是 volume 挂载失败事件里常常会出现Failed mount如果是 CNI 网络配置失败容器会启动成功但一直在 CrashLoopBackOff因为业务进程访问不了网络地址。我排查时最依赖的路径就是kubectl describe pod看事件再到对应节点上用crictl ps -a看真实容器状态两者交叉验证。5.3 kubelet 的常见异常表现kubelet 自己出问题通常表现为节点状态异常最常见的是节点变成 NotReady。原因可能很多但按出现频率排我遇到过这几类一是 kubelet 无法连上 apiserver。网络链路问题或者证书过期都会导致状态上报失败节点会短暂或长期 NotReady。证书过期的判断方法很简单看 kubelet 日志里是否有certificate has expired字样或者查/var/lib/kubelet/pki下证书的有效期。二是 PLEGPod Lifecycle Event Generator卡死。PLEG 是 kubelet 内部用来同步容器状态的模块当容器 runtime 响应变慢或 Docker socket 拥堵时PLEG 可能长时间无法完成状态同步结果是 Pod 状态一直不更新看起来像“假死”。重启 kubelet 能临时恢复但根治要检查 runtime 的存储和进程状态。三是资源驱逐。当节点磁盘、内存、inode 达到阈值kubelet 会开始驱逐 Pod这种驱逐往往只针对超过 limits 或优先级较低的工作负载。此时节点状态事件里会出现EvictionThresholdMet。四是 kubelet 和容器运行时的版本不匹配。比如 containerd 升级后没有重启 kubelet导致 CRI 连接异常。这时节点也可能 NotReady需要优先检查 CRI 插件是否正常。排查 kubelet 的第一命令永远是journalctl -u kubelet -f -n 200在 systemd 管理的节点上这个命令能一次性看到 kubelet 的核心日志。如果 kubelet 不在 systemd 下那就找/var/log/kubelet/下的日志文件。看日志时先找error和failed再顺着时间线往上回溯比自己瞎猜强多了。6. 从一条 kubectl apply 看四个组件如何配合6.1 一次 Deployment 创建的端到端时序前四章讲的是单个组件这一章把它们串起来。我用一条最常见的命令举例kubectl create deployment nginx --imagenginx这条命令背后四个组件会依次行动大致时序是这样的kubectl 把 Deployment 对象 POST 到 apiserver经过认证、授权、准入、字段校验后写入 etcd。Deployment controller 通过 watch 发现新的 Deployment发现副本数当前为 0于是创建对应的 ReplicaSet。ReplicaSet controller 通过 watch 发现新 ReplicaSet对照副本数后创建一个不带nodeName的 Pod 对象。scheduler 发现这个未调度的 Pod过滤、打分后选择一个节点并调用 bind 接口把 Pod 的nodeName更新为目标节点。kubelet 通过 watch 发现这个 Pod 属于自己节点开始拉镜像、创建 sandbox、启动容器。容器起来了kubelet 执行探针并把 Pod 的 Ready 状态补全。随后 Deployment 和 ReplicaSet 的 status 也由对应 controller 更新。整个过程如果顺利大概几十秒内完成。但注意第四步 scheduler 是异步调度的所以 Pod 从创建到分配到节点之间可能会短暂处于 Pending 状态这是正常的不代表集群卡住。6.2 哪一步最容易卡住怎么定位根据我维护测试集群的经验最容易卡住的位置往往是第 1 步和第 5 步。第 1 步卡住通常是因为准入 webhook。集群里如果装了一些带 mutating webhook 的组件比如为 Pod 注入 sidecar 的 Istio、或者自定义的 admission controller它们一旦超时请求就会一直悬挂最后 kubectl 报Internal error occurred: failed calling webhook。这种问题要看 apiserver 日志和 webhook 服务的健康状态而不是盯着 Deployment 看。第 5 步卡住则多半是镜像拉取、存储卷、CNI 网络这三类。定位思路很统一先kubectl describe pod看 Kubernetes 层面的事件再到节点上用crictl ps -a和crictl logs container-id看运行时层面的状态。事件和容器状态经常能组成完整证据链比单独看哪一边都靠谱。还有一个隐蔽的场景Deployment 已经创建Pod 也 Running但 Service 访问不通。这就是另一个链路了主要看 EndpointSlice 是否更新、kube-proxy 是否同步了规则不是四大组件本身的问题。遇到网络问题要记得“四大组件管创建网络转发另找人”这样排查时不会被带偏。7. 常见问题与排查技巧实录7.1 apiserver 变慢或请求被拒绝我整理了一个速查表方便第一时间对照处理现象可能原因排查命令/动作所有 kubectl 命令都超时apiserver 过载、etcd 慢kubectl get --raw /healthz结合 metrics 看 QPS 和 etcd 延迟返回 ForbiddenRBAC 权限不足kubectl auth can-i --list -n namespace返回 401 Unauthorized认证失败、证书过期检查 kubeconfig 和客户端证书有效期创建资源卡住无响应准入 webhook 超时查 apiserver 日志检查 webhook 服务状态记住apiserver 的很多“慢”其实是下游 etcd 慢永远是先排查存储再排查 API 层。7.2 controller-manager 不干活但没报错如果你创建了 Deployment却迟迟没有 ReplicaSet先确认 controller-manager 的 Pod 是否正常。命令kubectl -n kube-system logs deployment/kube-controller-manager --tail200日志里如果完全没有相关控制器的输出可以检查 leader election 的锁是否被一个不健康的实例占着。用kubectl -n kube-system get lease查看 holder如果 holder 指向一个已经不存在的 Pod等待 Lease 到期后另一个实例会自动接管。不要手动去删 etcd 里的 Lease 记录那属于直接操作状态存储容易引发更多问题。7.3 Pod 一直 Pending调度器说了什么Pending 是调度问题的最典型现象。处理顺序kubectl describe pod pod看 Events 里的调度失败原因。kubectl get nodes -o wide检查节点是否 Ready、是否有污点。检查资源和存储类PVC 是否 Bound节点是否还有可用 CPU、内存。检查 Pod 是否指定了schedulerName如果指向自定义调度器而该调度器不存在就会一直 Pending。大多数 Pending 问题都能在前两步里找到答案。7.4 kubelet NotReady怎么查节点 NotReady 时第一时间登录节点查看 kubelet 日志journalctl -u kubelet -f -n 200日志里经常已经明确提示了原因比如证书到期、无法连接 apiserver、PLEG 卡死。然后检查容器运行时crictl ps确认 containerd 或 docker 还能正常响应。最后看/var/lib/kubelet/pki/下的证书有效期。如果证书过期可以联系集群管理员续签或重新生成 kubelet 证书。7.5 独家避坑心得有几条经验我觉得比命令本身更有价值单独写出来一、不要直接编辑 etcd 里的数据去“修”对象哪怕只是想改个 status。正确做法是修改 API 对象的 spec让 controller 自己去调谐。手动改 etcd 很容易因为和 apiserver 缓存不一致导致一连串奇怪问题。二、升级 apiserver 前先检查所有 mutating webhook 的配置。我遇到过业务方自己部署的 webhook 在升级后超时导致整个集群创建资源全部被卡住最后只能临时删掉 webhook 配置才恢复。准入 webhook 是集群的“放大器”一个不起眼的服务挂了可能影响全局。三、排查 kubelet 之前先检查节点时间同步。NTP 不同步会造成证书验证失败、心跳上报异常很多“莫名其妙”的 NotReady最终都是时间偏差引起的。四、重启 controller-manager 时不要一股脑把所有副本都删掉再用手工方式拉起来最好用 Deployment 的滚动重启方式。这样能保持 leader election 的连续性降低人为操作出错的风险。最后再分享一个我自己的习惯排查 K8S 问题时先不急着翻日志而是先在脑子里画一条从“制造请求的人”到“真正动手的人”的链路再按这条链路一步步排除。你会发现K8S 里的问题大多不是某个组件坏了而是组件之间的衔接断了。把这四个组件吃透后面看调度、网络、存储都会轻松很多。