1. 为什么先说清“四大组件”控制平面的骨架与职责边界1.1 哪四个组件怎么给它们分工标准K8S集群的控制平面由四个核心组件构成kube-apiserver、etcd、kube-scheduler、kube-controller-manager。这句话本身不新鲜但很多人会把它们和节点侧组件混在一张图里讲导致后期排查问题时分不清“到底该去看谁的日志”。我自己的习惯是先按业务角色记etcd负责“记账”apiserver负责“看门和传话”scheduler负责“分房子”controller-manager负责“盯状态、随时纠偏”。四个角色围绕同一份集群状态反复协商谁都不是单打独斗。先捋一遍边界。kubelet 和 kube-proxy 虽然也是K8S的必备组件但它们跑在工作节点上职责是执行而不是决策。容器运行时containerd、CRI-O、docker只负责把镜像跑起来。我们说的“四大组件”是站在控制平面视角看的核心四件套它们共同决定了集群对外暴露的API、持久化状态、调度结果和期望状态收敛能力。1.2 为什么“多一个少一个”都不行这四个组件不是可以随意替换的四选一关系而是层层依赖的闭环。apiserver是唯一能操作etcd数据的入口其他组件都通过它读写集群状态etcd保存了所有对象的最终一致数据scheduler负责给待调度的Pod选择节点但它自己不直接碰etcd而是通过apiserver完成Pod的字段更新controller-manager则负责保证实际状态无限靠近期望状态。任何一个组件长时间不可用控制平面都会出现连锁反应但表现方式不一样比如apiserver挂了所有组件都“失联”etcd挂了apiserver会变成只读或直接不可用而scheduler挂了只是新Pod调度不了已运行的Pod不受影响。理解这个差异排障时才能第一时间锁定范围。还有一个常被忽略的点四个组件各自的故障半径完全不同。etcd和apiserver任何一方不可用整个集群都处于“半残”状态scheduler和controller-manager因为有选主机制多副本下通常能自动恢复反而没那么容易让集群完全瘫痪。如果你在画架构图一定要把“存数据”和“管API”这两层放在最核心的位置因为它们是所有协作链路的底座。2. 为什么说 apiserver 是集群所有操作的唯一入口2.1 一条 kubectl 命令背后的完整请求链我们平时用 kubectl apply 提交一个文件看起来是一行命令实际在 apiserver 内部走了一长串链路。首先 kubectl 会把 YAML 转换成 JSON 或 protobuf 请求通过HTTPS发送给 apiserverapiserver 拿到请求后不会立刻写库而是依次执行三个动作认证Authentication、授权Authorization、准入控制Admission Control。认证解决“你是谁”常见方式有客户端证书、ServiceAccount Token、OpenID Connect、静态Token等。对集群外部用户来说客户端证书和OIDC比较常见对集群内部组件比如scheduler和controller-manager访问apiserver时通常使用ServiceAccount的Token。授权解决“你能干什么”Kubernetes默认用RBAC也就是先定义Role/ClusterRole里的一组规则再通过RoleBinding/ClusterRoleBinding把规则绑到User、Group或ServiceAccount上。很多人遇到“403 Forbidden”都去查网络或证书其实八成是RBAC授权规则没写对。2.2 认证、授权、准入控制这三个环节的先后顺序顺序很重要先认证再授权最后准入。准入环节又分两步先是MutatingAdmissionWebhook变更为主的webhook可以修改请求内容比如自动注入sidecar、给Pod加默认标签后是ValidatingAdmissionWebhook校验为主只检查不修改比如校验资源配额、检查镜像地址是否在白名单内。内置的准入控制器也在这个阶段生效比如LimitRanger、ResourceQuota、PodSecurity还有最常用的NamespaceLifecycle它保证如果命名空间正在删除或不存在就不能在里面创建新资源。我实际排障时有一个经验凡是出现“请求被准入控制器拒绝”的报错错误信息里通常会带上具体的准入控制器名。比如明明配置了ResourceQuota创建Pod却说配额不足就要计算该namespace下其他工作负载的request总值而不是只看当前这个YAML有没有问题。如果遇到MutatingAdmissionWebhook不生效先确认webhook的failurePolicy是Ignore还是Fail生产环境很多事故都是把webhook配成Fail然后又把自己集群的apiserver流量也拦进来形成“死锁”。2.3 为什么 apiserver 必须无状态、可水平扩展apiserver是整个控制平面中唯一允许随便多副本部署的组件因为它本身不保存任何业务状态所有数据都在etcd里。官方部署里apiserver通常放在负载均衡后面例如把6443端口暴露给kubeconfig通过健康检查剔除异常实例多实例同时运行。这个设计带来的最大好处是滚动升级apiserver时不需要担心选主问题新Pod起来后直接接管流量某个节点上的apiserver出故障客户端通过负载均衡自动切走。不过“无状态”是相对业务数据而言的它内部还有很多运行时缓存和连接池。比如每个apiserver都会维护针对etcd的watch连接维护各个资源的informer缓存。也就是说即使有四副本每个副本也都会完整缓存一份集群对象内存消耗会随集群规模增长。在我维护过的中大规模集群里apiserver的常见瓶颈不是CPU也不是磁盘而是这两个一是大量客户端同时发起全量List请求瞬间峰值打满带宽二是每个watch事件都完整广播给所有订阅者当Pod频繁重建时事件风暴会拖垮apiserver。这里还有个容易忽视的点apiserver默认有两个流量控制参数max-requests-inflight 和 max-mutating-requests-inflight分别控制读请求和写请求的最大并发。很多人不改这个参数默认值分别是400和200。一旦集群规模上千并发的kubectl、监控采集、Controller的List-Watch叠加经常会触发拒绝请求。早两年我处理过一个问题监控程序每个采集周期都去List全部Pod导致apiserver持续出现“request was aborted”报错。后来把采集方式改成watch增量并稍微调大inflight限制集群才恢复稳定。记住一个原则能watch别list能增量别全量。3. etcd集群的“记账本”到底怎么工作3.1 数据结构从 /registry 到 watch 版本号etcd 不关心K8S对象长什么样它只存KV和版本号。K8S的资源数据在etcd里的key路径是有规律的/registry/pods/{namespace}/{name}、/registry/deployments/{namespace}/{name}、/registry/secrets/{namespace}/{name}。一个对象的所有字段包括metadata、spec、status都被序列化后存在这一个key下面。所谓“对象更新”在etcd层面就是一次覆盖写并生成一个新的Revision版本号。版本号是理解etcd和K8S协作的关键。K8S通过ObjectMeta.resourceVersion实现乐观并发控制当你提交一个update请求时apiserver会比对请求里的resourceVersion和etcd里的当前版本如果不一致就返回Conflict避免两个客户端同时覆盖写同一个对象。这也是Deployment滚动更新里ReplicaSet数量不能被随便改飞的原因之一。3.2 MVCC、压缩和 watch 通知的运作机制etcd的存储是MVCC结构每次写操作不直接覆盖磁盘数据而是追加一个新的版本记录方便客户端从任意历史版本开始订阅变更。但历史版本不会永远保留K8S会周期性调用etcd的压缩接口compaction把指定版本之前的历史记录清理掉。如果压缩不及时etcd的磁盘占用会持续增长最终触发quota限制。我曾经遇到过db大小涨到1.6GB才报警的案例这个值已经接近默认的2GB硬限制当时只能先做 defrag 缓解同时检查是哪个资源在疯狂写。watch 是K8S各组件之间协作最重要的机制。一个客户端可以传入fromRevisionetcd会从那个版本开始持续推送后续变更事件。对etcd来说watch消费者越多维护成本越高对K8S组件来说这个机制保证了每个控制器只关心自己负责的资源类型不依赖轮询。我在做故障复盘时经常强调K8S集群本质上是“一堆围绕apiserver转的watch进程”哪个环节Watch断了哪个环节就失去感知。3.3 生产环境部署 etcd 必须盯住的三个指标etcd在整个控制平面里性能最敏感因为它直接承受频繁的写盘操作K8S对etcd的写延迟极其敏感。我的生产经验有三个重点。一是磁盘。etcd强烈建议使用本地SSD并且最好独立挂载不要和系统盘、容器镜像目录混用。因为Raft每个日志条目都要fsync落盘磁盘的fsync延迟直接决定写请求的P99。如果磁盘IO被镜像拉取占满etcd必然产生高延迟进而拖慢整个集群。二是节点数量和网络。生产环境至少3个etcd实例最多建议5到7个再多反而因为Leader和Follower之间的心跳网络开销下降。etcd的多副本依赖Raft选举多数派写成功才算成功所以必须是奇数个。网络分区时如果失去多数派etcd会拒绝写请求这是数据一致性优先于可用性的取舍属于正常保护机制。三是容量和碎片。默认quota-backend-bytes是2GB超过后etcd进入只保留读不写状态这是所有K8S故障里最可怕的之一。要提前配置告警定期用 etcdctl endpoint status 查看 db size以及定期执行 defrag 压缩碎片。另外如果集群规模的Secrets很多建议开启etcd客户端压缩否则每1000个Secret的watch/read流量可能让带宽吃紧。4. Scheduler如何为 Pod 选出最合适的节点4.1 调度主流程从入队到绑定分两步走新版调度器把所有动作拆成两个阶段调度周期Scheduling Cycle和绑定周期Binding Cycle。调度周期里调度器从队列中取出一个待调度Pod对它执行一系列过滤和打分得到一个候选节点列表绑定周期则负责把调度结果落实下去包括给Pod绑定节点、处理PVC绑定、运行前置绑定插件等。调度器不主动创建Pod它只监听那些 spec.nodeName 为空的Pod。一旦看到新Pod进入Pending状态才会入队处理。整个过程的数据都来自apiserver的informer缓存所以出现调度器反复重启时不会因为缓存丢失就盲目重算而是以apiserver返回的事件为准继续工作。4.2 过滤与打分predicates/priorities 到底在算什么旧版文档里常叫 predicates 和 priorities新版已经变成插件化的扩展点但底层逻辑没变。过滤阶段是硬性条件只要有一个不满足就剔除节点资源CPU/内存.request是否够用、端口是否冲突、主机名/zone是否匹配、nodeSelector和亲和性是否满足、污点是否有对应容忍。打分阶段是软性选择在过滤后的剩余节点里排名最常用的是资源分配比例选择CPU和内存剩余最多的节点此外还有镜像局部性、Pod亲和性反亲和性、节点拓扑分布等加分项。这里我特别想提一点调度器默认只看 Pod 的 requests而不看实际使用量。如果很多应用申请的requests远大于实际用量会出现“分配了但用不满”的假性资源紧张导致Pod堆在一台节点上其他节点空转。反过来如果requests设得很低又容易超卖。所以运维的日常功课之一就是定期分析 request 与真实 usage 的比值帮助团队校准配额。4.3 让调度更符合业务需求的几个实用手段第一是节点亲和性和反亲和性。比如把有状态的中间件固定到SSD节点把网络敏感的Pod尽量打散到不同可用区用 topologySpreadConstraints 控制Pod在节点间的均衡分布。第二是污点Taint和容忍度Toleration这是给节点做“定向筛选”的利器节点打上专用污点后只有声明容忍的Pod才能调度上去。我一般会为监控组件和日志组件单独划分节点池并打污点避免普通工作负载挤占它们的稳定性。第三是配置调度器扩展。如果内置节点打分策略无法满足可以部署自定义scheduler或开发scheduling framework插件。不过我个人不太建议一上来就写自定义调度器而是先用Pod的优先级PriorityClass、NodeSelector、资源预留这些内置能力解决90%的问题。只有类似GPU拓扑感知、机房带宽感知这种强业务约束时才值得走向插件化改造。还有两个非常常见的坑一是集群规模很大时调度器默认不会遍历所有节点而是通过 percentageOfNodesToScore 参数限制参与评分的节点比例导致在某些边缘条件下调度结果不是全局最优二是Pod挂到Pending状态后如果不是资源不足先去看有没有污点不匹配、PVC不满足、NodeSelector没有节点符合别一上来就怀疑调度器坏了。5. Controller Manager整个集群的“纠错循环”是如何转起来的5.1 控制器模式期望状态、当前状态、Reconcile 循环K8S的控制器设计哲学可以概括为一句话所有资源都有期望状态控制器负责让当前状态不断逼近期望状态。Controller Manager 内部其实聚合了非常多独立控制器常见的有NodeController、ReplicaSetController、DeploymentController、StatefulSetController、DaemonSetController、EndpointController、NamespaceController、GarbageCollector等等。它们共享同一个informer框架各自监听自己关心的资源。一个Reconcile循环的基本动作是从informer里取出某个资源的当前状态与期望状态做差异比较如果有差异就通过apiserver发起写请求去校正。比如ReplicaSet期望3个副本实际只有2个PodRS控制器就会创建第3个Pod实际有4个就删掉多余的。这个“差异驱动”的模型好处是天然容错即便控制器进程崩溃重启重启后从apiserver恢复informer缓存依然能根据当前状态继续收敛不需要额外保存工作进度。5.2 Deployment、Endpoint 等高频控制器的内部逻辑DeploymentController 不直接管Pod它管理的是ReplicaSet然后由ReplicaSetController控制Pod数量。Deployment滚动更新时DeploymentController会创建一个新RS逐步调整新旧RS的副本数直到所有Pod完成版本切换。这个过程受strategy.maxSurge和maxUnavailable两个参数约束前者控制滚动中最多可以多出多少个Pod后者控制最多可以少多少个Pod。很多人升级应用时发现Pod数量突然变成双份不是故障是maxSurge在起作用。EndpointController/EndpointSliceController 负责把Ready Pod的地址写入到Service的后端端点中。这里的“Ready”很关键如果Pod的readinessProbe失败控制器会把它从端点列表中移除但不会删除Pod本体。这就是为什么服务有时候明明有Pod但访问时却有可能出现部分后端失败的现象需要结合探针结果去看而不是直接看Pod是不是Running。5.3 多个 Controller Manager 同时运行怎么防止脑裂生产环境为了高可用通常会给controller-manager和scheduler各部署多个副本比如3副本但它们同时只能有一个真正在工作。这个机制叫Leader Election选主默认通过 Lease 资源旧版本是Endpoint实现。每个副本启动时都会尝试获取同一把锁拿到锁的成为Leader其他副本处于Standby状态只监听Lease对象的变化一旦Leader失联Standby就会重新竞争选主。很多新手看到Controller Manager副本状态是“not leading”就以为集群出问题了其实这是正常现象。排查选主问题时要关注的是Lease对象是否持续续约、Leader日志里有没有频繁切换。比如节点时间不同步、网络抖动会导致租约续不上和频繁重新选举。我还遇到过一次因为两个节点时钟偏差超过几百毫秒导致选主反复横跳最终表现为Deployment控制器间歇性不干活。所以在部署控制平面时NTP时间同步是必须提前做好的基础工作。6. 四组件协作的一次完整叙事从创建 Deployment 到 Pod 真正跑起来6.1 正常链路谁先醒、谁后动、谁负责收尾假设你在CI里执行了 kubectl apply -f deployment.yaml我们可以完整走一遍四组件协作链路。第一步apiserver收到请求认证、授权、准入全部通过后把Deployment对象写入etcd。第二步DeploymentController通过watch收到新增Deployment事件发现spec.replicas3于是创建对应的ReplicaSet对象也通过apiserver写入etcd。第三步ReplicaSetController看到新RS的replicas3当前没有Pod于是创建3个Pod对象。这3个Pod的spec.nodeName为空进入Pending状态。第四步Scheduler从队列中取出这3个Pod执行过滤打分为每个Pod选出节点然后通过apiserver更新Pod的spec.nodeName完成Bind。第五步节点上的kubelet通过watch察觉到分配到自己名下的Pod调用容器运行时把镜像拉起来同时持续上报Pod状态。第六步状态更新后EndpointController把Ready Pod加入Service端点。这条链路最值得玩味的一点是所有组件之间没有直接的RPC调用全部通过apiserver的watch来解耦。谁先启动、谁后启动都不重要因为每个组件都是“看到期望状态和实际状态不一致就动手”的循环。所以即使DeploymentController晚于Scheduler启动最终状态依然会被收敛。6.2 节点故障时控制平面的紧急联动再看一个故障场景某个工作节点突然宕机网络也断了。首先感知到的是NodeController因为它持续监听节点心跳NodeLease。超过node-monitor-grace-period默认40秒没有心跳NodeController会把节点置为NotReady。之后进入pod-eviction-timeout默认5分钟计时到期后NodeController会开始驱逐该节点上由控制器管理的Pod它把Pod删除其他控制器或scheduler随后把这些Pod放在其他节点重新拉起。这里需要区分直接创建的裸Pod没有控制器保护不会被自动重建只有Deployment、StatefulSet、DaemonSet等控制器管理的Pod享受这个福利。与此同时EndpointController会更新Service端点把故障节点上不再Ready的Pod从后端摘除保证新流量不再打过去。整个过程都由controller-manager里的不同控制器并行完成最终让集群在约5分钟级别的时间窗口内完成自愈。最近几版还可以通过TaintManager更快地给故障节点打上污点并立即驱逐容忍时间更短的Pod细节略有差异但总体故事就是这个。7. 日常运维中我建议你重点盯住的三类问题7.1 怎么快速判断“谁”出了问题遇到集群异常我最常做的三个动作先看apiserver的健康状态再查etcd的磁盘和延迟最后看scheduler和controller-manager的选主日志。apiserver的状态接口通常通过kube-system里对应Pod的/healthz或/readyz暴露可以直接用curl查看etcd则看 etcdctl endpoint health 和 endpoint status重点比较节点的WAL fsync延迟scheduler和controller-manager如果配置了多副本一定要确认最近没有反复切换Leader切换次数异常往往是时钟或网络抖动造成的。还有一类问题是各组件活着但集群行为异常。这时候要看对应控制器的日志比如Deployment不伸缩先确认ReplicaSetController有没有因为RBAC报错无法创建PodPod一直Pending要看Scheduler日志里有没有具体的过滤失败原因Service后端为空要看EndpointController是不是因为Pod readiness探针失败被剔除。记住一个原则K8S的错误链路几乎都可以顺着“对象字段变化”一层层在上层组件的日志与事件里查到根因。7.2 控制平面性能问题通常出在哪几个环节根据我这几年的实践控制平面的性能瓶颈基本集中在三处apiserver的流量风暴、etcd的磁盘延迟、informer缓存的内存增长。流量风暴最常见于所有节点同时重启或者监控系统用全量List拉取状态。从设计上就应该限定List的大对象范围改用watch和分页拉取同时合理配置apiserver的并发限制必要时对特定客户端设置请求限流。etcd磁盘延迟只能靠基础设施保证使用本地SSD、独立挂载、配置好告警。informer缓存内存则取决于数量级一个几千Pod规模的集群每个apiserver和controller-manager都完整缓存所有资源内存消耗相当可观规划节点资源时要一并算进去。7.3 给团队新人的三点学习建议如果要快速建立“四大组件如何协作”的整体认知我建议按这个顺序实践第一步用kind或minikube起一个单节点集群在节点上 ps 查看apiserver、etcd、scheduler、controller-manager四个进程的启动参数亲手把证书路径、数据目录、绑定地址对应起来。第二步用一个简单的Deployment做实验边创建边同时运行 kubectl get events -w 与 kubectl get pod -o yaml观察spec.nodeName、status.conditions 的逐步变化。第三步把一个节点的kubelet停掉观察Node状态、Pod驱逐、Service端点更新的时间线记录每一步发生在第几秒。走完这三步很多人对K8S的“声明式协作”会有脱胎换骨的认识。我自己带过不少刚入门的小伙伴发现最常犯的误区是一上来就死记组件名词却不知道每个组件对应哪条日志、哪个指标。真正的理解是在异常中建立起来的当你亲手把一个组件停掉看到其他组件如何反应远比背十遍架构图更深刻。这也是我为什么愿意花大量笔墨描述协作链路和故障场景的原因。最后再分享一个小技巧排查任何一种控制平面异常先不要急着看YAML配置而是按“哪条链路的哪种资源期望状态和实际状态不一致”这个思路去切入。只要把问题定位到具体组件、具体资源、具体阶段剩下的就只是看日志和指标的事了。K8S没有那么多玄学大部分疑难杂症最后都能落回到这四件事上谁在存数据、谁在管API、谁在做调度、谁在纠偏差。