面向 P6-P7+ 工程师 问题现场型 · 长文 约 9,000–11,000 字 信息截止 2026-08

K8s 生产稳定性配置:
配额、探针、优雅启停与 OOMKilled 规避

这不是又一份「YAML 字段速查」,而是给已经会写 Deployment、却仍被 CrashLoop、发布 502、Exit 137 轮番叫醒的工程师: 弄清 requests 与 limits 各管什么、三类探针失败分别意味着什么、SIGTERM 窗口如何与摘流对齐,以及为何「堆很空」仍会被内核杀掉。

主线风格:问题现场 + 体系架构 + 框架训练 版本假设:Kubernetes 1.28+ 证据等级:官方文档优先,标注推断与生产经验
问题现场 · 复合场景

1同一发布窗口里,为什么三盏红灯一起亮

复合场景 · 综合多起发布事故的典型叠加,非指代具体单一事件 TYPICAL SCENARIO

某团队在 Kubernetes 1.28 集群上线一套 Java 微服务。发布窗口内监控同时亮起:部分 Pod 进入 CrashLoopBackOff,Ingress 返回 502,另有 Pod 的 Last State 显示 Reason: OOMKilled、Exit Code 137。开发查应用日志,堆内存并未抛出 OutOfMemoryError;运维查 kubectl describe pod,看到 Liveness 连续失败触发重启; SRE 进一步看到节点内存压力驱逐信号,以及命名空间 ResourceQuota 即将触顶、HPA 扩不动的事件。

表面上是三类独立故障,根因却指向同一组配置缺口:资源规格与真实用量脱节探针把「慢」误判成「死」滚动更新时未给进程留出摘流与收尾时间。 T16 讲清了 Pod、Deployment、Service 是什么;本篇聚焦「上线前必须写对的稳定性参数」,并与 T10(容器化 JVM 适配)在 OOM 维度联动。

从故障成本看,这类问题具有「配置一次、长期潜伏、流量高峰或发布窗口集中爆发」的特征:平时低流量下 limit 富余、探针勉强通过、 grace 30 秒「似乎够用」;大促或密集发布时,OOM 重启、502、HPA 扩不动同时出现,排障跨开发、运维、中间件多条线,耗时长且易误判。 把稳定性参数纳入发布门禁,比事后调参便宜一个数量级。

本篇正文基于 Kubernetes 1.28+ 行为撰写;不同小版本在探针 gRPC 支持、cgroup v2 默认路径等方面可能有差异,升级集群前请对照 Release Notes 复核。 文中标注「复合场景」处均为教学用合成案例,不指向特定客户或事故。机制描述以官方文档为共识,时序窗口与告警阈值为生产经验, 数量级必须用你自己的集群复测。目标读者不是第一次听说 Probe 的人,而是已经能提交工作负载、却在评审会上说不清「改哪个旋钮」的工程师。

分享结构:第二章给出四层约束地图;第三至六章分别拆配额、探针、优雅停机、OOMKilled;第七章划清适合与不适合; 第八章给可勾选的上线前检查表与发布后验证动作;第九章收束为决策矩阵与相邻知识地图。证据等级约定:已核验事实对应文档语义; 生产经验对应值班复盘常见模式;作者解释是把机制串成决策语言的推理,允许被更严证据修正。文中不编造无来源的默认值性能数字。

常见误区

把 Exit 137 直接等同于「Java 堆 OOM」,或把 Liveness 失败直接等同于「应用逻辑坏了」。 前者混淆 cgroup 杀容器与 JVM 内部 OOM;后者把「慢启动 / 下游抖动」当成「进程该死」。

还可以把冲突翻译成三个可验证问题。第一,Pod 是否 Pending 或因 Quota 创建失败?若是,问题在准入与配额,不在探针。 第二,重启事件是 Unhealthy(探针)还是 OOMKilled?两者 Remediation 完全不同。第三,Ingress 5xx 是否与 Terminating Pod 时间戳对齐? 若对齐,优先查 PreStop 与 grace,而不是盲目加副本。三个问题答清,值班会议才会从情绪升级变成证据升级。

团队协作上常见另一种割裂:应用研发只盯业务错误码,平台组只盯控制器事件,网关组只盯入口成功率。三者不同步时, 最容易出现「对象看起来都健康,但用户已经在报障」或相反的「对象在抖动,用户无感」。因此本文强调可辩护: 任何半夜决策都要能同时解释副本合同、就绪后端、资源硬顶与业务 SLI,而不是只解释某一个面板。 分享讨论环节可准备一份脱敏的 Deployment YAML,现场对照第八章检查表找缺口,比纯讲幻灯片更易形成团队共识。

读者对象不是「第一次听说 Probe」的新手,而是已经能提交工作负载、却在故障归因上反复踩层的工程师。 若你仍需要逐字段解释 apiVersion,建议先完成 T16 再读本文;若你已经写过滚动发布却解释不清 「为何 502 与 137 同时出现」,本文就是为你准备的。目标产出是可在评审会上辩护的判断,而不是又一份字段速查。

体系总览 · 概念地图

2四层约束如何叠加:调度、健康、生命周期、cgroup

生产稳定性不是单个 YAML 字段,而是调度保障、健康语义、生命周期钩子、内核 cgroup 边界四层机制叠加后的结果。 理解层级关系,才能判断改哪个旋钮、改多大。命名空间的 ResourceQuota / LimitRange 约束「团队总共能用多少」; Pod 的 requests / limits 驱动调度与硬顶;探针决定「何时接流量、何时重启」;PreStop 与 grace period 决定「如何体面离开」。

