一、问题背景
我们的订单中心 order-service 跑在 Kubernetes 集群(v1.27,containerd 运行时)上,Deployment 配置 6 个副本,平时负载稳定,内存常驻约 600MB。该服务是交易链路的核心,下游由支付、库存等服务调用,上游又被 API 网关和多个内部业务系统依赖。
某天上午,运营临时做了一个营销活动,订单量较平日上涨约 3 倍。活动开始约 20 分钟后,订单成功率告警触发。此时我们并未发版,纯属流量驱动的突发故障。集群节点规格为 8C16G,节点本身剩余内存充足,看起来不像是节点级资源问题。
二、故障现象
监控面板在 09:05 左右集中飘红:
order-service的CrashLoopBackOff副本数从 0 涨到 4,6 个副本里一度只剩 2 个在Running;- 上游 API 网关大量返回 504,
order-service调用超时率从 0.2% 升到 35%; - 订单创建成功率从 99.6% 跌到 58%,部分用户出现重复下单;
- 滚动更新状态卡在
Progressing:新副本ready数量始终达不到minAvailable,Deployment 既不成功也不回滚。
最迷惑的是:节点 kubectl top node 显示内存还有 6G+ 空闲,但 Pod 就是不断重启。这说明问题出在 Pod 级别内存限制(limit),而不是节点容量。
三、排查过程
第一步,看 Pod 事件,直接命中关键字:
|
|
Reason: OOMKilled 和 Exit Code: 137 明确说明容器是被 cgroup 杀了——进程实际用量超过了 memory limit。
第二步,看内存配置和真实用量:
|
|
limit 设了 1Gi,但流量上涨后 RSS 已逼近 980Mi,剩余空间几乎为 0。一旦有请求峰值或 GC 不及时,立刻越线被杀。
第三步,确认是不是内存泄漏。拉取一个还没被杀的 Pod 的堆信息:
|
|
OrderCacheEntry 实例高达 31 万个且持续增长——这是一个本地订单缓存,使用 ConcurrentHashMap 但没有设置容量上限和过期策略,活动期间订单不断写入,缓存无限膨胀,导致内存只涨不落。
第四步,解释"雪崩"链条。Pod 被 OOMKilled 后重启,重启期间 readinessProbe 在应用冷启动 + 缓存重建完成前一直返回 503,于是 Endpoint 不把流量切给它;而剩余存活副本要承接全部流量,内存进一步上涨,再次 OOMKilled——形成重启→探活失败→流量集中→再 OOM的死亡螺旋。滚动更新因为新副本始终 ready 不过半,卡死在 Progressing,无法自愈。
四、解决方案
应急(先止血):
- 立即调大
memory limit到 2Gi、request 提到 1Gi,并临时把副本数扩到 12,分散单 Pod 压力,打破越线被杀的循环:
|
|
-
给
readinessProbe增加initialDelaySeconds: 30与更宽容的failureThreshold,避免冷启动期被误判摘流(临时缓解,非根治)。 -
紧急重启有泄漏风险的缓存:对
OrderCacheEntry增加容量上限(LRU,最大 5 万条)+ TTL(10 分钟),重新发版。 -
给 Deployment 配置
minReadySeconds与progressDeadlineSeconds: 300,并在探针上加startupProbe,让冷启动阶段不被 readiness 误杀:
|
|
发版后副本稳定 Running,上游超时率 5 分钟内回落到 0.3%,订单成功率恢复到 99.4%。
五、根因分析
根因是无界本地缓存导致的内存泄漏,在流量上涨时触顶 memory limit 被 cgroup OOMKilled。雪崩的放大器是两点:
- readiness 探针在冷启动期误摘流:Pod 重启后缓存未预热完即被判定不健康,Endpoint 不接流量,存活副本承压更重。
- 滚动更新卡死:新副本始终达不到 ready 阈值,Deployment 既不能完成也不能自动回滚,故障窗口被人为拉长。
节点有空余内存是假象——Kubernetes 的 OOM 是按 Pod cgroup 边界触发的,和节点总内存无关。
六、预防措施
-
所有本地缓存必须有界:用 Caffeine/Guava Cache 显式设置
maximumSize与expireAfterWrite,禁止裸用ConcurrentHashMap当缓存。 -
内存 limit 留足 headroom:limit 不应紧贴常驻用量,建议 limit ≥ 常驻峰值 × 1.5,并配置
requests与limits一致以避免被调度到紧张节点。 -
探针三件套合理配置:
startupProbe保护冷启动、readinessProbe控制接流、livenessProbe控制重启,三者职责分离,避免互相误伤。 -
设置 PodDisruptionBudget + progressDeadlineSeconds,配合告警,让滚动更新卡死能被及时发现并人工/自动干预。
-
内存监控与压测:对
container_memory_working_set_bytes设 80% 告警,大促前按 3 倍流量做容量压测,提前暴露泄漏。
七、总结
这次故障的教训是:Kubernetes 的 OOM 是 Pod 级的,不是节点级的。看到节点有内存就以为安全,是典型的认知陷阱。更深的坑在于 OOMKilled 与探针、滚动更新的耦合——单点重启会被链路放大成整服务雪崩。排查时 kubectl describe pod 里的 Reason: OOMKilled + Exit Code 137 是第一线索,再用 jmap -histo:live 定位泄漏对象。容器环境下,任何"无限增长"的本地状态都是隐患,给缓存设边界、给探针分职责、给更新加兜底,三者缺一不可。