资讯动态

Kubernetes核心原理拆解:声明式控制循环、调度、网络存储与排障

发布时间:2026/10/27 14:08:31 来源:尧图企业网站定制
1. 先把K8S核心原理拆成一张“控制论”地图我把K8S核心原理讲透的第一句话通常是这句Kubernetes 不是一台机器而是一套“让现实不断逼近期望”的反馈系统。刚上手那阵我也把它当成一堆命令的集合去背——kubectl run、kubectl expose、kubectl scale背得滚瓜烂熟可一旦 Pod 起不来、Service 不通、扩容卡住就只能靠猜。后来真正让我开窍的是把整套东西还原成一条链路用户写下一份“期望状态”某个组件把它存起来另一些组件盯着它看看到现实和期望不一致就动手修修完再上报循环往复。这条链路叫声明式控制循环它是理解 K8S 核心原理的骨架也是后面调度、网络、存储、排查所有问题的共同底座。这篇内容写给三类人手上有集群、日常跑业务但遇到问题只会重启的运维和开发正准备面试、被问到“一次 apply 背后发生了什么”就卡壳的同学以及想把集群设计得更稳、容量算得更准的架构角色。我不会从“容器是什么”讲起默认你至少敲过 kubectl、见过 YAML。读完你应该能把“K8S 为什么这么设计”和“出问题时该看哪个组件”串成一条线而不是记一堆孤立的知识点。下面这张地图我会先用它把全局铺开然后逐块往下钻。1.1 声明式API从“下命令”到“交作业”的思维切换命令式和声明式最直观的差别我用一个生活类比命令式是你站在厨房门口喊“切葱、下锅、翻三下、关火”每一步都要你盯着声明式是你把菜谱贴在冰箱上说“我要一盘葱花蛋”然后有人反复看菜谱、看锅里缺什么补什么。K8S 选的是后者它的“菜谱”就是你提交的 YAML。你写replicas: 3意思不是我命令你“创建 3 个”而是“我要 3 个现在有几个你自己看着办”。这个切换带来的第一个直接后果YAML 里的字段是“期望”不是“动作”。所以当副本数从 3 改成 5控制器不会“创建 2 个”而是“发现现有 3 个、期望 5 个、差 2 个那就补 2 个”反过来你删掉一个 Pod它也不会“撤销删除”而是“发现只有 2 个补 1 个”。理解了这一点你就不会奇怪为什么删 Pod 之后它又活了——那不是 bug那是 ReplicaSet 在忠实执行你的菜谱。第二个后果是幂等。同一份 YAML 提交十次结果应该一样。这让 GitOps 那套流程成立仓库里的 YAML 是唯一事实集群状态是它的投影。我实际维护过的一个模拟项目里把发布流程从“人登机器执行脚本”改成“改仓库、等同步”最大的收益不是自动化而是你能随时知道集群“应该”长什么样。有了这个基准排查时才有对照物。第三个后果也是最容易被忽略的声明式 API 要求所有组件都遵守同一套“读写协议”。组件之间不直接调用对方的私有接口而是统一走 apiserver 读写资源对象。这条约束是 K8S 能做成插件化生态的根本原因——你想加一个自定义能力不用改核心代码注册一个 CRD 加一个控制器就行。理解到这一层你再去看 Operator 模式会觉得顺理成章。注意声明式不等于“什么都不用管”。声明式只保证“最终一致”不保证“立刻一致”。中间那段时间窗里系统是可以处于任何中间状态的这就是为什么滚动更新期间会出现新旧版本混跑。1.2 控制循环K8S真正的心跳在哪如果只能记一个词我建议记reconcile调和。整套系统里几乎每一个“智能”行为本质都是一个死循环读取期望状态 → 读取实际状态 → 比较 → 采取动作 → 回到开头。ReplicaSet 控制器、Deployment 控制器、Node 控制器、Endpoint 控制器、Job 控制器、你写的 Operator全是这个套路。这带来一个很反直觉的结论控制器的代码里没有“事件处理逻辑”只有“当前状态到目标状态的差值处理”。消息丢了无所谓因为下一轮循环会重新读一遍全量状态。这种设计让系统天然抗抖动——apiserver 短暂不可用、watch 连接断了恢复之后不需要补历史事件重新 list 一次就能对齐。我见过不少自研组件栽在这里把逻辑写成“收到 event A 就做 B”一旦 event 丢了就永久错位而写成“每次都根据当前状态判断该不该做 B”丢多少次都不影响最终结果。控制循环能跑起来靠的是List-Watch 机制加上本地缓存Informer。每个组件启动时先 list 一次全量拿到资源版本号然后从这个版本开始 watch 增量所有数据存在本地缓存里业务逻辑读缓存而不是每次打 apiserver。这套机制解决了两个问题一是 apiserver 不会被成百上千个组件的轮询打爆二是组件处理速度不受网络往返限制。代价是缓存可能短暂滞后所以你会遇到“Pod 明明删了控制器还在处理它”的情况——那不是矛盾是时序。我在调一个大规模场景时踩过一个坑某个控制器每次处理都去 apiserver 实时 Get节点数一多apiserver 的 QPS 直接顶到限流阈值整个集群的写操作开始排队。改成读 Informer 缓存、只在真正需要强一致时才回源问题立刻消失。这个经验让我形成一个习惯看到某个组件在“狂打 apiserver”第一反应不是加机器而是看它有没有该用缓存却没用。1.3 全景视角一次kubectl apply之后到底发生了什么这是我最喜欢问面试者的题因为它能把整条链路串起来。我按实际发生的顺序拆成八步每一步对应一个你能在排查时定位的组件。第一步kubectl 在本地读 YAML、做基本校验、把内容转成 JSON然后决定用 create 还是 patch。它其实会先发一个 GET 看对象存不存在这就是为什么第一次 apply 会比后续慢一点。第二步请求打到 apiserver先过认证你是谁再过鉴权RBAC 允许你干什么然后进准入控制——这里分两块变更类准入可以改你的对象比如自动注入 sidecar、补默认值校验类准入只能拒绝不能改。第三步通过之后写 etcd。注意apiserver 是唯一直接和 etcd 对话的组件其他所有组件都只能通过它读写这条设计保证了权限和校验只有一处实现。第四步Deployment 控制器 watch 到这个新对象按模板创建 ReplicaSetReplicaSet 控制器再创建 Pod 对象。注意此时 Pod 已经有了但它还没落在任何节点上spec.nodeName是空的。第五步调度器 watch 到这批“待调度”的 Pod开始筛选和打分选出一个节点然后通过 binding 子资源把节点名写回去。第六步目标节点上的 kubelet watch 到这个被绑定的 Pod开始干活拉镜像、调容器运行时创建沙箱、调网络插件配 IP、挂载存储卷、启动容器。第七步kubelet 把 Pod 状态定期上报给 apiserver。第八步Endpoint 控制器发现 Pod 变成 Ready把它加进对应 Service 的后端列表流量这才真正能打进来。这八步里任何一步断了表现都不一样。Pod 一直 Pending说明卡在第五步Pod 一直 ContainerCreating多半卡在第六步Pod Running 但 Service 访问不通大概率卡在第八步。排查的本质就是先判断断在第几步再去看那一步对应的组件日志。这套定位法比“重启试试”高效得多也是我后面写排查速查表的底层逻辑。2. 调度与资源模型Pod到底被谁安排在哪调度这块很多人只记“调度器决定 Pod 去哪”但真实集群里“去哪”这件事被拆成了三层调度器决定初始位置、kubelet 决定能不能真跑起来、节点压力大时还会被驱逐走。三层各有各的规则混在一起看就容易糊涂。我下面按这三层展开。2.1 调度器的两段式过滤与打分调度器每次处理一个 Pod走的是先过滤、再打分两段式流程。过滤阶段是一票否决只要有一个条件不满足这个节点直接出局打分阶段是给剩下的节点排个名选分最高的。过滤阶段的常见关卡有这么几类资源够不够节点的可分配资源减去已分配 requests还能不能塞下这个 Pod 的 requests 总和、节点选择器与亲和性是否匹配、污点能不能容忍、端口有没有冲突、卷能不能挂到那个节点比如某个可用区的云盘只能挂到同区节点。这里有个关键点必须强调过滤看的是 requests不是实际用量。这是很多人配错的根源我后面单独讲。打分阶段就比较有艺术性了。默认策略里权重比较高的几项是资源均衡度让 CPU 和内存的使用比例都别太偏、节点亲和性偏好、拓扑分布约束让副本尽量散在不同的机架或可用区、镜像本地性节点上已经有这个镜像的加分省拉取时间。最终得分是各项加权求和选最高分。如果分数相同的节点不止一个调度器会随机挑一个——这是刻意设计的为了打散负载避免所有 Pod 都往第一个空节点堆。我实际调试过一个案例一批 Pod 总是集中在少数几个节点上。翻调度日志发现这些 Pod 的资源请求配得极小比如 100m CPU而节点上已有 Pod 的 requests 也很小导致“资源均衡度”这项打分区分度极低最后基本靠随机。解决方案不是改调度器而是把 requests 调到接近真实用量的水平让打分有真实依据。这件事让我意识到资源请求配得虚低不只是浪费还会让调度失去准星。提示想看某个 Pod 为什么没调度成功直接看调度器生成的 Event 就行它会把每个节点被排除的原因写得很清楚比如“0/6 nodes are available: 3 Insufficient memory, 3 node(s) had taint”。这条信息比任何文档都好用。2.2 requests和limits的真实含义这两个字段天天见但真正理解的人不多。我用一句话概括requests 决定“能不能上车”limits 决定“能坐多宽”。requests 是调度依据和资源保障集群会保证节点上所有 Pod 的 requests 之和不超过节点可分配资源limits 是运行时上限超过 CPU 上限会被节流throttle超过内存上限会被杀掉OOMKill。这里必须讲清楚一个常见误区requests 小、limits 大不等于“弹性使用”。看起来很美——平时用一点忙起来能吃到上限。但实际情况是节点上所有 Pod 的 requests 之和可能远小于真实用量之和一旦所有 Pod 同时忙起来物理资源立刻超卖结果是大家一起被节流、一起被 OOM。我见过一个集群因为 requests 普遍配得极低导致节点 CPU 长期跑在 90% 以上业务方天天喊慢加机器也解决不了——因为调度器认为节点还很空新 Pod 继续往上塞。那 requests 和 limits 到底怎么配我的经验是分类型看。CPU 是可压缩资源超了只是变慢所以 requests 按 P50 用量配、limits 放宽到 P99 甚至不设是合理的能提高打包率。内存是不可压缩资源超了就是被杀进程所以 requests 和 limits 最好配成相等宁可浪费一点也别让进程被杀。这个区别是我从几次线上事故里换来的CPU 被限流顶多是响应变慢、监控上能看到延迟曲线抖动内存 OOM 是硬崩用户侧直接看到 5xx而且往往伴随冷启动。还有一点requests 会影响 HPA 的使用体验。基于 CPU 使用率的 HPA算的是“实际用量 / requests”的百分比。如果 requests 配得虚低使用率百分比会虚高HPA 就会疯狂扩容扩出来的 Pod 又因为资源实际不够继续高负载形成正反馈。这类问题在压测时特别明显我一般会建议先把 requests 校准再开 HPA。2.3 QoS等级与驱逐顺序节点资源紧张时kubelet 需要决定先杀谁后杀谁规则就是 QoS 等级。系统根据你配的 requests 和 limits 自动分三档。QoS 等级判定条件驱逐优先级典型适用场景Guaranteed每个容器都设置了 CPU 和内存的 requests 且等于 limits最低最后被驱逐数据库、核心网关、有状态中间件Burstable至少一个容器设置了 requests 或 limits但不满足 Guaranteed居中绝大多数业务无状态服务BestEffort所有容器都没设任何 requests 和 limits最高最先被驱逐临时任务、可中断的批处理驱逐触发靠的是 kubelet 的硬阈值比如可用内存低于某条线、节点磁盘可用率低于某条线。触发后kubelet 按优先级排序先杀 BestEffort、再杀 Burstable 里“超出 requests 最多”的Guaranteed 放最后。这里的关键理解是驱逐看的是“超出 requests 的部分”不是“是否接近 limits”。所以一个 Pod 如果实际用量远高于自己的 requests即使它在 Burstable 这一档里配置得很“规矩”也会优先被盯上。我在设计节点资源预留时用的是一套固定的计算思路。节点总容量是 8 核 32G其中操作系统和系统进程需要预留我一般配system-reserved给 1 核 1Gkube-reserved给 0.5 核 1G再留一点驱逐余量比如memory.available1Gi那么真正可分配给 Pod 的 Allocatable 就是 8 - 1.5 6.5 核32 - 2 - 1 29G。这个数字要和调度器看到的保持一致否则会出现“调度器觉得能放、kubelet 实际放不下”的错配。绕开预留、把可分配资源算满短期看打包率高了长期必然出问题——节点一紧张系统进程先饿死那你连登录都费劲。3. 网络与存储Service、DNS与持久卷的底层逻辑网络是 K8S 里最容易被“会用但不懂”覆盖的一块。能访问通就当懂了可一旦出现“偶发超时”、“重启后短暂不通”又完全没头绪。我把它拆成服务发现、流量转发、域名解析三条线看会清楚很多。3.1 Service的三种类型与kube-proxy的工作方式Service 解决的第一个问题是Pod IP 不稳定。Pod 重建就换 IP如果调用方直接连 Pod IP那每次发布都是一次全量配置变更。Service 给了一组 Pod 一个稳定的虚拟 IP 和一个稳定的 DNS 名这是最核心的价值。三种类型的分工很清楚ClusterIP 只在集群内部可达是默认类型也是最常用的NodePort 在每个节点上开一个固定端口外部通过“节点 IP 端口”访问LoadBalancer 在 NodePort 基础上再向底层基础设施申请一个外部负载均衡入口需要有对应的实现支持。NodePort 的分配范围默认是 30000 到 32767算一下也就两千多个可用端口所以大规模集群里端口冲突是真实存在的运维负担——我一般会尽量少用 NodePort改用统一的入口层Ingress 或网关收敛流量。真正决定“流量怎么转发到 Pod”的是 kube-proxy。它有三种实现模式行为差异很大。模式转发机制性能特征适用规模userspace用户态进程监听端口再转发开销大已基本淘汰仅历史兼容iptables内核规则链做 DNAT小规模稳定规则数随 Service 线性增长匹配是逐条走中小集群够用IPVS内核哈希表做负载均衡查找复杂度低支持更多调度算法大规模集群推荐选型的逻辑很简单iptables 模式下每个 Service 会生成一串规则几千个 Service 之后规则更新本身的耗时和请求匹配的路径都会变长我实测过大几千 Service 的集群iptables 规则同步一次要好几秒期间新规则还没生效新增服务会出现短暂不通。切到 IPVS 之后这类问题基本消失因为它是哈希查找规则数量不敏感。但 IPVS 也不是银弹它需要额外的内核模块而且某些边界场景下排查比 iptables 更绕。不管哪种模式都绕不开一个组件后端列表。早年用的是 Endpoints 对象一个大 Service 对应几百个 Pod 时这个对象会变得很大任何一次变更都要全量推送是个明显的扩展性瓶颈。较新的版本引入了 EndpointSlice把一个 Service 的后端切成多片每片有上限变更只推变化的那一片。如果你维护的集群规模不小检查一下有没有把旧版 Endpoints 当作唯一数据源的自研组件那会成为隐性瓶颈。注意Service 本身不健康检查它只是看 Pod 的 readiness 探针结果决定要不要把这个 Pod 放进后端列表。所以“Service 后端少了一半”这类问题八成是探针配得不对而不是网络坏了。3.2 DNS解析链路与服务发现的坑集群内的服务发现基本靠 DNS。解析一个 Service 时短名比如order-svc会被搜索域补全成order-svc.当前命名空间.svc.cluster.local所以跨命名空间访问必须写全名或者至少写服务名.命名空间这是新手最常踩的坑之一。我见过有人排查了半天“网络不通”最后发现是没写命名空间DNS 一直在自己命名空间里找不到目标。解析链路本身是这样Pod 的resolv.conf指向集群 DNS 服务的地址DNS 组件接到查询后集群内部域名自己回答外部域名转发给上游。这条链路上有三个常见故障点。第一DNS 组件本身的 Pod 出问题表现是“所有域名解析都失败”这时候用 IP 直连是通的第二网络插件没有正确放通 DNS 流量表现是间歇性超时第三ndots配置导致解析走了多余的搜索路径每次外部域名解析都要多试几个后缀才成功表现是“首包特别慢”。最后这个坑我要展开说因为它非常隐蔽。默认配置下如果查询的域名里点数少于阈值解析器会先按搜索域一个个试。所以访问一个外部短域名时可能要先试xxx.当前命名空间.svc.cluster.local、再试xxx.svc.cluster.local、再试xxx.cluster.local全失败了才走真实解析。每次失败都要等一小段超时几次叠加下来首次请求能慢出好几秒。解决办法有两个方向在应用侧把域名写成带结尾点的全限定格式或者在 Pod 的 DNS 配置里调整搜索域和阈值。我在一个对冷启动敏感的服务里用过前者改完之后首包延迟从两秒多降到几十毫秒。3.3 PV、PVC与StorageClass的绑定逻辑存储这块最容易搞混的是“谁绑谁”。用一句话说清PVC 是用户提的需求我要 20G、能读写PV 是集群里已有的实际存储这块盘 20G、支持读写StorageClass 是按需生产 PV 的模板。绑定过程有个关键词叫延迟绑定。传统方式是先有 PV 再创建 PVC 去匹配这在小规模场景没问题但云环境下盘是按需创建的先建盘再建 PVC 很别扭。所以较新版本默认用延迟绑定PVC 创建时先不绑等 Pod 调度到某个节点、确定可用区之后再触发创建和绑定。这个顺序很关键因为它解决了“盘建在 A 区、Pod 调度到 B 区、挂不上”的问题。我见过有人把绑定模式改回立即绑定结果跨可用区调度时频繁挂载失败排查了很久才想到是这里。回收策略也得说清楚三种取值对应三种后果Retain 是 PV 和 PVC 解绑后数据保留需要人工处理这是生产环境的默认选择Delete 是自动删除底层存储方便但要小心误删Recycle 是简单清空已基本淘汰。生产环境我一律建议 Retain理由很实在误删 PVC 的操作太常见了Retain 至少给你一次后悔的机会。访问模式经常被误解。ReadWriteOnce 指的是“能被单个节点以读写方式挂载”不是“单个 Pod”同一节点上的多个 Pod 是可以共享的ReadOnlyMany 和 ReadWriteMany 则取决于底层存储支不支持。很多共享文件类的需求用块存储配 ReadWriteOnce 是满足不了的硬上会出现挂载冲突。我在一个需要多副本共享上传目录的模拟项目里就遇到过最后换成了支持多节点读写的文件存储类型才解决。4. 常见问题与排查技巧实录前面铺垫的原理到这里要变成能用的东西。我把自己反复遇到的几类问题整理成了一条固定的排查路径基本能覆盖八成场景。4.1 Pod起不来先看状态再看事件最后看日志我的顺序永远是三步。第一步看状态kubectl get pod里的 STATUS 列信息量极大Pending、ContainerCreating、ImagePullBackOff、CrashLoopBackOff、Running 但 Ready 为 0/1每一种都指向不同环节。第二步看事件kubectl describe pod底部的 Events 是最直接的线索拉镜像失败、挂载失败、调度失败都会写在这里而且带原因。第三步看日志只在前两步指向“容器内部问题”时才看kubectl logs加上--previous能看到上一次崩溃前的输出这个参数救过我很多次。Pending 绝大多数是调度问题资源不够、节点亲和性太严、污点没容忍、PVC 没绑上。ContainerCreating 卡住看事件基本就是镜像拉不下来或者卷挂不上。ImagePullBackOff 除了镜像名写错还要考虑私有仓库凭据没配、网络策略挡了出站。CrashLoopBackOff 是容器启动了又退重点看应用自己的日志和退出码137 一般是内存被杀1 一般是应用自身报错。Running 但 Ready 是 0/1那一定是 readiness 探针没过去查探针的路径、端口、初始延迟。4.2 一份可对照的排查速查表现象最可能的原因优先核查项Pod 长期 Pending调度失败describe 里的调度事件、节点可分配资源、污点与亲和性ContainerCreating 不结束镜像或存储问题事件里的拉镜像/挂载报错、镜像仓库连通性、PVC 绑定状态CrashLoopBackOff应用启动即退出logs --previous、退出码、启动参数、配置挂载路径Running 但 0/1 Ready就绪探针未通过探针路径与端口、初始延迟是否够、应用真实监听地址Service 访问不通后端列表为空或转发异常后端列表是否有 Ready 的 Pod、端口映射、网络策略偶发超时节流或 DNS 慢解析容器 CPU 节流指标、DNS 搜索域配置、连接池设置节点 NotReadykubelet 或运行时异常节点上的 kubelet 日志、磁盘压力、容器运行时状态用这张表的时候有个技巧先确认“是不是只有某一个 Pod 出问题”。单个 Pod 出问题大概率是配置或应用自身同节点多个 Pod 一起出问题往节点层面查全集群一起出问题往控制面或 DNS 查。这个分层判断能省掉大量无效排查。4.3 我踩过的几个坑值钱的部分都在这坑一探针配得太激进。早期我给一个启动要 40 秒的服务配了initialDelaySeconds: 10的存活探针结果每次发布都崩溃重启重启后又进入新一轮循环。这里的原理是存活探针失败会触发重启重启后应用又要重新加载永远追不上。正确做法是用启动探针把“启动期”单独保护起来等它成功了再交给存活探针。这个改动之后同类的发布抖动彻底消失。坑二滚动更新没配好把服务更成了半瘫。默认策略下更新过程中可用副本数可能掉到一个很低的水平。我在一个只有 2 副本的服务上直接滚动更新新 Pod 还没 Ready旧 Pod 已经被干掉一个剩下一个扛全量流量直接被打挂。后来我固定了两个参数最大不可用设为 0 或很小最大超出设为一个合理值再配合就绪探针保证新 Pod 真正能接流量了再替换旧的。用户的感知从“更新期间偶尔 502”变成了“完全无感”。坑三把配置写进镜像里。这个问题不算 K8S 独有但在 K8S 里放大得特别明显。配置改了要重新构建镜像、重新推送、重新拉取一整套流程走下来十几分钟。改用配置对象挂载之后改配置只需要更新对象、触发一次滚动重启而且同一份镜像能在多个环境复用。代价是挂载的配置不会自动热更新有些实现会定期同步但应用未必重新加载所以我的习惯是配置变更后主动触发一次滚动。坑四忽略了节点磁盘这个隐形杀手。有段时间集群频繁出现 Pod 被驱逐、节点变成不可调度查了半天是镜像和日志把节点磁盘吃满了。镜像其实有回收机制但阈值配得太宽松回收跟不上堆积速度。后来我把镜像回收的阈值收紧同时给日志加了轮转和总量限制问题就压住了。节点磁盘和内存一样是会被慢慢吃光的资源必须有主动治理。4.4 从原理反推几条可以直接落地的配置基线如果让我给一套新集群定基线我会按这几条来。资源方面所有业务容器必须配 requestsCPU 的 requests 按压测的 P50 定、limits 按 P99 定或留空内存的 requests 与 limits 相等系统侧预留必须配足宁可少分配一点也别让系统进程抢不到资源。探针方面启动慢的服务必须配启动探针就绪探针的路径要指向真实可用的健康检查接口而不是首页存活探针的失败阈值不要设成 1给一两次容错。发布方面最大不可用控制住配合就绪探针让“能接流量”成为替换的前置条件。存储方面回收策略统一用保留PVC 命名带上环境与用途前缀避免误删。这些基线的共同点是每一条背后都有一个具体的故障场景不是抄来的最佳实践清单。我在不同规模的模拟项目里反复验证过能在不增加多少运维成本的前提下把最常见的几类事故挡在门外。最后分享一个我自己一直在用的小方法遇到任何 K8S 问题先在心里过一遍那八步链路——对象写进去了吗、控制器生成了吗、调度绑定了吗、kubelet 拉起来了吗、状态上报了吗、后端列表加进去了吗。多数时候你会在其中某一步停住然后就知道该去翻哪个组件的日志了。这套思路比记住一百条命令管用得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