声明式带来的心智转变是:你描述资源与健康合同,控制器与 kubelet 按合同行事,而不是依赖「节点上大概够用」的运行时侥幸。 若团队仍用登录节点调进程的方式救火,会不断与调度器、Endpoint 控制器、cgroup 打架——你删掉的 Pod 会被补回,你手工抬的 limit 会在下一次 GitOps 同步时被改回去。

图 1 · K8s 生产稳定性四层约束叠加
自制示意图
命名空间治理层 ResourceQuota 准入期总量硬顶 LimitRange 默认值 / max / min 工作负载层 requests / limits QoS 与规格合同 Liveness / Readiness / Startup 重启 vs 摘流 vs 启动窗 PreStop + grace 摘流与 SIGTERM 窗口 运行时兑现 kube-scheduler 按 requests 选节点 EndpointSlice 按 readiness 摘流 cgroup memory.max 超 limit 则 OOM Kill 137 改旋钮前先定位:准入失败 / 调度 Pending / 探针误杀 / 终止截断 / cgroup 杀进程
关键点:Quota 约束规格总量,规格驱动调度与 cgroup,探针驱动流量与重启,优雅停机衔接摘流与 SIGTERM。值班时先分清红灯落在哪一层。
来源:Kubernetes · Resource Management / Configure Probes / Pod Lifecycle(manage-resources · probes · pod-lifecycle)。版本假设 1.28+。

各层职责与失败窗口

层级核心对象答什么问题典型失败表现
命名空间ResourceQuota、LimitRange团队/环境总共能用多少 CPU、内存、Pod 数创建失败、HPA 无法扩容、隐性「无 limits」Pod 挤占节点
工作负载resources、probesQoS 等级、何时接流量、何时重启调度 Pending、探针误杀、就绪前接流量
生命周期PreStop、grace period、PDBSIGTERM 之后还能跑多久、如何主动摘流滚动发布 502、消息重复消费、连接泄漏
内核 cgroupmemory limit、OOM score进程 RSS 超过 limit 时谁被 killOOMKilled 137、节点 MemoryPressure 驱逐

组织协同:谁负责什么

稳定性配置横跨开发、平台与 SRE。建议分工:开发提供压测数据与探针端点,填写 resources / probes,Java 服务同步完成 T10 参数; 平台/SRE 维护命名空间 Quota、LimitRange、PDB 基线,并在 CI 接入静态规则;发布评审每次过检查表;Quota 余量与无 limits Pod 清单建议每月巡检, 探针与 grace 配置漂移建议每季度与 Git 仓库对账。核心原则是:能声明的尽量声明在 YAML 里。 让 scheduler、kubelet、Endpoint 控制器按声明行事,而不是依赖「节点上大概够用」的运行时侥幸。

从组织语言看,很多事故复盘写成「K8s 不稳定」,至少要拆成五类:准入失败(Quota)、调度失败(Pending)、 实例崩溃(CrashLoop / OOMKilled)、发现断裂(无 Endpoints / 探针误杀)、容量合同被挖空(滚动或驱逐过猛)。 前几类对应不同对象与不同修复动作;最后一类往往要同时看 maxUnavailable、PDB 与节点维护窗口。 把复盘标题写精确,是架构判断力的一部分。以下四章按「问题现场 → 机制解释 → 配置框架」展开, 比例约为问题现场 40%、体系架构 40%、可复用框架 20%,与 execution-plan 中 T17 路由一致。

也可以把四层约束看成一张「合同表」:命名空间合同回答「租户边界」;Pod 规格合同回答「调度与硬顶」; 探针合同回答「何时算活、何时算可服务」;生命周期合同回答「如何离开而不伤人」。任何线上变更只要改动其中一纸合同, 就应触发对应的验证:改 Quota 看 Pending 与 HPA;改 limits 看 OOM 与 throttling;改探针看重启与摘流;改 grace 看 5xx 与消息重复。 用合同视角写变更说明,比写「优化了一下 YAML」更能让评审者抓住风险面。

已核验事实

在 Kubernetes 中,容器 resources.requests 是调度器放置决策的主要依据之一; resources.limits.memory 映射为 cgroup 内存上限,超出时由内核 OOM Killer 终止容器进程(常见 Exit 137)。 二者回答不同问题,不可互相替代。

核心机制 · 资源配额

3Requests、Limits 与命名空间配额:规格合同如何写

复合场景 A:订单服务 memory.limit 设为 512Mi,JVM 堆按容器可见内存约 75% 自动设为约 384Mi,再叠加元空间、线程栈、DirectBuffer 与 glibc 缓存, RSS 峰值超过 512Mi。Pod 被 cgroup OOM Killer 终止,应用日志没有 Java OOM——这是 T10 讨论的「内核杀容器 vs JVM 内部 OOM」分界。

复合场景 B:同一命名空间内部分 Deployment 未写 requests,scheduler 视为 0 需求塞进繁忙节点;HPA 要扩容时, ResourceQuota 的 limits.memory 已耗尽,新 Pod 长期 Pending,发布卡在「旧 Pod 已删、新 Pod 起不来」。 复合场景 I:数据平台在共享命名空间部署 Flink TaskManager,未单独调整 Quota,批高峰拒绝在线服务扩容——根因是命名空间配额未按工作负载类型拆分。

