1同一发布窗口里,为什么三盏红灯一起亮
某团队在 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 同步时被改回去。
各层职责与失败窗口
| 层级 | 核心对象 | 答什么问题 | 典型失败表现 |
|---|---|---|---|
| 命名空间 | ResourceQuota、LimitRange | 团队/环境总共能用多少 CPU、内存、Pod 数 | 创建失败、HPA 无法扩容、隐性「无 limits」Pod 挤占节点 |
| 工作负载 | resources、probes | QoS 等级、何时接流量、何时重启 | 调度 Pending、探针误杀、就绪前接流量 |
| 生命周期 | PreStop、grace period、PDB | SIGTERM 之后还能跑多久、如何主动摘流 | 滚动发布 502、消息重复消费、连接泄漏 |
| 内核 cgroup | memory limit、OOM score | 进程 RSS 超过 limit 时谁被 kill | OOMKilled 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,批高峰拒绝在线服务扩容——根因是命名空间配额未按工作负载类型拆分。
机制:三件独立事情
- 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
规格估算四步法
- 压测基线:在接近生产的镜像与运行时参数下测 P95 CPU 与 RSS 峰值;压测环境 cgroup 限制应与生产 limits 一致。
- requests:取 P95 × 1.1~1.2(作者经验区间,需复测),保证常态调度不过度拥挤。
- limits:取峰值 × 1.2~1.5,且必须大于堆 + 元空间 + 堆外 + 本地缓存(Java 见 T10)。
- 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 |
| 生产禁 BestEffort | Admission 拒绝无 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: 10、periodSeconds: 5。启动第 15 秒起探针失败,kubelet 连续杀容器,Pod 永远进不了 Ready,Ingress 502。
复合场景 D:liveness 与 readiness 共用含慢 SQL 的 /health。数据库短暂抖动时 readiness 摘流(正确),
但 liveness 也失败导致全量重启(过度)。
复合场景 J:Spring Boot 已分离 liveness/readiness,但 Ingress 仍打聚合 /actuator/health,磁盘空间不足导致整站被标记不健康——平台各层健康检查语义必须对齐。
| 探针 | 失败后果 | 典型用途 | 不应承担 |
|---|---|---|---|
| Startup | 启动完成前阻塞其它探针判定 | 慢启动(大缓存、JIT、证书) | 长期运行期健康检查 |
| Liveness | kubelet 杀容器并重启 | 死锁、无限阻塞 | 依赖外部系统的短暂不可用 |
| 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
决策框架与探针类型
- 启动时间 > 30s → 必须加 startupProbe(或极大调高 liveness 的 initialDelay,但不如 startup 清晰)。
- liveness 应轻量、只依赖进程自身;readiness 可检查 DB、缓存、下游。
timeoutSeconds小于periodSeconds;failureThreshold × period 为动作窗口。- 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 选型
| 类型 | 适用 | 注意点 |
|---|---|---|
| httpGet | Spring Actuator、/healthz | port 为容器端口非 Service port;HTTPS 需配置 scheme |
| grpc | gRPC 原生服务(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」与「入口不健康」两套结论,优先对齐语义再谈加副本。
再强调一次失败阈值的可读性:把 failureThreshold、periodSeconds、timeoutSeconds
换算成「最长忍受不可用秒数」,写进服务 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。
终止时序(机制共识)
- 若存在 PDB,需满足 minAvailable / maxUnavailable 合同后才允许进一步驱逐或滚动。
- API Server 标记删除;若存在 PreStop Hook,kubelet 先执行 PreStop,再向容器发 SIGTERM。
- 并行:EndpointSlice 因 deletionTimestamp 或 readiness 失败将 Pod 从后端移除(存在传播延迟)。
- 等待
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: 0、maxSurge: 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 才从「经验拍脑袋」变成「可辩护参数」。对长连接网关与消息消费者,这组实验几乎是上线门禁的一部分。
6规避 OOMKilled:RSS 预算、节点驱逐与 JVM 联动
复合场景 G(与 T10 联动):Java 17 容器误用固定 -Xmx 大于 limit,或堆外、元空间、CodeCache、DirectByteBuffer 与 native 内存不受堆上限约束。
监控显示堆使用率约 60%,Pod 仍 OOMKilled——杀的是 cgroup 内 RSS 总量,不是 JVM Heap。
| 内存区域 | Java 典型 | 是否受 -Xmx 约束 | 是否计入 cgroup RSS |
|---|---|---|---|
| Heap | -Xmx / MaxRAMPercentage | 是 | 是 |
| Metaspace | -XX:MaxMetaspaceSize | 部分 | 是 |
| Thread stacks | 线程数 × 默认栈 | 否 | 是 |
| Direct / Mapped | Netty、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 pod 看 Last 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,避免仅看「副本数达标」即宣告发布成功。
8反例对照、上线前检查表与发布后验证
验证层强调「反例 → 后果 → 改法」。自动化拦底线,评审表管业务语义(例如 limit 是否匹配 T10 堆比例)。
| 维度 | 反例 | 后果 | 推荐 |
|---|---|---|---|
| 内存 | limit 512Mi,-Xmx512m | 无堆外余量,OOMKilled 137 | MaxRAMPercentage 75%,limit ≥ 约 1.4× 堆目标 |
| CPU | 仅 limit 4 核,无 request | 调度拥挤,throttling 误判为慢 | request 按 P95,limit 为 burst 上限 |
| 探针 | 仅 liveness,10s 后开始 | 慢启动 CrashLoopBackOff | startupProbe 覆盖启动窗口 |
| 探针 | liveness 查 DB | DB 抖动全量重启 | DB 检查仅放 readiness |
| 停机 | 默认 grace 30s,无 PreStop | 滚动 502 | PreStop 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 尖刺恰好出现在流量切到新版本后的前几个请求突发里,错过这个窗口就只剩用户投诉。
kubectl rollout status是否在 deadline 内完成。- 新 Pod RESTARTS 是否递增,describe 有无探针失败事件。
- Ingress/网关 5xx 与 P99 相对基线是否尖刺。
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+元空间+堆外 | 开发 | 每次发布 |
| 命名空间 ResourceQuota | limits 总量 ≥ 最大副本×单 Pod limit×1.2 | 平台 / SRE | 季度 |
| LimitRange 默认值 | 新 Pod 不能无 limits 创建(若策略要求) | 平台 | 季度 |
| startupProbe(慢启动) | 启动 >30s 必须配置;窗口 ≥ 启动 P99 | 开发 | 每次发布 |
| liveness 轻量独立 | 不依赖 DB/下游;与 readiness 路径分离 | 开发 | 每次发布 |
| readiness 反映可接流量 | 依赖就绪才 200;滚动验证无 502 尖刺 | 开发 / QA | 每次发布 |
| PreStop + grace | grace ≥ PreStop + shutdown;含 sleep 或主动摘流 | 开发 | 每次发布 |
| PDB minAvailable | 多副本服务设置,避免并发不可用 | SRE | 每次发布 |
| OOM 与重启告警 | Exit 137、restart、working_set/limit >0.85 | SRE | 持续 |
| JVM 容器参数(Java) | 按 T10:MaxRAMPercentage、Metaspace、无超限 -Xmx | 开发 | 每次发布 |
| Sidecar resources | Istio/日志/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_total、
container_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 很漂亮」,而是发布窗口不再被同一类红灯反复叫醒。这是工程判断力。