我的 Java 服务一上 K8s 就莫名其妙地被反复重启、退出码永远是 137,我对着 OOMKilled 这个状态和容器内存限制排查了大半天才搞懂的惨痛经历
这是一个让我对"容器里的内存限制"彻底改观的故事。我有一个 Java 服务,在物理机/虚拟机上,跑得稳如老狗,从来不出问题。可当我把它打包成容器、部署到 K8s 上之后,怪事就发生了:它会莫名其妙地被反复重启。kubectl get pods 一看,RESTARTS 的次数蹭蹭上涨;kubectl describe pod 一查,赫然写着一行:Last State: Terminated, Reason: OOMKilled, Exit Code: 137。
我当时就纳闷了:OOMKilled?内存不足被杀?可这服务在虚拟机上用 4G 内存跑得好好的,我给容器也配了 4G 的 limit 啊,怎么会内存不足?我顺着 OOMKilled 和退出码 137 这两个线索深挖,才终于揭开真相,补上了我对"容器化"一个致命的认知盲区:问题的核心,是 JVM(以及很多老程序),在容器里,"看不见"容器的内存限制。我的容器 limit 设的是 4G,可这台 K8s 节点(宿主机)的物理内存,有 64G;而我的 JVM,启动时没有显式指定堆大小(-Xmx),于是它就按照"默认规则",去读取它以为的"可用内存"——而在老版本的 JDK / 没正确配置时,它读到的,不是容器那 4G 的 limit,而是宿主机的 64G!于是,JVM 自作主张地,把堆的上限(-Xmx),设成了宿主机内存的 1/4,也就是 16G!悲剧就此注定:JVM 以为自己有 16G 堆可用,于是放心大胆地分配内存;可它实际所在的容器,只有 4G 的硬限制;当 JVM 真实使用的内存,悄悄越过 4G 这条红线时,容器的 cgroup 内存限制就被触发,Linux 内核的 OOM Killer 会毫不留情地,把这个超限的进程,直接 SIGKILL 杀死(退出码 137 = 128 + 9,9 就是 SIGKILL)——这,就是 OOMKilled 反复重启的真相!我这才痛彻地明白:容器,不是一个"完整的小机器",它只是宿主机上一个"被 cgroup 限制了资源的进程组";那些诞生于"独占整台机器"时代的老程序(尤其是 JVM),如果不被明确告知"你现在被关在一个有内存上限的笼子里",它们就会按宿主机的庞大资源去自我规划,从而必然撑爆那个小小的容器。容器化,绝不是"把程序塞进容器就完事",更要让程序"感知"到它所处的资源边界。
故障现场:JVM 看不到容器 limit,按宿主机内存设堆
我把这个"OOMKilled"的现场,用配置和命令摊开给你看:
# ✗ 灾难: 容器限制 4G, 但 JVM 没被告知, 按宿主机 64G 设堆
# K8s Deployment 片段:
resources:
limits:
memory: "4Gi" # 容器硬限制 4G —— 超了就 OOMKilled
requests:
memory: "4Gi"
# 启动命令(✗ 没有指定 -Xmx, 也没开容器感知):
# java -jar app.jar
# → 老版本 JDK / 未配置时, JVM 读到的"可用内存"是宿主机的 64G!
# → JVM 默认把 -Xmx 设为 内存的 1/4 = 16G(远超容器 4G limit!)
# 进容器一看就露馅:
# $ java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
# MaxHeapSize = 17179869184 ← 16G! JVM 以为自己能用 16G 堆
# 实际发生:
# 1. JVM 以为有 16G 堆, 放心大胆分配内存。
# 2. 真实内存用量悄悄越过容器 4G 红线。
# 3. cgroup 内存限制触发 → 内核 OOM Killer 把进程 SIGKILL。
# 4. 退出码 137 = 128 + 9(SIGKILL), 容器重启, 周而复始。
# kubectl describe pod 现场:
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
# 根因: JVM 看不到容器 cgroup 内存 limit, 按宿主机内存设堆 → 实际超 limit → 被 OOMKilled。
看着这些配置和那个刺眼的 MaxHeapSize = 16G,我才算彻底想明白了这场反复重启的根源。问题的核心,是 JVM 看不到容器的内存 limit:容器 limit 是 4G,可 JVM 启动时没指定 -Xmx、也没开容器感知,在老版本 JDK 下,它读到的"可用内存"是宿主机的 64G,于是把堆上限默认设成了 64G 的 1/4 = 16G。悲剧就此发生:第一,JVM 以为自己有 16G 堆,放心大胆分配;第二,真实内存用量悄悄越过容器 4G 的红线;第三,cgroup 内存限制被触发,内核的 OOM Killer 把进程 SIGKILL;第四,退出码 137 = 128 + 9(9 即 SIGKILL),容器重启、周而复始。进容器跑一句 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize,就当场露馅——那个 16G 的堆上限,远超容器的 4G。kubectl describe pod 里的 Reason: OOMKilled, Exit Code: 137,也与之完全吻合。归根结底:JVM 看不到容器 cgroup 的内存 limit,按宿主机的庞大内存去设堆,导致实际用量必然超过 limit,被内核 OOMKilled。
第一件事:搞懂容器内存限制与 JVM 的"内存近视"
定位到根源,我必须把"容器里的内存"这件事,从根上彻底搞清楚:
容器内存限制: cgroup 划定上限, 超了就被内核 OOM Killer 杀
# 容器的本质:
# - 容器 NOT 虚拟机, 它只是宿主机上"被 cgroup 限制资源的进程"。
# - memory limit 由 cgroup 实施: 进程内存用量超过 limit → 触发内核 OOM。
# - 被 OOM Killer 杀 = SIGKILL → 退出码 137(128+9)。
# JVM 的"内存近视"(关键!):
# - 老版本 JDK(8u131 前)完全不感知 cgroup → 读到宿主机内存。
# - 没设 -Xmx 时, JVM 按"它以为的可用内存"的 ~1/4 设默认堆。
# - 宿主机 64G → 堆默认 16G → 远超容器 4G limit → 必然 OOMKilled。
# 更隐蔽的: JVM 内存 ≠ 只有堆!
# 一个 Java 进程的总内存 = 堆(-Xmx) + 非堆:
# - Metaspace(类元数据)
# - 线程栈(每线程约 1M, 线程多很可观)
# - JIT 编译缓存 / 代码缓存
# - 直接内存 DirectByteBuffer(NIO/Netty 常用, 默认约等于 -Xmx)
# - GC 自身开销 / native 库内存
# → 即使 -Xmx 设成 limit, 加上非堆也会超! 必须给非堆留余量。
# 退出码速记:
# 137 = 128 + 9 → SIGKILL(被 OOM Killer 或 kill -9)
# 143 = 128 + 15 → SIGTERM(正常停止/优雅关闭信号)
# 关键认知: 让程序"感知"它所处的资源边界。
# 容器化不是塞进去就行, 要告诉程序"你被限制在多大的笼子里"。
# 核心: 容器内存超 cgroup limit 就被 OOMKilled(退出码137);
# JVM 默认看不到 limit 按宿主机设堆, 且总内存=堆+非堆, 都要算进 limit。
原理终于清晰了。容器的本质,不是虚拟机,而是宿主机上"被 cgroup 限制了资源的进程";内存 limit 由 cgroup 实施,进程用量一超,就触发内核 OOM、被 SIGKILL(退出码 137)。而 JVM 有"内存近视":老版本 JDK(8u131 之前)完全不感知 cgroup,读到的是宿主机内存;没设 -Xmx 时,它按"以为的可用内存"的约 1/4 设默认堆,宿主机 64G 就设出 16G 堆,必然撑爆 4G 容器。更隐蔽的是:JVM 的总内存,远不止堆!一个 Java 进程的内存 = 堆(-Xmx)+ 非堆,而非堆包括 Metaspace、线程栈(每线程约 1M)、JIT 代码缓存、直接内存(DirectByteBuffer,NIO/Netty 常用,默认约等于 -Xmx)、GC 开销、native 库——即使你把 -Xmx 设成等于 limit,加上这些非堆,照样会超!必须给非堆留出余量。我还记下了退出码速记:137 = 128 + 9(SIGKILL,被 OOM Killer 杀);143 = 128 + 15(SIGTERM,正常停止)。由此,我刻下一个关键认知:容器化不是"塞进去就行",要让程序"感知"它所处的资源边界——告诉它"你被关在多大的笼子里"。容器内存超 cgroup limit 就被 OOMKilled;JVM 默认看不到 limit、按宿主机设堆,且总内存 = 堆 + 非堆,都要算进 limit。
第二件事:正解——让 JVM 感知容器,并给非堆留余量
搞懂了原理,正解就清晰了:要让 JVM 感知到容器的内存限制,并在设堆时给非堆内存留出足够余量。
# ✓ 正解一: 开启容器感知(JDK 10+ 默认开; JDK 8u191+ 需显式开)
# JDK 8u191 ~ 8uXXX:
java -XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \ # ✓ 堆最多用容器 limit 的 75%, 留 25% 给非堆
-jar app.jar
# UseContainerSupport: 让 JVM 读 cgroup limit(而非宿主机内存)。
# MaxRAMPercentage: 堆占"容器 limit"的百分比(注意是百分比, 不是小数!)
# ✓ 正解二: 干脆显式写死 -Xmx(最稳妥, 不依赖版本行为)
java -Xmx3g -Xms3g \ # ✓ 容器 4G, 堆给 3G, 留 1G 给非堆+系统
-XX:MaxMetaspaceSize=256m \ # ✓ 限制 Metaspace, 防它无限涨
-jar app.jar
# ✓ 验证: 进容器确认 JVM 真的看到了 4G(而不是 64G)
java -XX:+PrintFlagsFinal -version | grep -E "MaxHeapSize|MaxRAMPercentage"
# MaxHeapSize 应约等于 3G(而不是 16G)→ 说明生效了
# 怎么给非堆留余量(经验法则):
# 容器 limit = -Xmx(堆) + Metaspace + 线程栈*线程数 + 直接内存 + GC + native
# - 小服务: -Xmx ≈ limit * 0.7~0.75, 留 25~30% 给非堆。
# - 用 Netty/NIO 大量直接内存的: 还要 -XX:MaxDirectMemorySize 限制, 留更多。
# ⚠ 老版本 JDK(8u131 前)不支持 UseContainerSupport:
# 必须老老实实显式 -Xmx, 或升级 JDK。
# 核心: 用 UseContainerSupport + MaxRAMPercentage 让 JVM 感知容器,
# 或直接显式 -Xmx; 并务必给非堆(Metaspace/线程栈/直接内存)留出余量。
修复的方向,清晰而坚定。正解一:开启容器感知——用 -XX:+UseContainerSupport 让 JVM 去读 cgroup 的 limit(而不是宿主机内存),再用 -XX:MaxRAMPercentage=75.0 让堆最多占容器 limit 的 75%(注意这是百分比,不是小数!这是个经典坑),给非堆留出 25%。(JDK 10+ 默认就开了容器感知,JDK 8u191+ 需要显式开。)正解二(最稳妥、不依赖版本):干脆显式写死 -Xmx——容器 4G,堆就给 -Xmx3g,留 1G 给非堆和系统,再用 -XX:MaxMetaspaceSize 限制 Metaspace 防它无限涨。改完一定要验证:进容器跑 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize,确认它真的约等于 3G(而不是 16G),才算生效。而怎么给非堆留余量?记住:容器 limit = 堆 + Metaspace + 线程栈×线程数 + 直接内存 + GC + native;小服务,-Xmx 取 limit 的 70~75%;大量用 Netty/NIO 直接内存的,还要用 -XX:MaxDirectMemorySize 限制、留更多余量。归根结底:用 UseContainerSupport + MaxRAMPercentage 让 JVM 感知容器,或直接显式 -Xmx;并务必给非堆留出余量。
第三件事:OOMKilled 的排查路径——分清谁 OOM 了
这次踩坑也让我意识到,"OOM"其实有好几种,排查时第一步就是分清是哪一种,否则方向全错:
OOM 排查: 先分清是"容器 OOMKilled" 还是 "JVM 堆 OOM"
# 两种 OOM, 完全不同!
# A. 容器 OOMKilled(本文): 进程总内存超 cgroup limit, 被内核杀。
# - 现象: 退出码 137, kubectl describe 显示 Reason: OOMKilled。
# - 没有 Java 异常栈(进程被直接 SIGKILL, 来不及打日志)。
# - 根因: -Xmx 太大 / 非堆没留余量 / limit 太小 / 内存泄漏。
# B. JVM 堆 OOM: 堆内对象超过 -Xmx, JVM 自己抛异常。
# - 现象: 日志里有 java.lang.OutOfMemoryError: Java heap space。
# - 进程可能还活着(看 OOM 行为配置), 有完整异常栈。
# - 根因: 堆设太小 / 内存泄漏(对象一直被引用不释放)。
# 排查步骤:
# 1. kubectl describe pod → 看 Last State / Reason / Exit Code
# 137 + OOMKilled → 走 A 路线(容器层面超限)
# 2. 看应用日志有没有 OutOfMemoryError → 有 → 走 B 路线(堆内超限)
# 3. 看内存监控曲线:
# - 缓慢只涨不跌 → 内存泄漏(堆 或 非堆 或 native)。
# - 突然飙高 → 某次大请求/大批量加载。
# 4. 容器 OOMKilled 但 -Xmx 已合理 → 查非堆:
# - Metaspace 涨? 线程数爆炸? 直接内存泄漏(Netty)? native 内存?
# - 加 -XX:NativeMemoryTracking=summary 看各区用量。
# 经验:
# - 退出码 137 + 无 Java 异常 → 几乎一定是容器 OOMKilled。
# - 设了合理 -Xmx 还 OOMKilled → 重点查非堆和泄漏, 别只盯着堆。
# 核心: 先用退出码137/OOMKilled 与 OutOfMemoryError 区分"容器OOM"和"堆OOM";
# 容器 OOM 查总内存(堆+非堆+泄漏), 堆 OOM 查堆设置与对象泄漏。
这套排查路径,让我面对各种"OOM"不再慌乱。最关键的,是分清两种截然不同的 OOM:A. 容器 OOMKilled(本文)——进程总内存超 cgroup limit、被内核杀,现象是退出码 137、Reason: OOMKilled、且没有 Java 异常栈(进程被直接 SIGKILL,来不及打日志);B. JVM 堆 OOM——堆内对象超过 -Xmx、JVM 自己抛 OutOfMemoryError: Java heap space,有完整异常栈。排查步骤就此展开:第一,kubectl describe pod 看退出码——137 + OOMKilled 走 A 路线;第二,看应用日志有没有 OutOfMemoryError——有就走 B 路线;第三,看内存曲线——缓慢只涨不跌是泄漏,突然飙高是某次大请求/大批量加载;第四,如果 -Xmx 已合理却还 OOMKilled,就重点查非堆(Metaspace 涨没涨?线程数爆没爆?直接内存/native 有没有泄漏?),用 -XX:NativeMemoryTracking=summary 看各区用量。我也总结了经验:退出码 137 + 无 Java 异常,几乎一定是容器 OOMKilled;设了合理 -Xmx 还 OOMKilled,重点查非堆和泄漏,别只盯着堆。归根结底:先用"退出码 137/OOMKilled"和"OutOfMemoryError"区分容器 OOM 与堆 OOM,再分别对症。
下面这张图,是这次 OOMKilled 的诊断与解决全过程:
第四件事:容器退出码与状态的速查表
这次踩坑后,我把容器常见的退出码和状态,整理成一张速查表,下次再遇到"容器又挂了"能快速判断方向。
| 退出码 / 状态 | 含义 | 常见原因 |
|---|---|---|
| 137 + OOMKilled | SIGKILL(128+9), 内存超 limit 被内核杀 | -Xmx 太大 / 非堆没留余量 / limit 太小 / 泄漏 |
| 143 | SIGTERM(128+15), 收到停止信号正常退出 | 滚动更新、缩容、手动停止(正常) |
| 0 | 进程正常结束 | 主进程跑完就退(如把一次性任务当常驻服务) |
| 1 / 非0 | 程序自身异常退出 | 启动报错、配置错误、依赖连不上 |
| CrashLoopBackOff | 反复崩溃, K8s 拉长重启间隔 | 上述任意原因反复发生(看日志/describe) |
| OOMKilled(无重启) | 被杀但策略不重启 | 同 137, 检查 restartPolicy |
这张表,是我用这次事故换来的"容器急救手册"。最该刻进脑子的,是 137 和 143 的区别:137(SIGKILL)是"被强制杀死"——内存超限被 OOM Killer 干掉,是异常、要排查;而 143(SIGTERM)是"收到了礼貌的停止信号"——滚动更新、缩容时的正常退出,不用慌。其余的也各有指向:退出码 0 却一直重启,往往是把一次性任务当常驻服务跑了(主进程跑完就退);非 0 是程序自身启动报错(看日志);而 CrashLoopBackOff 不是原因、而是现象——它表示"反复崩溃,K8s 在拉长重启间隔",真正的原因,还得回到 describe 和日志里去找。它给我的最大启发是:容器挂了别瞎猜,先看退出码——它是定位问题方向的第一把钥匙。
第五件事:容器资源配置的常见误区
顺着这次的根因,我把团队 K8s 资源配置里那些"看着没问题、其实埋雷"的做法,系统排查了一遍。
| 误区做法 | 埋下的雷 | 正确姿势 |
|---|---|---|
| JVM 不设 -Xmx, 也不开容器感知 | 按宿主机内存设堆, 必 OOMKilled | UseContainerSupport 或显式 -Xmx |
| -Xmx 设成 = 容器 limit | 没给非堆留余量, 仍会超 | -Xmx ≈ limit * 0.7~0.75 |
| requests 远小于 limit | 超卖严重, 节点内存紧张时被驱逐 | requests 贴近真实用量, 别画太大饼 |
| 不设 limit(无限制) | 一个服务泄漏拖垮整个节点 | 务必设 limit, 隔离爆炸半径 |
| limit 拍脑袋设, 不看监控 | 设太小频繁 OOM, 太大浪费资源 | 按实际峰值用量 + 余量设定 |
| CPU limit 设太小 | 被 CPU throttle, GC/请求变慢卡顿 | CPU 给足, 关注 throttling 指标 |
这张表,让我对"配置资源"这件事,有了远超"填两个数字"的敬畏。最核心的两条,都和这次的根因一脉相承:其一,JVM 必须感知容器(否则按宿主机设堆,必挂);其二,-Xmx 不能等于 limit,必须给非堆留 25~30% 的余量。而其余几条,是我顺藤摸瓜发现的更大隐患:不设 limit,等于让一个服务的内存泄漏,有能力拖垮整个节点上的所有服务(limit 的意义,正是隔离爆炸半径);requests 画太小的饼,会导致节点超卖,内存紧张时你的 Pod 被驱逐;limit 拍脑袋设,小了频繁 OOM、大了浪费资源,正确做法是看监控、按真实峰值用量加余量来定;甚至 CPU limit 设太小,也会让进程被 throttle,导致 GC 和请求莫名卡顿。它们共同指向一个道理:资源配置不是"随便填个数",而是要基于"对程序真实资源消耗的理解"和"线上监控数据"来精算的工程决策——拍脑袋设的每一个数字,都是一颗待引爆的雷。
第六件事:容器化一个服务时,我现在会怎么决策
现在,每当我准备把一个服务容器化、配置它的资源,脑子里都会过一遍这张决策图——核心就一句:让程序感知边界,给资源留余量。
这张图的灵魂,是把容器化从"打个镜像跑起来",升级为"让程序与它的资源边界正确对齐"。第一步,看程序是不是 JVM 这类有"自适应内存"的运行时:是 JVM,就再看 JDK 版本——JDK 10+ 容器感知默认开,设好 MaxRAMPercentage=75 即可;JDK 8,就显式 -Xmx 或加 UseContainerSupport;普通进程,则评估它的真实内存峰值。第二步:无论哪种,都给非堆和系统留出 25% 以上的余量。第三步:设 limit(峰值+余量)和 requests(贴近真实用量)。最后,也是我以前最缺的一步:进容器验证 MaxHeapSize 真的生效了,再压测、看内存曲线,确认稳定、不 OOMKilled,才敢上线——而不是"部署上去,跑着没崩就以为没事"。
我立下的几条规矩
这场"上 K8s 就反复 OOMKilled"的事故,换来了我做容器化部署时,刻进骨子里的几条铁律:
- JVM 进容器,必须让它感知容器内存。JDK10+ 设 MaxRAMPercentage,JDK8 显式 -Xmx 或开 UseContainerSupport,绝不让它按宿主机猜。
- -Xmx 永远小于 limit,给非堆留余量。总内存 = 堆 + Metaspace + 线程栈 + 直接内存 + native,-Xmx 取 limit 的 70~75%。
- 退出码是第一把钥匙。137=OOMKilled(查内存),143=SIGTERM(正常),0 还重启(任务当服务跑了),非0(启动报错)。
- 分清容器 OOM 与堆 OOM。137 无 Java 异常=容器 OOM(查总内存/非堆);有 OutOfMemoryError=堆 OOM(查堆/泄漏)。
- limit 必设,按监控定,别拍脑袋。不设 limit 会让一个泄漏拖垮整个节点;数值按真实峰值+余量。
- 改完要进容器验证。java -XX:+PrintFlagsFinal 看 MaxHeapSize 是否真的等于预期,别活在"我配了"的错觉里。
- 容器不是虚拟机。它是被 cgroup 限制的进程,永远要让程序知道自己的资源边界在哪。
附:一份容器化 Java 服务的内存自查清单
为了让团队不再重蹈覆辙,我把这次的教训,固化成一份"上线前必过"的内存自查清单和命令,贴在这里:
# ===== 容器化 Java 服务内存自查清单(上线前逐项确认)=====
# 1. JVM 是否感知到了容器内存?(进容器执行)
java -XX:+PrintFlagsFinal -version | grep -E "MaxHeapSize|MaxRAMPercentage"
# ✓ MaxHeapSize 应约等于 (limit * 你设的百分比), 而不是宿主机的 1/4
# 2. 看容器实际内存 limit(进容器执行, 确认 JVM 读的是这个)
cat /sys/fs/cgroup/memory/memory.limit_in_bytes # cgroup v1
cat /sys/fs/cgroup/memory.max # cgroup v2
# ✓ 应等于你在 yaml 里设的 limit, 而不是宿主机总内存
# 3. 运行时看 Java 进程各内存区用量(开 NMT)
# 启动加: -XX:NativeMemoryTracking=summary
jcmd VM.native_memory summary
# ✓ 看 heap / metaspace / thread / direct 各占多少, 加起来要 < limit
# 4. 实时看容器整体内存用量 vs limit
kubectl top pod # 看 MEMORY 是否逼近 limit
# ✓ 稳态应在 limit 的 70~85%, 留出 GC 波动空间
# 5. 压测 + 观察(最关键!)
# 用接近生产的流量压测 10~30 分钟, 盯着:
# - kubectl top pod: 内存有没有持续上涨(泄漏)?
# - RESTARTS: 有没有偷偷 +1(OOMKilled)?
# - GC 日志: 是不是频繁 Full GC(堆太小)?
# 6. 退出码兜底检查
kubectl describe pod | grep -A3 "Last State"
# ✓ 不应出现 Reason: OOMKilled / Exit Code: 137
# 核心: 容器化 Java 上线前, 逐项验证 JVM 看到的内存=容器limit、
# 各内存区之和
这份清单,现在是我们团队所有 Java 服务上线前的"过关卡"。它的每一条,都直指这次事故的某个环节:第 1、2 条,确认 JVM 真的读到了容器的 limit(而不是宿主机内存)——这是这次悲剧的正面照妖镜;第 3 条,用 NMT 把堆、Metaspace、线程、直接内存各区用量摊开,确认它们加起来真的小于 limit;第 4、5 条,用 kubectl top 和压测,在真实流量下观察内存曲线有没有泄漏、有没有偷偷重启;第 6 条,最后用 describe 兜底,确认没有 OOMKilled。它把我过去那种"配好参数、部署上去、跑着没崩就算成功"的侥幸,彻底替换成了"每一个关键假设,都有对应的命令去验证"的踏实。这,正是这次事故留给我的最宝贵的东西:对于资源这种"有硬上限、超了就死"的东西,永远不要相信"应该没事",而要用一条条命令、一次次压测,把"确实没事"亲手验证出来。
写在最后
回头看,这场由"JVM 看不见容器内存"引发的、反复 OOMKilled 的事故,真正教给我的,是一个比"配置 JVM 参数"本身更深远的道理:当我们把一个旧世界的程序,搬进一个新世界的环境时,最危险的,往往不是那些"明显不兼容"的地方,而是那些"它自以为还和过去一样、其实早已天翻地覆"的隐性假设。JVM 的"按可用内存的 1/4 设堆",这个在"独占整台物理机"的旧时代无比合理的默认行为,一旦被搬进"被 cgroup 切割资源"的容器新时代,就瞬间变成了一颗定时炸弹——它还在用旧世界的眼睛(宿主机内存),打量着新世界的笼子(容器 limit),而这,正是悲剧的源头。所以,做容器化、做任何"环境迁移",最需要的,是一种"主动质疑一切隐性假设"的清醒:你要不断地问:"这个程序,它以为自己运行在一个什么样的环境里?这个假设,在新环境下,还成立吗?"真正平滑的迁移,从来不是"把东西搬过去就行",而是逐一审视并校正,那些被程序默默依赖、却在新环境下已然失效的前提。让程序"看清"它真实身处的世界——这,是我用一次"退出码 137"的崩溃,换来的、关于容器化最朴素、也最深刻的领悟。如果这篇复盘,能让你在下一次把服务塞进容器之前,多问一句"它知道自己只有这么点内存吗",那我对着 OOMKilled 熬的这大半天,就值了。
—— 别看了 · 2026