图 2 · memory requests 调度路径与 limits cgroup 硬顶
自制示意图
requests.memory 调度与驱逐优先级 limits.memory cgroup 硬顶 kube-scheduler requests 总和 ≤ allocatable cgroup v2 memory.max RSS 超限 → OOM Kill 137 QoS 等级 Guaranteed / Burstable / BestEffort 节点 MemoryPressure 时:BestEffort 先退 → Burstable 超量优先 → Guaranteed 通常最后 只设 limits 不设 requests:按 0 调度,HPA 利用率分母失真
关键点:requests 决定「放得下」与驱逐优先级;limits 决定「跑得过」的硬顶。把二者当成同一个旋钮,是 OOM 与 Pending 同时出现的常见根因。
来源:Kubernetes · Resource Management for Pods and Containers / Resource Quotas(manage-resources · resource-quotas)。

机制:三件独立事情

  • requests:scheduler 调度依据;节点 allocatable 减去已分配 requests 决定能否放置。requests 不参与 cgroup 硬限制。
  • limits:cgroup 上限。CPU limit 通过 CFS 配额 throttling;memory limit 超出时触发容器级 OOM Kill(Exit 137)。
  • QoS 等级:requests 与 limits 全设且相等为 Guaranteed;只设 requests 或二者不等为 Burstable;都不设为 BestEffort——节点紧张时 BestEffort 最先被驱逐。

ResourceQuota 与 LimitRange

ResourceQuota 在 API Server 准入阶段生效:创建 Pod 时若累计 requests/limits 将超过 hard 配额,请求被拒绝。 这与运行时 OOM 无关,却同样表现为「服务扩不出来」。LimitRange 对未填写的容器注入 default,或在超出 max/min 时拒绝。 组合顺序应是:先定 Quota 总量,再定 LimitRange 默认,最后才是单服务自定义 resources。

apiVersion: v1
kind: ResourceQuota
metadata:
  name: prod-order-quota
  namespace: prod-order
spec:
  hard:
    requests.cpu: "40"
    requests.memory: 80Gi
    limits.cpu: "80"
    limits.memory: 120Gi
    pods: "200"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: prod-order
spec:
  limits:
  - type: Container
    default:
      cpu: "500m"
      memory: 512Mi
    defaultRequest:
      cpu: "100m"
      memory: 256Mi
    max:
      memory: 4Gi
    min:
      memory: 64Mi

规格估算四步法

  1. 压测基线:在接近生产的镜像与运行时参数下测 P95 CPU 与 RSS 峰值;压测环境 cgroup 限制应与生产 limits 一致。
  2. requests:取 P95 × 1.1~1.2(作者经验区间,需复测),保证常态调度不过度拥挤。
  3. limits:取峰值 × 1.2~1.5,且必须大于堆 + 元空间 + 堆外 + 本地缓存(Java 见 T10)。
  4. Quota 反推:limits 总和 × 最大副本数 + 缓冲 < ResourceQuota;留 HPA 余量。接近上限时优先扩 Quota 或拆命名空间,而非压低单 Pod limit 赌运气。

四步法落地时有两个常见偏差。其一是「用开发笔记本的空闲内存」估规格:本地没有生产级并发与 sidecar,得出的 RSS 往往偏乐观。 其二是「按平均值设 requests、按峰值设 limits」却忘记 HPA 与 Quota:平均值偏低会让利用率虚高或虚低(取决于指标定义), 峰值偏高会迅速吃光命名空间额度。正确做法是把压测环境的 cgroup 限制、镜像、JVM 参数与生产对齐,再读 Prometheus 的 working set, 而不是读 JVM 堆使用率曲线直接当 limit。规格评审会上应同时展示:P95/P99 RSS、堆与堆外分解、目标副本上下限、Quota 余量。

生产经验

只设 limits 不设 requests:调度器按 0 调度,监控利用率分母失真,HPA 行为异常。 requests 与 limits 差距过大:Burstable 在 MemoryPressure 下优先被驱逐。 Sidecar(Istio、日志 agent)未单独设 resources:主容器「看起来」有富余,整 Pod 总和仍 OOM。 CPU limit 过紧会造成 CFS throttling,P99 飙升却显示「CPU 利用率 100%」——那是人为封顶,不是「算力刚好用满」。

当节点触发 DiskPressure、MemoryPressure 或 PIDPressure 时,kubelet 按 QoS 与用量驱逐:BestEffort 最先,Burstable 超量优先,Guaranteed 通常最后。 在线核心链路若频繁被驱逐,应评估是否将 requests 与 limits 设为相等以获得 Guaranteed,代价是节点利用率下降——这是 SLA 与成本账户的典型 trade-off。 HPA(autoscaling/v2)若 requests 过低,利用率长期偏低导致扩容滞后;若 Quota 已满则扩容失败但 Deployment 仍认为「副本未达标」,与场景 B 叠加会放大发布风险。

LimitRange 典型策略对比

策略做法优点风险
强制默认default + defaultRequest 均设新服务不会裸奔默认值不适配所有 workload,可能误杀或浪费
仅 max 封顶max.memory 限制单容器上限防止单 Pod 吃满节点不设 min 时仍可能 requests=0
生产禁 BestEffortAdmission 拒绝无 limits 的 Pod驱逐顺序可控需豁免系统组件命名空间

复合场景 H:某 API 服务 limits.cpu: "200m"requests.cpu: "50m",常态仅占用 0.05 核, 大促瞬时打满 0.2 核后被 CFS throttling,P99 延迟从数十毫秒飙到数百毫秒。监控 CPU 利用率按 limit 计算只有 100%, 看似「没超限」,实际是人为封顶。对此类延迟敏感服务,requests 应接近常态 P95,limit 留出 burst 空间, 或评估是否使用 Guaranteed QoS 的等值 request/limit。多副本服务 requests 总和还决定「最少需要多少节点」, 与集群 autoscaler 节点池容量联动——规格写小了,调度器以为能塞,节点池却按真实用量被打满。

