资讯动态

K8s高可用实战:Deployment与StatefulSet配置及排障

发布时间:2026/10/3 14:25:57 来源:尧图企业网站定制
1. 先从选型说起Deployment 和 StatefulSet 的分界线K8s 系列写到第九篇前面把集群搭建、Pod、Service、网络、存储这些基础轮完一遍之后终于该碰生产环境里最让人头疼的问题了高可用到底怎么落很多人一开始会把高可用等同于“多副本 负载均衡”这个理解没错但太粗了。同样一套业务用 Deployment 能扛住流量换成 StatefulSet 可能就是为了保住数据反过来有些服务压根不适合有状态部署硬上 StatefulSet 只会给自己找麻烦。高可用首先要回答的问题不是“怎么部署”而是“这个服务能不能接受重启、能不能接受漂移、它的数据到底谁来管”。这一篇我打算把 Deployment 的高可用配置深度拆开再把 StatefulSet 从存储、网络身份到有序扩缩容完整捋一遍最后附上一堆生产环境里真正影响用户的事故案例和排查路径。适合刚把 K8s 集群跑起来、正准备把核心业务从测试环境往生产迁移的同学也适合那些已经上了生产、但三天两头被“Pod 起不来”“数据丢了”“升级就挂”折磨的运维和研发。很多细节不是你看一遍官方文档就能会的东西踩过坑和没踩过坑的区别往往就体现在这些“为什么这么配”的思路上。先说几个和本系列开篇相关的背景。这一整套文章的场景默认是你已经用 kubeadm 或者其他方式把集群装好了比如在 Rocky 上装 K8s 1.36 这种常见组合控制平面、工作节点都就绪了kubectl 也能正常访问集群。对 Docker 和 K8s 的区别还不太清楚的可以往回翻一下前几篇这里不再复述基础概念。我们直接从工作负载的高可用设计开始。1.1 高可用的核心问题你的服务真的能“缺席”吗高可用不是一个绝对状态而是一组权衡。你可以在副本数量、节点分布、滚动更新的速率、存储冗余这些维度上不断加码但每加一层都会带来成本和复杂度。真正的思路应该是先定义“不可用多久算事故”再反推需要什么样的架构。举个最简单的例子一个用户登录服务后端是纯计算逻辑登录态放在 Redis 里服务本身不保存任何数据。这种服务挂了直接重启就行重启过程中哪怕断流 30 秒影响也有限。那它就适合用 Deployment 管理配合多副本和滚动更新能做到几乎无损发布。但如果是 Redis 本身、MySQL、Elasticsearch、ZooKeeper 这类服务它们保存了业务的“现场”重启意味着数据恢复、主从重新同步、甚至可能丢失最近几秒的写入。这类服务的“缺席”是不能接受的所以需要 StatefulSet 的稳定网络标识和持久化存储来兜底。先把这个分界线划清楚后面所有配置都围绕它展开。另一个容易被忽略的点是高可用不只是“多副本”还包括“副本要分布在不同的故障域里”。如果三个 Pod 全在同一台物理机上那节点宕机就是集群级事故如果三个 Pod 分布在三个不同机架上交换机挂了一个还能剩两个。这一层调度策略我会在第四章专门展开。1.2 有状态业务怎么判断Redis、MySQL、ES、GPU 推理各有各的活法很多人在刚学 K8s 的时候会把 StatefulSet 理解成“数据库专用控制器”这其实窄了。判断一个工作负载适不适合 StatefulSet核心要看三点它是否需要稳定的网络标识、是否需要独立的持久化存储、成员之间是否有启动顺序或发现关系。第一类典型是 Redis Cluster。每个 Redis 实例都得有自己的数据目录Pod 重建后还得找回原来的磁盘实例之间要发现彼此并完成握手这就必须靠 Headless Service 加 StatefulSet。第二类是 MySQL 主从主库和一个从库从库要等主库 Ready 才能开始同步这种明确的先后顺序也只有 StatefulSet 能原生表达。第三类是 ES 集群它有 master 节点、数据节点、协调节点的角色划分节点数一变就得重新做分片分配同样依赖稳定身份。GPU 推理服务则有点特别。比如一个在线推理服务模型文件在共享存储或者每个节点上都有一份推理进程本身是无状态的但它必须调度到有 GPU 的节点上而且要保证同型号 GPU 的调度策略一致。这种服务通常用 Deployment 加资源配额就够但如果推理服务内部需要维护会话状态、或者需要模型分片协作那就得上 StatefulSet。所以别按“数据服务还是计算服务”来一刀切还是按那三条标准来判断最靠谱。2. Deployment 进阶把无状态服务的可用性“卷”到极致Deployment 在很多人眼里就是个“能滚动更新的 ReplicaSet”确实功能上就是这样但生产环境的差距全在配置细节上。我见过不少集群Deployment 用默认参数跑了一年没出事一升级就故障原因就是 maxUnavailable 默认值把可用副本数打穿了。也见过更惨的探针配错了Pod 显示 Running流量一进来就 502排了大半天才想起来就绪探针写的是“进程存在”而不是“服务可用”。这一章我按生产发布时最容易出问题的几个环节挨个拆解。每个参数背后我都会把计算逻辑和取舍说清楚你拿去可以直接照着调。2.1 滚动更新两个黄金参数maxUnavailable 和 maxSurge 怎么配滚动更新是 Deployment 默认的更新策略但默认值不一定适合高可用场景。它的行为由两个参数控制参数默认值含义maxUnavailable25%滚动更新期间允许最多几个 Pod 处于不可用状态maxSurge25%滚动更新期间允许最多超出期望副本数多少个 Pod以 3 副本为例默认配置下更新时 K8s 可以先删除 1 个 Pod25% 向上取整就是 1等新 Pod Ready 后再往下走maxSurge 则允许先额外拉起 1 个新 Pod即使总数短暂变成 4 也没问题。两个参数配合起来K8s 会在“先删旧”和“先起新”之间找一个最优节奏。高可用场景下我强烈建议用maxUnavailable: 0maxSurge: 1或者小百分比。意思很简单一个旧 Pod 都不能少要更新就先多拉起一个新的等新 Pod 通过探针确认可用再删掉一个旧 Pod。这样可以保证整个更新过程中服务始终有至少完整副本数的 Pod 在扛流量。这个配置有个前提你的副本数至少 3 个而且每个 Pod 的启动时间不能太长。如果 Pod 启动要 5 分钟那maxUnavailable: 0会导致更新过程非常慢期间新 Pod 一直堵塞旧的也删不掉。这时候就要权衡是把 maxUnavailable 调到 1 个还是接受更长的发布耗时。有些业务宁可让发布慢一点也不愿意掉可用性那就是零停机发布有些内部系统没那么敏感就可以用maxUnavailable: 1换取更快的发布速度。另外注意一点这两个参数的百分比会自动向上取整所以 2 副本的 25% 实际等于 14 副本的 25% 也是 18 副本才是 2。你别拿 4 副本配 25% 以为能容忍 1 个不可用实际也算 1但 2 副本配 50% 就直接等于 1很容易计算出最小副本数的下限。经验是核心服务就别用百分比了直接写整数。strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1这个配置写进 Deployment 之后滚动更新的节奏就会变成“先建新 Pod - 等 Ready - 删一个旧 Pod - 再建下一个新 Pod”整个过程非常稳。代价是更新期间同一时间可能有两个不同版本的 Pod 同时在服务如果你的新旧版本完全不兼容、数据库表结构对不上那就要考虑用蓝绿发布或者分批灰度而不是单纯靠这两个参数。2.2 探针与优雅终止从“看起来正常”到“真正可用”滚动更新做得再好如果 Pod 内部已经坏了而 K8s 不知道一切等于白搭。这里的“知道”靠的就是三类探针就绪探针readinessProbe、存活探针livenessProbe和启动探针startupProbe。我遇到太多人把就绪探针写成了“TCP 端口通就行”。TCP 端口能通说明进程在监听但不代表服务真的可以处理请求。更合理的做法是暴露一个独立的健康检查接口比如/health/ready在这个接口里做依赖检查——数据库连接池能不能拿到连接、缓存是否可达、必要的配置是否加载完成。只有这些全 OK探针才返回 200Pod 才被标记为 Ready才会被 Endpoints 纳入流量转发。存活探针的目的是把“活死人”杀掉重启。它不应该做太重的依赖检查否则数据库抖动 5 秒就被误杀一批 Pod反而制造雪崩。存活探针一般只检查自己这个进程“活着没”比如进程内部的一个轻量锁。启动探针则是给那些冷启动特别慢的服务准备的它会暂时接管存活探针的判断避免“还没起来就被杀”的尴尬循环。三个探针配合起来我常用的示例配置是readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 15 periodSeconds: 15再说优雅终止。K8s 删除一个 Pod 时会先向容器发送 SIGTERM然后等待一个宽限期默认 30 秒超时后就发 SIGKILL。如果我们的服务根本不处理 SIGTERM进程直接退出那正处于处理中的请求就会断掉。正确做法是在应用里监听 SIGTERM停止接收新请求、处理完存量请求再退出。可这种方式不是一天能改完的很多历史应用也改不动。这时可以用 preStop 钩子做缓冲让 Pod 在真正收到 SIGTERM 之前先“装死”一会儿给负载均衡器摘除节点留出时间lifecycle: preStop: exec: command: [/bin/sh, -c, sleep 10]terminationGracePeriodSeconds也要跟着调比如 preStop 睡了 10 秒宽限期至少得 30 秒不然 10 秒没睡完就被 SIGKILL 了。有了探针和优雅终止打底滚动更新才能真正做到“用户无感、流量无损”。2.3 版本回滚把事故现场还原成事发前状态再稳的发布也有翻车的时候。一旦新版本 CPU 飙升、日志疯狂报错、用户开始投诉最要紧的事儿不是排查根因而是先把流量切回上一个版本。K8s 的 Deployment 天然支持这个能力但要保证回滚“回得去”有几个前置条件你得提前埋好。第一个条件revisionHistoryLimit别设成 0。默认值是 10也就是保留最近 10 次修订历史放心用默认就行。如果你手贱把它改成 0回滚菜单里就永远只有“当前版本”一个选项。第二个条件发布时尽量改镜像版本而不是改latest标签。用latest意味着新老 Pod 拉到的镜像可能是一样的回滚根本分不清版本。第三个条件发布后立刻用kubectl rollout status等发布结束再观察一段时间确认稳定。如果发布过程被中断回滚的时候可能会和未完成的滚动产生奇怪的交叉。基本操作就三条命令# 查看发布历史 kubectl rollout history deployment/order-service # 回滚到上一个版本 kubectl rollout undo deployment/order-service # 回滚到指定版本 kubectl rollout undo deployment/order-service --to-revision3回滚的底层逻辑也是触发一次滚动更新所以 maxUnavailable、maxSurge、探针这些参数同样生效。这点很容易被忽略——有些人以为回滚是“瞬间恢复”实际它是“再发布一次旧版本”该走的流程一个都不会少。如果旧版本镜像本地缓存丢了回滚过程中还需要重新拉取网络一慢照样会卡住。实战里的教训是不要把回滚当成银弹。有些事故根本是配置变更引起的比如环境变量、挂载路径、资源限制这些倒是跟随 Deployment 的 spec 变化被保留了历史记录可以一起回滚但如果是数据库结构已经变更、新版本写了不兼容的数据那回滚应用代码不等于回滚数据流水数据是回不去的。能在发布前做好兼容性设计永远胜于事后补救。2.4 副本数与 PDB 的搭配让每一次升级都“无损”Deployment 的副本数还有一个双击陷阱你以为设了 3 副本就安全了但有没有想过节点维护时 Pod 被驱逐、加上滚动更新同时在跑可能同一时刻可用的小于 2整个服务就扛不住流量了。解决这个问题有两个手段一是提高副本数并合理分布二是引入 PodDisruptionBudgetPDB。PDB 解决的是“自愿中断”场景比如你执行kubectl drain给节点做维护或者通过 cluster-autoscaler 缩容节点K8s 会先检查 PDB 是否允许驱逐当前 Pod。如果驱逐会导致可用副本数低于阈值驱逐就会被拒绝直到你手动干预或者找到其他方式。apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: order-service-pdb namespace: production spec: minAvailable: 2 selector: matchLabels: app: order-service这个配置的意思很直接不管出于什么自愿原因order-service 至少得有 2 个 Pod 保持可用。配合前面maxUnavailable: 0的滚动更新策略你可以把发布和运维动作都约束在“无损”的范围内。PDB 也可以写成maxUnavailable: 1两种写法的语义其实等价但minAvailable对小数量的服务更直观。补充一点PDB 不保护非自愿中断比如节点宕机、磁盘损坏、OOM 杀进程。它管不到这些所以你不能只靠 PDB 做高可用它只是“配合人做维护”的工具。真正扛住意外故障的还是副本数、反亲和和探针这一整套体系。副本数本身也有门道。3 副本和 2 副本的差别不只是 1 个 Pod 的数量问题2 副本意味着任何一个节点故障可用副本数直接掉到 1某些极端情况下会触发资源争抢3 副本在多数故障模型下都能保住过半可用而且能满足 PDBminAvailable: 2的要求。多副本也多不了太多成本核心生产服务直接 3 起步。3. StatefulSet 全解析网络身份、存储与顺序的艺术Deployment 讲完进入 StatefulSet。它是 K8s 里最复杂的工作负载控制器没有之一。它之所以复杂是因为它把服务编排从“一堆可互换的豆子”变成了“一组有身份的成员”。每个成员不但有自己的名字、自己的磁盘还有自己明确的启动顺序和退出顺序。这套机制对数据库、中间件这类有状态服务来说是救命的但对新手来说也是最容易踩坑的地方。3.1 Headless Service为什么必须有它解决了什么问题StatefulSet 创建出的 Pod 名字是固定套路$(StatefulSet名称)-$(序号)比如redis-node-0、redis-node-1、redis-node-2。Pod 重建后名字不变这是“稳定网络身份”的第一层含义。但光有名字还不够应用之间要互相访问得有一条稳定的寻址通道这就需要 Headless Service。普通 Service 有一个 ClusterIP作为负载均衡入口请求会被随机转发到某个后端 Pod。StatefulSet 的场景恰恰不需要这种随机转发——Redis 集群里客户端要明确知道每个实例的地址和角色而不是让 Service 随机路由因为随机路由会破坏集群的节点发现。Headless Service 就是不分配 ClusterIP 的 Service创建后你直接用 Pod 名访问redis-node-0.redis-headless.namespace.svc.cluster.local。这个域名会解析到对应 Pod 的具体 IP稳定不变。这个设计还有个深层含义即使 Pod 被删了、重建了、IP 变了应用通过域名访问时仍然会解析到新的 Pod IP。有状态服务的各成员之间通过这种域名互相发现就能在节点迁移之后接上之前的拓扑关系。实践中Service 要设置clusterIP: None才叫 HeadlessapiVersion: v1 kind: Service metadata: name: redis-headless namespace: redis spec: clusterIP: None selector: app: redis ports: - port: 6379 name: redis有人会问那我可不可以直接创建 Pod然后自己维护端口和 IP 映射理论上可以但那就绕回了手工运维时代。StatefulSet 加 Headless Service 的价值在于Pod 的创建、销毁、域名注册都是自动完成的你不用再写脚本去同步 IP 变化。3.2 PVC 模板与持久化数据别丢是底线有状态服务最大的诉求是Pod 可以换数据必须留。这就引出 StatefulSet 的核心特性之一——volumeClaimTemplates。它像一个 PVC 的模版StatefulSet 每创建一个 Pod就会自动根据这个模版创建一个独立的 PVC然后绑定到 Pod 上。举个例子3 副本的 Redis 集群StatefulSet 会自动创建 3 个 PVC分别名为>volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 50Gi storageClassName: csi-rbd这里几个关键点要重点提醒。第一accessModes 用 ReadWriteOnceRWO表示一个 PV 同一时间只能被一个节点挂载这是绝大多数有状态服务的正确选择。除非你的存储插件支持 ReadWriteManyRWX否则别乱改。第二storageClassName 要选对。有的集群默认 StorageClass 是本地路径不支持跨节点Pod 漂移后数据就读不回来了。生产环境强烈建议用分布式存储比如 Ceph RBD、云厂商的云盘、或者 NFSNFS 性能弱一些但胜在简单。第三PVC 模板一旦创建删除 StatefulSet 不会自动删除 PVC这其实是保护机制防止你误删数据。需要删除数据时得手动删 PVC。还有个细节StatefulSet 更新时如果镜像和存储模板都变了PVC 一般不会重建因为数据迁移是禁区。你要是想动态调整存储大小得看存储插件是否支持扩容不支持就老老实实规划好容量别把“以后不够再加”当成常规操作。我见过太多人把 10Gi 的存储模板用到 90%然后想扩容发现插件不支持只能手动备份迁移非常痛苦。3.3 有序调度部署、扩缩容、升级的节奏感StatefulSet 的 Pod 不是一瞬间全部创建的而是按序号依次创建而且下一个 Pod 必须等到当前 PodRunning 且 Ready之后才会被创建。这个机制源自一个朴素的道理数据库这类服务第二个节点往往要等第一个节点就绪后才能做初始化、加入集群。比如 MySQL 从库要等主库 Ready 后开始同步如果没有这个顺序约束从库启动时可能根本找不到主库直接崩溃。删除 Pod 时顺序是反过来的序号最大的先删然后依次往小删。扩缩容同样遵循这个规则。缩容前你最好确认一下要摘掉的节点是否承担了特殊角色比如 Redis 的 master 节点是否转移了槽位、ES 的数据分片是否已经迁移完毕不然就是真实的生产事故。升级rollingUpdate时也有个很有用的参数叫partition它代表“从哪个序号开始更新新版本”。比如partition: 2意味着只有序号 2 的 Pod 会更新到新版本序号小于 2 的保持旧版本。这是灰度发布和 canary 的绝佳工具你想让 Redis 集群先升级一个节点验证稳定性其他节点按兵不动那就设置 partition验证通过后再逐步往 0 的方向推进。updateStrategy: type: RollingUpdate rollingUpdate: partition: 2这个参数在日常运维中的价值被严重低估了。很多团队搞大规模数据库升级靠的其实是不断改 partition 实现“一次一个节点”而不是把数据库一次性全部重启。后者一旦新版本与旧协议不兼容整个集群都会原地爆炸。3.4 实战场景跑起一个高可用的 Redis 集群把前面的理论串起来用一个 3 节点 Redis 集群的示例收尾这一章。这个示例虽然只做演示但结构可以直接抄到生产里改一改就能用。整体结构是Headless Service 负责集群节点域名发现StatefulSet 管 3 个 Redis 实例每个实例一块 PVC 持久化数据Pod 之间通过反亲和尽量打散到不同节点。简单版本可以这样写apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-node namespace: redis spec: serviceName: redis-headless replicas: 3 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: terminationGracePeriodSeconds: 30 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: [redis] topologyKey: kubernetes.io/hostname containers: - name: redis image: redis:7.2-alpine command: [redis-server] args: [--appendonly, yes, --cluster-enabled, yes] ports: - containerPort: 6379 name: redis volumeMounts: - name: data mountPath: /data volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 10Gi storageClassName: standard创建完成后你会看到三个 Pod 依次启动redis-node-0先 Ready再创建redis-node-1最后redis-node-2。用kubectl exec进任意一个 Pod执行 Redis 集群初始化命令让它通过域名发现其余节点。整个过程Pod 之间的身份是稳定的数据是持久的顺序是可控的这才能叫高可用。实际生产里你还要考虑密码认证、持久化策略、内存 maxmemory 设置、节点数规划等一堆参数这些已经超出控制器本身的范畴但掌握 StatefulSet 的基本逻辑之后往里面填充这些配置都会很顺手。4. 高可用设计的最后一公里把 Pod 安排对控制器层面的配置做得再完善如果 Pod 的物理分布一塌糊涂高可用依然是一句空话。这一章专门聊调度包括反亲和性、拓扑分布、资源隔离以及 GPU 这类特殊资源的高可用思路。4.1 反亲和与拓扑分布别把鸡蛋放一个篮子里亲和性的本质是“给调度器提供约束和偏好”。正亲和affinity表达的是“尽量/必须调度到某些节点”反亲和anti-affinity表达的是“尽量/必须避开某些 Pod”。高可用场景里反亲和用的频率远高于正亲和——我们要的就是让同一服务的副本尽量散开别互相挤在一台机器上。打法有两种第一声明式的 Pod 反亲和如上节 Redis 示例中的podAntiAffinity。它告诉调度器带有appredis这个标签的 Pod尽量别和另一个带有相同标签的 Pod 调度到同一个hostname上。topologyKey是灵魂你可以按节点hostname、按可用区zone、按机架rack来控制散开的粒度。第二用topologySpreadConstraints拓扑分布约束做更精细的“名额分配”。它能让 Pod 在不同拓扑域之间实现更均衡的分布。比如你有 3 个可用区每个区 1 台节点要部署 6 个副本它可以做到每个节点 2 个 Pod而不是某个节点 4 个、另一个节点 0 个。topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: order-service这里的maxSkew: 1表示各节点上 Pod 数量之差最多为 1DoNotSchedule表示不满足就不调度。如果你的环境节点不均匀、某台机器资源太满强制DoNotSchedule可能导致 Pod 长期 Pending。这时可以改成ScheduleAnyway放软约束或者在反亲和里用preferred而不是required给自己留点灵活性。4.2 Namespace 与资源隔离防止“邻居”互相伤害多团队共用集群是一个很常见的现状比如你在一个集群里同时跑订单、用户、日志等多个业务。这种共享模式会带来一个致命问题资源争抢。某个业务突然流量暴涨如果没做限制它会拖垮整个集群的其他业务。这也是很多生产事故的根源。解决方案就是 Namespace 加 ResourceQuota。Namespace 不只是逻辑隔离它还可以承载资源配额策略。每个业务一个 Namespace每个 Namespace 设置 CPU、内存、PVC 数量和对象数量的上限。这样单个业务再怎么失控也无法超过配额集群其他部分不会跟着遭殃。ResourceQuota 的配置例子apiVersion: v1 kind: ResourceQuota metadata: name: order-quota namespace: production spec: hard: requests.cpu: 8 requests.memory: 16Gi limits.cpu: 16 limits.memory: 32Gi persistentvolumeclaims: 10配合 LimitRange 给单个 Pod 的默认请求和限制兜底防止有人把 Pod 不声明资源就扔上来。这些措施听上去基础但绝大多数事故复盘到最后都能看到“谁都可以创建不限额的工作负载”这个小洞。多 Namespace 还有一层价值网关策略、网络策略NetworkPolicy、RBAC 权限都可以按 Namespace 做细分。生产环境建议把团队权限控制到 Namespace 级别别给全局权限否则误操作面太大。很多安全整改要求“最小权限”在 K8s 里从 Namespace 权限限制开始是最容易落地的第一步。4.3 GPU 等特殊资源调度的高可用思路GPU 调用在 K8s 里是个专门话题。标题里的热词提到了“k8s调用gpu”实际生产里常见的场景包括模型推理、训练任务、视频处理。GPU 调度有几个特点GPU 节点数量少、GPU 不能像 CPU 那样随意分片、GPU 型号驱动直接影响可用性。要在 K8s 里用 GPU先得装上设备插件比如 NVIDIA 的 device plugin。装好之后节点就有了nvidia.com/gpu这个可分配资源你才能在 Pod 里声明resources: limits: nvidia.com/gpu: 1 requests: nvidia.com/gpu: 1GPU 的高可用区别于 CPU 服务的地方在于GPU 节点通常只有几台你没法盲目堆副本数。如果只有一个 GPU 节点那你的推理服务的可用性天然受限于这个节点。所以生产级方案一般会做几件事一是用节点亲和把推理 Pod 绑定到带 GPU 的节点组二是给节点组配置 GPU 驱动和镜像的标准化防止换节点后环境不一致三是在多 GPU 节点场景用 Pod 反亲和让推理副本散开。还有一种常见组合是“模型文件放共享存储 推理实例用 Deployment”。这样副本可以随时扩缩容任何一节点崩溃新 Pod 只要调度到另一台带相同 GPU 型号的机器就行。如果推理服务需要“模型分片”或者“多卡并行”那就和其他有状态服务一样考虑 StatefulSet。判断逻辑还是回到第一章那三条标准稳定身份、持久化、成员发现。5. 常见故障排查实录从初始化到运行期的坑最后这一章我打算多花点笔墨因为很多读者问我的问题都不是“原理”层面的而是“命令跑完报错了怎么办”。这里挑几个真实的高频故障逐个拆解。5.1 初始化报错the api server is not healthy after 4m0.00747357s这个话题非常高频基本搜“k8s 安装”就能看到一堆提问用 kubeadm 初始化控制节点卡了几分钟之后报错类似the api server is not healthy after 4m0.00747357s。这个错误的字面意思是 kubeadm 在等待 apiserver 就绪但等待了约 4 分钟还没等到。为什么 apiserver 起不来我按出现频率从高到低整理排查顺序第一容器运行时和 Kubelet 的 cgroup 驱动不一致。这是最常见的原因。containerd 默认或者配置成 systemd而 kubelet 用 systemd两边一致才稳。如果你用 Docker 作为运行时也要检查 Docker 的 cgroup driver 是不是 cgroupfs跟 Kubelet 的 systemd 不匹配节点就会一直不健康。解决办法是修改 containerd 配置里的SystemdCgroup true然后重启 containerd。第二核心镜像拉不下来。apiserver 启动依赖registry.k8s.io上的一堆镜像国内网络环境下经常超时。检查方法kubeadm config images list kubectl get pods -n kube-system如果 Pod 卡在 ImagePullBackOff说明就是镜像拉取问题去配置镜像源或者手动预拉镜像。第三kubelet 本身异常。先看 kubelet 日志journalctl -u kubelet -f如果是“failed to get sandbox image”或者和 DNS 相关的错误多半是沙箱镜像有问题或者系统 DNS 配置比如 systemd-resolved 的/etc/resolv.conf干扰了集群内 DNS。把 kubelet 的--resolv-conf指到正确的配置文件能解掉一大批古怪问题。第四etcd 端口或证书不通。apiserver 要连 etcd 才能完成初始化如果 2379 端口被防火墙拦了或者证书过期也会一直是 unhealthy。检查kubectl logs -n kube-system etcd-node-name总的来说这个报错的本质是 apiserver 没能在规定时间进入健康状态不要被 4 分钟这个数字吓到耐心看 kube-system 里的 Pod 状态和日志90% 的答案都在那里。踩过坑之后我现在在初始化前都会先做一轮环境预检Kubelet 是否运行、镜像是否拉全、cgroup 配置是否一致、DNS 和防火墙是否正常。预检做到位基本能把这个错误挡在门外。5.2 生产环境常见的用户影响故障Evicted、OOM、ImagePull 失败进入运行期真正让用户感到故障的事件往往集中在几类。第一类是 Evicted驱逐。节点磁盘空间不足、内存压力过高时kubelet 会按优先级驱逐 Pod。被驱逐的 Pod 进入“Evicted”状态然后被 Deployment/StatefulSet 重新拉起。表面上看服务能恢复但频繁驱逐说明节点资源严重过载用户体验依然很差。排查 Evicted先看节点的压力指标kubectl describe node node-name关注MemoryPressure、DiskPressure、PIDPressure这几个 Condition。如果节点 DiskPressure多半是镜像占满磁盘、日志没有清理、或者 PVC 空间爆了。解决思路是设置合理的日志轮转、及时清理无用镜像、给存储卷做容量规划。Pod 级别则要确保 requests 和 limits 都写了别让节点内存被打爆。第二类是 OOMKilled。Pod 反复出现OOMKilled八成是 limits.memory 设得比实际需求低或者是 Java、Python 这类应用堆内存配置和容器 limit 不匹配。处理方式不是只调大 limit还要分析这个服务真实需要多少内存比如通过监控看到 Rss 一直在涨优先优化应用其次是调整 limits。盲改 limit 的结果可能就是节点内存压力更大回头又引发 Evicted。第三类 ImagePull 失败。常见的表现是 Pod 卡在ImagePullBackOff。原因无非几种镜像 tag 不存在、镜像仓库需要认证没配置 imagePullSecret、网络通不到仓库、或者仓库限流。排查命令kubectl describe pod pod-name | grep -A10 Events如果是因为仓库认证记得在 Namespace 里创建imagePullSecret并挂到 ServiceAccount 上省得每个 Pod 手动指定。还有一个隐蔽问题Pod 重启了但整体没异常同时用户反馈“刚才断了几秒”。这往往是因为存活探针误杀导致的。探针配置太严格一次超时就杀掉重启整个 Pod 消失再建期间流量全断。碰上这种先看kubectl get events里是不是有「Liveness probe failed」然后把阈值放宽或者换成启动探针来容错。5.3 排查思路速查事件、日志、状态三件套最后分享一个我一直在用的排查路径基本能覆盖 80% 的疑难杂症。按顺序执行别跳步。第一步看事件。无论是 Deployment 还是 Pod只要状态不对先用 describe 看 Eventskubectl describe pod pod-name kubectl describe deployment deployment-nameEvents 里会直接告诉你“为什么没调度”“为什么拉不到镜像”“为什么探针失败”这是最快的定位入口。第二步看日志。确认 Pod 在跑但行为异常时就需要进容器看日志kubectl logs -f pod-name -n namespacePod 崩溃多次的可以看上一个实例的日志kubectl logs -f pod-name -n namespace -p多个容器的情况记得加容器名参数-c container-name。第三步看状态。如果 Pod 显示Running但Ready是 0/1问题基本在就绪探针。这时回到 describe重点看 Liveness、Readiness 那一节的配置和当前状态。如果显示Pending基本都是调度或存储问题看 Events 里的FailedScheduling、FailedAttachVolume、PVC not found这几个关键字。第四步上实时事件流。如果故障是偶发的用下面的命令持续观察一段时间kubectl get events -n namespace --watch再配合 Prometheus 监控看资源水位和 QPS很多玄学问题最后都能落到一个具体的告警曲线转折点。这四步走完如果还没定位就要往底层挖查看 kubelet 日志、容器运行时日志、存储插件日志。到这一层往往已经不是 K8s 配置问题而是节点或存储后端的问题。《K8s 权威指南》这类书读起来能补原理但真到生产上最靠得住的还是这套事件、日志、状态三板斧加上你对自己的业务特征足够熟悉。这一篇从 Deployment 的高可用参数一路讲到了 StatefulSet 的存储身份再到调度和故障排查信息量确实不小。我个人这几年最大的感受是高可用不是某一个参数灵光一闪就能实现的它是一整套“预期管理”的组合拳。每次配置改动之前都先问一句“某个节点挂了会怎样、版本升级会怎样、存储坏了会怎样”多想几层生产环境会少很多深夜的告警电话。

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

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

免费获取报价 →
↑