核心机制 · 健康探针

4Startup / Liveness / Readiness:失败分别意味着什么

复合场景 C:支付回调服务冷启动需约 90 秒加载证书与连接池。Deployment 仅配置 liveness, initialDelaySeconds: 10periodSeconds: 5。启动第 15 秒起探针失败,kubelet 连续杀容器,Pod 永远进不了 Ready,Ingress 502。

复合场景 D:liveness 与 readiness 共用含慢 SQL 的 /health。数据库短暂抖动时 readiness 摘流(正确), 但 liveness 也失败导致全量重启(过度)。 复合场景 J:Spring Boot 已分离 liveness/readiness,但 Ingress 仍打聚合 /actuator/health,磁盘空间不足导致整站被标记不健康——平台各层健康检查语义必须对齐。

图 3 · 三类探针语义与失败后果
自制示意图
Startup Probe 启动窗口内阻塞 liveness / readiness 失败 → 杀容器重启 Liveness Probe 只判「进程是否该死」 轻量、不依赖下游 失败 → kubelet 重启容器 Readiness Probe 判「能否接新流量」 可检查依赖就绪 失败 → 移出 Endpoints 时序演算:failureThreshold × periodSeconds ≈ 连续失败多久才动作 例:period=10s、failureThreshold=3 → 约 20~30s 连续失败才杀(非单次超时即杀) GC STW 15s 通常不误杀;STW 超过窗口且无缓冲仍可能误杀 → 联动 JVM 停顿治理
关键点:Startup 护启动窗,Liveness 管「只有重启能恢复」的死锁,Readiness 管背压与摘流。三者共用重依赖检查,是重启风暴的经典配方。
来源:Kubernetes · Configure Liveness, Readiness and Startup Probes(configure-liveness-readiness-startup-probes)。
探针失败后果典型用途不应承担
Startup启动完成前阻塞其它探针判定慢启动(大缓存、JIT、证书)长期运行期健康检查
Livenesskubelet 杀容器并重启死锁、无限阻塞依赖外部系统的短暂不可用
Readiness从 Service Endpoints 移除依赖未就绪、过载背压触发容器重启

推荐组合

spec:
  containers:
  - name: app
    ports:
    - containerPort: 8080
    startupProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      failureThreshold: 30
      periodSeconds: 10
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      periodSeconds: 10
      timeoutSeconds: 3
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 2

决策框架与探针类型

  1. 启动时间 > 30s → 必须加 startupProbe(或极大调高 liveness 的 initialDelay,但不如 startup 清晰)。
  2. liveness 应轻量、只依赖进程自身;readiness 可检查 DB、缓存、下游。
  3. timeoutSeconds 小于 periodSeconds;failureThreshold × period 为动作窗口。
  4. Exec 探针慎用:shell 开销与僵尸进程风险;优先 HTTP/gRPC。tcpSocket 仅验证端口监听,不宜单独作 readiness。
常见陷阱

liveness 检查下游 DB:DB 故障 → 全 Pod 重启 → 重启风暴。readiness 过严(successThreshold 过大): 新 Pod 迟迟不进 Endpoints,旧 Pod 已缩容导致流量空洞。探针经 Service 而非本容器端口:偶发绕路增加失败率,应探 127.0.0.1:containerPort

kubelet 在 1.28+ 仍按 Pod spec 独立执行探针;Readiness 变化经 EndpointSlice 控制器传播到 Service,通常数秒内生效 (取决于 sync 周期与 kube-proxy/ipvs 更新)。Headless Service 或 gRPC 客户端长连接场景下,连接池可能滞后于 Endpoints 变更, 联调阶段应验证 Pod NotReady 后是否仍有请求命中。Readiness 的 successThreshold 默认 1;若设为 3 适合「就绪后还需预热」, 但会拉长 Deployment progressDeadlineSeconds 窗口。

HTTP、gRPC 与 Exec 选型

类型适用注意点
httpGetSpring Actuator、/healthzport 为容器端口非 Service port;HTTPS 需配置 scheme
grpcgRPC 原生服务(1.24+ 起稳定)需实现标准 Health Checking Protocol
tcpSocket仅验证端口监听端口开不代表业务就绪,不宜单独作 readiness
exec无 HTTP 端口的遗留进程避免复杂 shell;注意 cgroup 下 exec 开销

探针时序与 JVM 停顿是耦合的:若 GC STW 暂停约 15 秒导致单次超时,在 failureThreshold=3、period=10s 的组合下通常不会误杀; 若 STW 超过约 30 秒且探针无独立 timeout 缓冲,仍可能触发重启——这又回到 T08/T09 的 JVM 停顿治理。 说明 K8s 稳定性配置无法单独「配完美」,必须与运行时内部行为一起看。平台侧 Ingress 健康检查若与 K8s 探针路径不一致, 值班会同时看到「Pod Ready」与「入口不健康」两套结论,优先对齐语义再谈加副本。

再强调一次失败阈值的可读性:把 failureThresholdperiodSecondstimeoutSeconds 换算成「最长忍受不可用秒数」,写进服务 README 或 Helm values 注释。这样值班同学看到 CrashLoop 时,能立刻判断是启动窗不够, 还是运行期真死锁。很多团队复制粘贴同一套探针到所有服务,导致批处理型接口与轻量 API 共用过严或过松的阈值—— 统一模板可以有,但必须按启动 P99 与依赖深度做分级,而不是全公司一个数字。

核心机制 · 优雅启停

5PreStop、grace period 与流量切走:如何不截断在途请求

复合场景 E:网关滚动更新,terminationGracePeriodSeconds 默认 30 秒,应用 drain 长连接需约 45 秒。 30 秒后 kubelet 发 SIGKILL,客户端看到 502;Kafka 消费者未 revoking 即被强杀,再平衡后重复消费。 复合场景 F:配置了 PreStop sleep 5,但未配合 readiness 失败或注册中心摘流:5 秒内 Service 仍把新请求打到即将删除的 Pod。

图 4 · Pod 终止时序:PreStop、摘流与 SIGTERM/SIGKILL
自制示意图
API Server deletionTimestamp Kubelet 执行 PreStop EndpointSlice 并行摘除后端 App 进程 shutdown drain SIGTERM(PreStop 完成后) terminationGracePeriodSeconds 倒计时(默认 30s,Pod 内容器共享) 应 ≥ PreStop 耗时 + 应用 shutdown + 缓冲;超时 → SIGKILL PDB minAvailable 保护并发不可用下限 maxUnavailable: 0 滚动不挖空 Ready 容量 超时仍存活 SIGKILL 截断在途请求
关键点:PreStop 与 Endpoint 摘流并行推进;grace 是 SIGTERM 到 SIGKILL 的上限,不是「建议等待时间」。sleep alone 无法替代应用层 cooperative shutdown。
来源:Kubernetes · Pod Lifecycle Termination / Pod Disruption Budgets(pod-termination · configure-pdb)。

终止时序(机制共识)

  1. 若存在 PDB,需满足 minAvailable / maxUnavailable 合同后才允许进一步驱逐或滚动。
  2. API Server 标记删除;若存在 PreStop Hook,kubelet 先执行 PreStop,再向容器发 SIGTERM。
  3. 并行:EndpointSlice 因 deletionTimestamp 或 readiness 失败将 Pod 从后端移除(存在传播延迟)。
  4. 等待 terminationGracePeriodSeconds(默认 30s),超时 SIGKILL。
spec:
  terminationGracePeriodSeconds: 60
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5 && curl -sf -X POST http://127.0.0.1:8080/actuator/shutdown || true"]
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080

与 Ingress / Service / 注册中心的配合

  • PreStop sleep:常见 5~15 秒,为 Endpoint 与 kube-proxy 更新争取窗口;时长应小于 grace,留出应用 shutdown。
  • grace period ≥ PreStop + 应用 shutdown 超时 + 缓冲(消息队列 commit、连接 drain)。
  • 注册中心:Nacos/Eureka 场景在 PreStop 中主动 deregister,避免与 K8s Endpoints 双轨不一致(见 T11)。
  • PDB + maxUnavailable:核心链路建议 maxUnavailable: 0 配合足够副本与 PDB,避免「全杀再全起」。
生产经验

即使 Endpoints 已移除 Pod,Ingress Controller 仍有 upstream 健康检查周期与连接池 idle 超时,可能残留数百毫秒到数秒「幽灵流量」。 sleep 秒数应在预发用 access log 与 Pod 终止事件对齐实测,不宜照搬固定值。 PreStop 调用外部 API 必须设超时,避免 Hook 卡死占满 grace。多容器 Pod 中 sidecar PreStop 过重会挤占主容器收尾时间。

对消息消费者,除 K8s 层 grace 外,还应在应用层实现 cooperative rebalance:PreStop 中 wakeup / graceful shutdown,确保 offset commit 后再退出。 仅拉长 grace 无法阻止重复消费。grace 过长会拖慢节点 drain 与维护;过短导致请求丢失——按「P99 请求处理时间 + 下游超时」估算,并在预发对比正常滚动与强制删除。

Deployment 滚动策略与多容器注意点

优雅停机不仅写在 Pod spec,还受 Deployment strategy.rollingUpdate 约束。 maxUnavailable: 0maxSurge: 1 可保证滚动过程中始终有足够 Ready 副本接流量,但发布更慢; maxUnavailable: 25% 则允许旧 Pod 先缩,若新 Pod 启动慢于旧 Pod 销毁速度,仍可能出现短暂容量缺口。 稳定性优先的在线服务建议 maxUnavailable: 0 配合足够副本数与 PDB。

grace period 定义在 Pod spec,对该 Pod 内所有容器共享同一倒计时。若 sidecar 与主容器并存,PreStop 串行执行可能占满 grace—— 1.28+ 可考虑 sidecar 的独立生命周期管理(SidecarContainers 特性在较新版本逐步落地,启用前请查集群 feature gate)。 多容器 Pod 应确保 sidecar PreStop 足够轻量,或为主业务容器单独评估 grace 是否需上调至 90~120 秒。 使用 Nacos/Eureka 时,PreStop 中主动 deregister 的超时必须小于 grace 留白,否则会出现「注册中心已摘、进程已被 SIGKILL」或相反的双轨不一致。

预发验证优雅停机时,建议刻意制造三种对照:正常滚动、缩短 grace 的强制截断、以及关闭 PreStop 的「裸 SIGTERM」。 用网关 access log 的时间戳对齐 Pod 进入 Terminating 的事件时间,统计截断请求占比与 5xx 尖刺宽度。 只有拿到这组数字,grace 与 sleep 才从「经验拍脑袋」变成「可辩护参数」。对长连接网关与消息消费者,这组实验几乎是上线门禁的一部分。

核心机制 · OOMKilled

6规避 OOMKilled:RSS 预算、节点驱逐与 JVM 联动

复合场景 G(与 T10 联动):Java 17 容器误用固定 -Xmx 大于 limit,或堆外、元空间、CodeCache、DirectByteBuffer 与 native 内存不受堆上限约束。 监控显示堆使用率约 60%,Pod 仍 OOMKilled——杀的是 cgroup 内 RSS 总量,不是 JVM Heap。

图 5 · 容器内存预算与两类「内存死亡」路径
自制示意图
limits.memory(cgroup 硬顶) Heap -Xmx / MaxRAM% Metaspace MaxMetaspace Thread stacks 线程数 × 栈 Direct / Native Netty / NIO / JNI 路径 A:容器 OOMKilled Last State: OOMKilled · Exit 137 调 limit / 堆外预算 / T10 参数 路径 B:节点 Evicted Reason: Evicted / MemoryPressure 调 requests / QoS / 扩节点
关键点:堆空不等于安全。limit 必须覆盖 Heap + Metaspace + 栈 + Direct/Native。OOMKilled 与 Evicted 是两条不同 Remediation 路径。
内存区域Java 典型是否受 -Xmx 约束是否计入 cgroup RSS
Heap-Xmx / MaxRAMPercentage
Metaspace-XX:MaxMetaspaceSize部分
Thread stacks线程数 × 默认栈
Direct / MappedNetty、NIO、JNI
OS page cache(容器内)文件 IO视 cgroup 统计口径
作者解释(经验公式,非基准测试)

limits.memory ≥ 堆上限 × 1.3~1.5 + 元空间上限 + 预估堆外。requests.memory 可取 limits 的 60%~80% 或 P95 RSS。 Guaranteed(requests = limits)利于避免驱逐,但弹性变小,需压测。Native 占比高时建议压测开 NMT,将堆外峰值纳入预算。

resources:
  requests:
    memory: 1536Mi
    cpu: "500m"
  limits:
    memory: 2Gi
    cpu: "2"
env:
- name: JAVA_TOOL_OPTIONS
  value: >-
    -XX:MaxRAMPercentage=75.0
    -XX:MaxMetaspaceSize=256m
    -XX:+ExitOnOutOfMemoryError

排查框架与告警

# 确认 OOMKilled 与上次退出码
kubectl describe pod <pod> -n <ns> | grep -A5 "Last State"

# cgroup 内存统计(需节点权限或 debug 容器)
kubectl exec -it <pod> -- cat /sys/fs/cgroup/memory.current 2>/dev/null || \
kubectl exec -it <pod> -- cat /sys/fs/cgroup/memory.max

kubectl top pod -n <ns>
指标 / 事件含义建议阈值思路(生产经验)
container_memory_working_set_bytes / limit工作集占 limit持续 >0.85 预警,>0.95 紧急
kube_pod_container_status_restarts_total重启累计15 分钟增量 >3 关联探针/OOM
kube_pod_status_reason{reason="Evicted"}节点驱逐任意 Evicted 开单查压力与 QoS
Events Unhealthy探针失败与发布窗口关联,区分 liveness/readiness

T10 深入 JVM 容器感知参数与 MaxRAMPercentage;本篇从 K8s 视角要求 limits 必须覆盖「JVM 总 RSS 预算」。 上线评审应同时过两张检查表。若「堆 OOM 日志」与「OOMKilled 137」交替,优先核对 limit 是否小于 JVM 自动推算堆上限,再查泄漏。 VPA 在生产常见做法是 updateMode: Off 只出推荐值,由人工写入 YAML,避免与 HPA 叠加导致未知重建。 非 JVM 工作负载(Go/Node/Python)共通原则仍是:压测峰值 RSS 加约 20% 余量写 limit,requests 取常态 P95。

节点级 MemoryPressure 与容器 OOM 的区别

容器 limit 触发的 OOMKilled 发生在 cgroup 层级,只杀该容器(或同 Pod 内 OOM score 最高的容器)。 节点整体内存不足时,kubelet 按驱逐协议杀 Pod,事件为 Evicted,reason 可能是 MemoryPressure。 排查时需区分:describe podLast State: OOMKilled 多为 limit 问题; Status: Failed, Reason: Evicted 多为节点或 QoS 问题。二者 Remediation 不同——前者调 limit/堆参数,后者调 requests、QoS 或扩容节点。

Go 服务需关注返回操作系统内存的延迟与 GOGC 行为,RSS 在 limit 附近可能偏高;Node.js 需关注 V8 heap 与 native addon; Python 关注多 worker 进程数 × 单进程 RSS。无头浏览器、PDF 渲染等 burst 型任务考虑临时 Job 而非长期 Deployment, 避免与在线服务共享 Quota。告警应携带 namespace、pod、container 标签,并链接到对应 Deployment 与最近 Git 变更, 缩短从「重启计数增加」到「定位哪次改 limit」的路径。

生产经验

与 T10 的分工:T10 深入 JVM 容器感知参数、cgroup v1/v2 差异与 MaxRAMPercentage 调优; 本篇从 K8s 视角要求 limits 必须覆盖「JVM 总 RSS 预算」。上线评审时应同时过两张表。 若出现「堆 OOM 日志」与「OOMKilled 137」交替,优先核对 limit 是否小于 JVM 自动推算堆上限,再查泄漏。

适合 · 不适合

7这套稳定性配置适合什么,不适合什么

本篇框架假设:工作负载由 Deployment(或同类控制器)管理、通过 Service / Ingress 对外、可接受声明式收敛延迟。 若你的场景落在下列「不适合」侧,应先换模型再谈参数微调。

适合继续用本框架

  • 无状态或弱状态 HTTP/gRPC 微服务,副本 ≥ 2
  • 可定义清晰的 liveness(进程死)与 readiness(可接流量)边界
  • 能做接近生产的压测,拿到 P95 CPU / RSS
  • 命名空间已有或可引入 ResourceQuota / LimitRange
  • 发布允许滚动窗口,可配 PDB 与 maxUnavailable

不适合或需换模型

  • 单副本有状态实例却当无状态 Deployment 滚动(应 StatefulSet + 专用停机剧本)
  • 批处理 Job/CronJob 用在线探针语义硬套(应关注 activeDeadline / backoff)
  • 把 liveness 当「业务健康大盘」(应拆 SLI 监控,勿靠杀容器)
  • 无压测数据却写死极小 limit「先上线再说」
  • 共享命名空间混部批流且拒绝拆 Quota(先治理租户边界)
作者解释

「配齐探针 + limits」不是稳定性的充分条件,只是必要合同。若副本合同被 maxUnavailable 挖空、或注册中心与 Endpoints 双轨不一致, 参数再漂亮也会在发布窗口翻车。适合/不适合的判断,本质是问:你的失败域是否与 K8s 声明式模型对齐。

另有一类边界需要单独点明:把本篇检查表当成「上线一次性仪式」而不做季度漂移对账,配置会在多次紧急热修中被悄悄改坏—— 某人临时去掉 startupProbe「先让发布过」,某人把 grace 改回默认 30「为了加速 drain」,几个月后大促窗口集中爆发。 治理要形成事前门禁、事中告警、事后复盘的闭环;本篇第八章给出事前与事中动作,第九章决策矩阵帮助在继续调参与换模型之间做选择。

若你的集群大量使用 Service Mesh,sidecar 的资源与停机钩子必须纳入同一套预算,否则会出现「主容器健康、数据面代理先被 OOM 或先被 SIGKILL」的假象。 若你使用 Argo Rollouts / Flagger 做金丝雀,分析模板中应纳入 restart 与 5xx,避免仅看「副本数达标」即宣告发布成功。

生产实践 · SOP

8反例对照、上线前检查表与发布后验证

验证层强调「反例 → 后果 → 改法」。自动化拦底线,评审表管业务语义(例如 limit 是否匹配 T10 堆比例)。

维度反例后果推荐
内存limit 512Mi,-Xmx512m无堆外余量,OOMKilled 137MaxRAMPercentage 75%,limit ≥ 约 1.4× 堆目标
CPU仅 limit 4 核,无 request调度拥挤,throttling 误判为慢request 按 P95,limit 为 burst 上限
探针仅 liveness,10s 后开始慢启动 CrashLoopBackOffstartupProbe 覆盖启动窗口
探针liveness 查 DBDB 抖动全量重启DB 检查仅放 readiness
停机默认 grace 30s,无 PreStop滚动 502PreStop sleep + grace ≥ shutdown
配额无 ResourceQuota单租户占满节点命名空间 quota + LimitRange
# 反例:切勿在生产直接使用
spec:
  containers:
  - name: bad
    resources:
      limits:
        memory: 512Mi
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 5
    # 无 requests、无 startupProbe、无 preStop、默认 grace 30s

CI 门禁与发布后 30 分钟验证

Merge Request 阶段建议接入 Polaris / kube-score(missing probes、resources 未设置)、OPA Gatekeeper / Kyverno (生产命名空间必须设 memory limit 等)、Helm values schema(约束最小值)。静态规则拦「明显空白」,压测仍不可省。

若团队暂时没有集群准入控制器,至少在应用仓库的 CI 中校验:每个容器是否声明 memory requests/limits、 是否同时存在 readiness 与(慢启动场景下的)startup、是否显式设置 terminationGracePeriodSeconds。 这类校验可以用简单脚本或 kubeconform + 自定义规则完成,不必一次上齐整套策略引擎。 关键是先拦住「空白配置进生产」,再逐步把业务语义检查表自动化。发布后三十分钟验证不是形式主义: 许多探针误杀与 OOM 尖刺恰好出现在流量切到新版本后的前几个请求突发里,错过这个窗口就只剩用户投诉。

  1. kubectl rollout status 是否在 deadline 内完成。
  2. 新 Pod RESTARTS 是否递增,describe 有无探针失败事件。
  3. Ingress/网关 5xx 与 P99 相对基线是否尖刺。
  4. kubectl top / Prometheus 的 working_set/limit 是否逼近 0.9。

上线前检查表

面向架构治理落地,可在发布评审与季度巡检中直接使用。第 1~8 项建议为阻塞项;Java 服务须连同 T10 一并签署。

检查项标准责任频率
requests.cpu / memory基于压测 P95,非 0;与 HPA 指标一致开发 / SRE每次发布
limits.memory 覆盖 RSS 峰值Java:配合 T10;limit ≥ 堆×1.3+元空间+堆外开发每次发布
命名空间 ResourceQuotalimits 总量 ≥ 最大副本×单 Pod limit×1.2平台 / SRE季度
LimitRange 默认值新 Pod 不能无 limits 创建(若策略要求)平台季度
startupProbe(慢启动)启动 >30s 必须配置;窗口 ≥ 启动 P99开发每次发布
liveness 轻量独立不依赖 DB/下游;与 readiness 路径分离开发每次发布
readiness 反映可接流量依赖就绪才 200;滚动验证无 502 尖刺开发 / QA每次发布
PreStop + gracegrace ≥ PreStop + shutdown;含 sleep 或主动摘流开发每次发布
PDB minAvailable多副本服务设置,避免并发不可用SRE每次发布
OOM 与重启告警Exit 137、restart、working_set/limit >0.85SRE持续
JVM 容器参数(Java)按 T10:MaxRAMPercentage、Metaspace、无超限 -Xmx开发每次发布
Sidecar resourcesIstio/日志/agent 独立 requests/limits开发 / 平台每次发布
滚动 maxUnavailable核心链路建议 0;验证无容量空洞SRE每次发布
预发与生产 spec 一致limits/probes/grace 与生产同量级,非缩小版假环境QA / 开发每次大版本

上表可复制为可勾选表格;第 1~8 项未通过不建议进生产。第 9~14 项可按服务类型裁剪,但须记录豁免理由。

检查表与静态扫描的分工再写清楚一次:kube-score / Polaris / Kyverno 擅长发现「字段缺失」与「明显违规」; 检查表擅长发现「字段在但语义错」——例如 liveness 路径指向含 DB 的聚合健康端点、limit 数值存在但小于压测 RSS、 PreStop 写了 sleep 却短于 Ingress sync 周期。两者都过,才接近「可发布」。豁免必须写明时限与回补计划, 避免「临时豁免」变成永久技术债。若团队使用 GitOps,可将阻塞项映射为 CI job,把人工勾选沉淀为流水线门禁。

决策框架 · 收尾

9决策矩阵、相邻知识地图与延伸

K8s 生产稳定性不是「记住几个 YAML 字段」,而是在调度、cgroup、探针语义、终止时序四条线上同时成立。 遇到红灯时,用下表决定继续调参、迁移模型,还是先补齐相邻能力。

决策矩阵:继续 / 迁移 / 选型

信号 / 处境继续(调参)迁移(换模型)选型补充
Exit 137,堆使用率不高 上调 limits、压测 RSS、收紧堆外 若必须固定超大堆,评估专用节点池 / Guaranteed 联动 T10 MaxRAMPercentage
慢启动 CrashLoop 加 startupProbe,拉长启动窗 启动逻辑本身不可收敛时,拆 init / 预热 Job 勿用 liveness 顶替启动窗
滚动 502 / 重复消费 PreStop + grace + PDB 强状态会话需会话粘滞或 Stateful 剧本 注册中心主动 deregister
HPA 扩不动 / Pending 扩 ResourceQuota、补 requests 批流混部则拆命名空间 LimitRange 防裸奔 Pod
DB 抖动触发全量重启 DB 检查移出 liveness 依赖熔断应在应用/Sentinel 层(T12) 平台健康检查语义对齐
已核验事实

Pod 终止时,若配置了 PreStop,kubelet 会在发送 SIGTERM 之前执行该钩子; terminationGracePeriodSeconds 约束从终止流程开始到强制 SIGKILL 的总时限。 详见官方 Pod Lifecycle 文档中的 termination 章节。

相邻知识地图

  • T16 K8s 核心资源理解:先建立 Pod / Deployment / Service 分层,再谈本篇参数合同。
  • T10 容器化下 JVM 适配:补齐 cgroup 感知与 MaxRAMPercentage,与本篇 limits 预算闭环。
  • T18 K8s 简单运维实战:上线后用 describe / logs / 监控把红灯归因到具体层。
  • T11 Nacos:注册中心摘流与 K8s Endpoints 双轨对齐。
  • T12 Sentinel:过载背压应在应用层完成,勿滥用 liveness 杀容器。

要点沉淀

  • 配额:requests 管调度,limits 管 cgroup 硬顶;ResourceQuota / LimitRange 防止租户级失控。
  • 探针:startup 护启动,liveness 管重启,readiness 管流量;三者不可混用同一「重」检查。
  • 优雅停机:PreStop 与 grace 必须覆盖 Endpoint 传播延迟与应用 shutdown。
  • OOMKilled:137 是内核杀容器,与 JVM OOM 不同;limits 必须预算堆外,Java 参数见 T10。
  • 门禁:检查表管事前,告警管事中,季度 Quota 复盘管事后容量。

建议将本篇检查表粘贴到发布工单模板,并与 Prometheus 告警 (kube_pod_container_status_restarts_totalcontainer_memory_working_set_bytes / container_spec_memory_limit_bytes)绑定, 使配置治理与运行时信号形成反馈回路。四篇合在一起(T16 → 本篇 → T10 → T18),构成 Docker/K8s 板块从入门到治理的闭环。

最后回到第一章的复合场景:当 CrashLoop、502、OOMKilled 同时亮起时,不要平行开三场无重点的战争。 先用事件 Reason 分层——Pending/Quota、Unhealthy、OOMKilled、Terminating 截断——再决定改 Quota、改探针、改 limit,还是改 grace。 分层对了,改一个旋钮往往就能熄灭两盏灯;分层错了,加再多副本也只是把故障复制到更多节点上。 这就是本篇相对 T16 的增量:对象模型告诉你「是什么」,稳定性配置告诉你「合同怎么写才不会在高峰翻车」。把合同写清楚,比事后加机器更便宜,也比半夜凭感觉回滚更可辩护。稳定性治理的终点不是「YAML 很漂亮」,而是发布窗口不再被同一类红灯反复叫醒。这是工程判断力。