我的容器三天两头被悄无声息地重启,exit code 137,应用日志里却啥错误都没留下,我查了好几天才发现是被内存限制 OOMKilled 的深度复盘

我的服务跑在 K8s 容器里,三天两头被悄无声息地重启,可应用日志里啥错误都没有——像跑得好好的突然被一闷棍打死。看容器状态才发现线索:exit code 137、Reason: OOMKilled。深究才懂:容器内存超过了我设的 memory limit,被 Linux 内核 OOM Killer 用 SIGKILL 强杀;SIGKILL 无法捕获、无法清理,所以应用没机会留下任何日志;137 = 128 + 9(SIGKILL)。更隐蔽的是老 JVM 不感知 cgroup,按宿主机整机内存设堆,必然 OOM。这篇从 OOMKilled=超限被内核 SIGKILL 强杀讲起,到设合理 limit/让运行时感知 cgroup(MaxRAMPercentage)/治内存的正解、内存超限被杀 vs CPU 超限被限速、requests/limits/cgroup 基础,以及那句最戳心的——容器资源受限,应用要感知并适配它,别假设拥有整机资源。

我的容器三天两头被悄无声息地重启,exit code 137,应用日志里却啥错误都没留下,我查了好几天才发现是被内存限制 OOMKilled 的深度复盘

这是一个让我对"容器里资源是受限的"刻骨铭心的故事。我的服务,跑在 Kubernetes 的容器里。运行一段时间后,它会毫无征兆地、被重启——而且很频繁,三天两头来一次。最让我抓狂的是:我去翻应用自己的日志,啥错误都没有!没有异常堆栈、没有 panic、没有任何"我崩了"的迹象——它就像是跑得好好的,突然就被人从背后,一闷棍打死了

我一开始完全摸不着头脑:应用日志干干净净,它到底为什么会被重启?我去看容器/Pod 的状态,才发现了第一条线索:容器上一次退出的 exit code,是 137;Pod 的状态里,写着一个词:OOMKilled。我顺着这两个线索深挖,才恍然大悟,补上了关于容器资源最重要的一课:原来,我的容器,是被"内存超限"给杀死的!具体来说:在 Kubernetes 里,我给容器,设置了一个内存上限(limit);而我的应用,在运行中,用的内存,超过了这个上限。一旦容器使用的内存,触碰到了它的 limit,Linux 内核的 OOM Killer(Out-Of-Memory 杀手),就会毫不留情地,用 SIGKILL 信号,把容器里的进程,直接杀死!而这,就解释了我所有的困惑:为什么应用日志啥也没留下?——因为这不是应用"自己崩溃"的!它不是抛了个异常、走了正常的崩溃流程;而是被外部的内核,用 SIGKILL 这种"无法被捕获、无法做任何收尾"的信号,瞬间、强制地杀掉了——应用根本没有机会,去打印任何一句"我要死了"的日志。为什么 exit code 是 137?——这是一个约定:128 + 信号编号;而 SIGKILL 的信号编号是 9,128 + 9 = 137,所以,看到 exit 137,就基本等于"它是被 SIGKILL 杀的"(而内存超限的 OOMKilled,正是用 SIGKILL)。归根结底:我一直以为,容器里的应用,能像在物理机上那样,随意地使用内存;殊不知,容器是被资源限制(limit)框住的——它能用的内存,是有一个硬上限的;一旦超过,就会被内核无情地、悄无声息地杀掉。我那个跑得好好的应用,不是自己崩了,而是因为吃内存吃过了头,被它所在的"笼子",给关停了

故障现场:容器内存超限,被内核 OOM Killer 杀死

我把这个"OOMKilled"的现场,用配置和过程摊开给你看:

# 容器配置: 设了内存上限
resources:
  requests:
    memory: "256Mi"      # 请求(调度依据)
  limits:
    memory: "512Mi"      # ✗ 上限! 应用一旦用超 512Mi, 就会被 OOMKilled

# 应用实际: 内存慢慢涨, 涨过了 512Mi
#   → Linux 内核 OOM Killer 检测到容器内存超限
#   → 发 SIGKILL(信号9), 强制杀死容器内进程
#   → 容器退出, exit code = 137 (= 128 + 9)
#   → Pod 状态: OOMKilled; k8s 重启它 → 过会儿又涨过 limit → 又被杀(反复重启)
# 排查线索:
kubectl get pod                       # STATUS 里可能看到 Running 但 RESTARTS 在涨
kubectl describe pod <pod>
#   → Last State: Terminated
#        Reason: OOMKilled          ← 关键! 内存超限被杀
#        Exit Code: 137             ← 137 = 128+9(SIGKILL)

# 为什么应用日志没有错误?
#   - SIGKILL(9): 无法被捕获、无法被忽略、无法做任何清理!
#     (和优雅停机的 SIGTERM 不同, SIGKILL 是"立即处决")
#   - 所以应用没机会打印任何日志, 直接被瞬间杀掉 → 日志干净。

# 为什么反复重启?
#   - 内存涨过 limit → 被杀 → k8s 重启 → 内存又涨过 limit → 又被杀 → 循环。
#   - (如果是内存泄漏, 更是必然反复 OOM)

# exit code 速查:
#   - 137 = 128 + 9  → 被 SIGKILL 杀(常见: OOMKilled, 或 kill -9)
#   - 143 = 128 + 15 → 被 SIGTERM(正常终止信号)
#   - 看到 137 + OOMKilled → 基本就是内存超限。

# 根因: 应用内存用量超过了容器的 memory limit
#   → 内核 OOM Killer 用 SIGKILL 强杀 → exit 137, 应用日志无痕。

看着这些线索,我才算真正理解了这场"悄无声息的重启"的根源。问题的核心,是我的容器,内存用量超过了它被设置的 memory limit,从而被Linux 内核的 OOM Killer,用 SIGKILL 强制杀死了。这就完美解释了我那两个最大的困惑:第一,为什么应用日志啥也没留下?——因为杀死它的,是 SIGKILL(信号 9),而 SIGKILL 是一个无法被捕获、无法被忽略、无法做任何清理的信号(这一点,和优雅停机用的、温和的 SIGTERM 截然不同——SIGKILL 是"立即处决");所以,应用根本没有机会去执行任何代码、打印任何日志,就被瞬间杀掉了,日志自然干干净净。第二,为什么 exit code 是 137?——这是 Linux 的一个约定:被信号杀死的进程,exit code 是 128 + 信号编号;SIGKILL 的编号是 9,128 + 9 = 137——所以,看到 exit 137,就基本等于"它是被 SIGKILL 杀的"(而内存超限的 OOMKilled,正是用 SIGKILL;另外,143 = 128 + 15 则是被 SIGTERM 正常终止)。为什么会反复重启?因为:内存涨过 limit → 被杀 → k8s 重启它 → 内存又慢慢涨过 limit → 又被杀……如此循环(如果存在内存泄漏,那更是必然反复 OOM)。归根结底:我一直想当然地以为,容器里的应用,能像在物理机上那样,随意地使用内存;殊不知,容器是被"资源限制(limit)"牢牢框住的——它能用的内存,有一个硬性的上限;一旦超过,就会被内核,无情地、悄无声息地杀掉。我那个"跑得好好的"应用,根本不是自己崩了,而是因为吃内存吃过了头,被它所在的那个"资源的笼子",给强制关停了。而它被杀时连一句遗言都来不及留下,正是因为 SIGKILL 这种"一闷棍打死"的残酷。

第一件事:搞懂 OOMKilled——超限被内核杀,exit 137

定位到根源,我必须把"容器 OOMKilled"的机制彻底搞清楚:

容器 OOMKilled: 内存超限, 被内核 SIGKILL 强杀

# 容器的内存是"受限"的:
#   - k8s 给容器设了 memory limit(如 512Mi)——这是个"硬上限"。
#   - 容器(里的进程)能用的内存, 不能超过这个 limit。

# 超限会发生什么?
#   - 容器内存用量 > limit → 触发 Linux 内核的 OOM Killer。
#   - OOM Killer 发 SIGKILL(信号9), 强制杀死进程。
#   - 容器退出, exit code = 137 (128 + 9), Pod 状态标记 OOMKilled。

# 为什么"无声无息、日志干净"?
#   - SIGKILL 无法捕获、无法清理 → 进程被瞬间杀, 没机会打印任何日志。
#   - 它不是"应用自己崩溃"(没有异常堆栈), 是"被外部强杀"。
#   → 所以你在应用日志里找不到原因, 要去看"容器/Pod 的退出状态"。

# 怎么识别 OOMKilled?
#   - kubectl describe pod → Last State: Terminated, Reason: OOMKilled, Exit Code: 137。
#   - exit 137 + 频繁重启 → 高度怀疑内存超限。

# 两种 OOM, 要分清:
#   1. 容器超限(本文): 容器自己用超了 limit → 该容器被杀(Reason: OOMKilled)。
#   2. 节点内存不足: 整个 node 内存耗尽 → 内核杀掉"某些"容器(可能殃及别人)。
#      (设好 requests/limits, 能让调度更合理、减少这种情况)

# 关键认知: 容器不是"整机", 它的资源(CPU/内存)是被 cgroup 限制的。
#   应用必须在"这个限制内"运行, 否则被 OOMKilled。

# 核心: OOMKilled = 内存超限被内核 SIGKILL 强杀(exit 137, 日志无痕)。
#   要去看 Pod 退出状态识别它; 根治要从"内存用量 vs limit"入手。

原理终于清晰了。容器的内存,是"受限"的:k8s 给容器设的 memory limit(如 512Mi),是一个"硬上限",容器里的进程,能用的内存不能超过它。一旦超限:容器内存用量 > limit → 触发 Linux 内核的 OOM Killer → 它发 SIGKILL 强制杀死进程 → 容器退出、exit code = 137、Pod 标记为 OOMKilled为什么"无声无息、日志干净"?因为 SIGKILL 无法捕获、无法清理,进程被瞬间杀掉、没机会打印任何日志;而且,它不是"应用自己崩溃"(没有异常堆栈),而是"被外部强杀"——所以,你在应用日志里找不到原因,得去看"容器/Pod 的退出状态"。怎么识别它?kubectl describe pod,看 Last State: Terminated, Reason: OOMKilled, Exit Code: 137;看到 exit 137 + 频繁重启,就高度怀疑内存超限而 OOM 其实有两种,要分清:第一种是容器超限(本文)——容器自己用超了它的 limit,于是该容器被杀(Reason: OOMKilled);第二种是节点内存不足——整个 node 的内存耗尽了,内核会杀掉"某些"容器(可能殃及无辜);而设好 requests/limits,能让调度更合理、减少第二种情况。由此,我建立起一个关键认知:容器,不是"整机",它的资源(CPU、内存),是被 cgroup 限制的;应用,必须在"这个限制内"运行,否则就会被 OOMKilled。归根结底:OOMKilled,就是内存超限、被内核 SIGKILL 强杀(exit 137,且应用日志无痕);要去看 Pod 的退出状态来识别它;而根治,要从"应用的内存用量 vs 容器的 limit"这个核心矛盾入手——这,是我用一次"应用无故被反复重启",补上的、关于容器资源最关键的一课。

第二件事:正解——设合理 limit + 让运行时感知 cgroup + 治内存

搞懂了根因——"内存用量超过容器 limit 被杀"——正解就清晰了:从两端入手——一是把容器的 requests/limits 设到合理的值(够用、但不浪费);二是让应用运行时,"感知"到这个内存限制(尤其是 JVM 这类有自己内存管理的——老版本不感知 cgroup,会按整机内存来分配,必然 OOM);三是从根上排查并治理应用的内存用量(有没有泄漏、能不能优化)。

# 正解1: 设合理的 requests / limits(基于实测的真实用量)
resources:
  requests:
    memory: "512Mi"      # 请求 = 平时稳定用量(调度依据)
  limits:
    memory: "1Gi"        # 上限 = 峰值用量留点余量(别卡得太死, 也别太松)
# 怎么定? 压测/观测应用的真实内存曲线(平时多少、峰值多少), 据此设。
# - limit 太小: 一点波动就 OOMKilled。
# - limit 太大: 浪费资源 / 掩盖了内存问题。
# 正解2: 让运行时"感知"容器的内存限制(关键! 尤其 JVM)
# 问题: 老 JVM 不感知 cgroup, 它看到的是"宿主机"的总内存(如 64G),
#       于是把堆设得巨大 → 容器 limit 才 1G → JVM 想用远超 1G → 必被 OOMKilled!
# 解法:
#   - JDK 10+ / JDK8u191+: 默认开启 -XX:+UseContainerSupport(感知 cgroup limit)。
#   - 用 -XX:MaxRAMPercentage=75.0 让堆按"容器内存的百分比"设, 而非整机。
#   - (老版本: 手动 -Xmx 设到容器 limit 内, 留出非堆内存的余量)
# 其它运行时同理: Node 的 --max-old-space-size, 也要按容器内存设, 别用默认。
# → 核心: 让应用"知道自己在一个受限的容器里", 按容器的资源来配置自己。
# 正解3: 从根上治理内存
#   - 排查内存泄漏(若内存只涨不降 → 大概率泄漏): 用 profiler / heap dump 分析。
#   - 优化内存用量: 减少缓存、流式处理大数据(别全load进内存)、对象复用。
#   - 别盲目调大 limit 来"掩盖"泄漏——那只是推迟 OOM, 治标不治本。

# 核心: 设合理 limit + 让运行时感知 cgroup(JVM 尤其!)+ 排查治理内存。
#   三管齐下: 给够资源、让应用按限制配置自己、并从根上控制内存用量。

这套正解,从三个层面,系统地解决 OOMKilled。正解1(设合理的 requests/limits):基于实测的真实用量来设——requests 设成平时稳定的用量(作调度依据)、limits 设成峰值用量再留点余量;怎么定?去压测、观测应用的真实内存曲线(平时多少、峰值多少),据此来设;limit 太小,一点波动就 OOMKilled,太大,则浪费资源、还会掩盖内存问题。正解2(让运行时"感知"容器的内存限制,关键!尤其 JVM):这是一个极其隐蔽、又极其重要的点——老版本的 JVM,不感知 cgroup 的限制,它看到的,是"宿主机"的总内存(比如 64G),于是把堆(heap)设得巨大;可容器的 limit 才 1G,JVM 一想用远超 1G 的内存,就必然被 OOMKilled!解法是:用 JDK 10+(或 8u191+),它默认开启 -XX:+UseContainerSupport感知 cgroup limit;并用 -XX:MaxRAMPercentage 让堆按"容器内存的百分比"来设,而不是按整机;其它运行时也同理(如 Node 的 --max-old-space-size)。核心,是让应用"知道自己在一个受限的容器里",并按容器的资源,来配置自己正解3(从根上治理内存):排查内存泄漏(如果内存只涨不降,大概率是泄漏,用 profiler / heap dump 分析)、优化内存用量(减少缓存、流式处理大数据而非全加载进内存、对象复用);别盲目调大 limit 来"掩盖"泄漏——那只是推迟 OOM、治标不治本。归根结底:设合理 limit + 让运行时感知 cgroup(JVM 尤其!)+ 排查治理内存——三管齐下:给够资源、让应用按限制配置自己、并从根上控制内存用量。我那次的错误,既没有让 JVM 感知容器的限制、也没排查内存,只是干等着它一次次被杀;而正解,是从这三个层面,系统地把它解决掉。

下面这张图,对比了"放任 OOM"和"系统解决"两条路径:

这张图的对比很清楚:容器频繁 OOMKilled(exit 137)时,先 describe pod 确认 Reason,再追问内存为什么超 limit——是运行时不感知限制(如老 JVM 按整机设堆)就让它感知 cgroup、是 limit 太小就基于实测调大、是内存泄漏就 profiler 排查修复;改对之后,应用就能按容器限制稳定运行、不再被杀。根本分野,在于你有没有让"应用的内存用量",真正适配"容器的内存限制"。

第三件事:容器资源相关的其它坑

填平了内存 OOM 这个坑,我系统排查了容器资源相关的其它常见坑:

容器资源相关的其它坑:

# 1. 内存超 limit 被 OOMKilled(本文): exit 137, 设 limit + 感知 cgroup + 治内存。

# 2. 没设 limit: 一个容器吃光节点内存 → 殃及同节点其它容器(node OOM)。
#    → 一定要设 requests/limits, 把资源用量框起来, 保护邻居。

# 3. CPU limit 不是"杀", 是"限流(throttle)":
#    - 超 CPU limit, 不会被杀, 而是被"限速"(变慢, CPU throttling)。
#    - 现象: 应用莫名变慢, 但不崩。要看 CPU throttling 指标。
#    (内存超限是"杀", CPU 超限是"限速"——两者处理方式不同!)

# 4. JVM/运行时不感知 cgroup(本文重点): 老版本按整机配置 → 必 OOM。

# 5. requests 设太低: 调度器以为不占资源, 塞太多 Pod 到一个节点 → 节点过载。

# 6. 临时存储(ephemeral-storage)超限: 写日志/临时文件太多, 也会被驱逐。

# 7. OOM 的不一定是"你以为的进程": OOM Killer 会挑"评分高"的杀, 容器里 PID1
#    被杀整个容器就退出。

# 排查工具:
#   - kubectl describe pod / kubectl top pod(看实时用量)
#   - 监控(Prometheus): 内存用量 vs limit、CPU throttling、重启次数。
#   - dmesg / 节点日志: 看内核 OOM Killer 的记录。

# 共同点: 容器是"资源受限"的运行环境, 应用要在限制内运行、并适配它。
# 原则: 设好 requests/limits、让运行时感知限制、监控资源用量。

这一排查,让我对容器资源问题,有了全面的认识。除了内存超限 OOMKilled(本文),还有几个相关的坑:没设 limit(一个容器吃光节点内存,殃及同节点的其它容器——所以一定要设 requests/limits,把用量框起来、保护邻居);CPU limit 是"限流"不是"杀"(这是个关键区别!超内存 limit 是被,而超 CPU limit 不会被杀、而是被"限速(throttle)"——现象是应用莫名变慢但不崩,要看 CPU throttling 指标);JVM/运行时不感知 cgroup(本文重点,老版本按整机配置必 OOM);requests 设太低(调度器以为不占资源、往一个节点塞太多 Pod,导致节点过载);临时存储超限(写日志/临时文件太多也会被驱逐);被 OOM 的不一定是你以为的进程(OOM Killer 会挑"评分高"的杀)。排查工具:kubectl describe pod/kubectl top pod(看实时用量)、监控(内存用量 vs limit、CPU throttling、重启次数)、dmesg/节点日志(看内核 OOM Killer 的记录)。归根结底,这些的共同点是:容器,是一个"资源受限"的运行环境;应用,必须在限制内运行、并适配它。所以,核心原则就是:设好 requests/limits、让运行时感知限制、监控资源用量——把这三件事做好,容器资源相关的这一类问题,就都能被你掌控。

第四件事:理清 requests/limits 与 cgroup 的基础

这次踩坑,逼我把 Kubernetes 的资源模型(requests/limits)和底层的 cgroup,系统地补了一遍:

requests / limits 与 cgroup: 容器资源限制的基础

# requests(请求) vs limits(上限):
#   - requests: "我至少需要这么多"。是"调度依据"——
#     调度器据此决定把 Pod 放到哪个节点(确保节点有足够资源)。
#   - limits: "我最多能用这么多"。是"硬上限"——
#     超了: 内存→被 OOMKilled; CPU→被 throttle 限速。
#   → requests 影响"放哪", limits 影响"超了怎么办"。

# 内存 limit vs CPU limit, 超限处理"完全不同":
#   - 内存超 limit: 不可压缩资源 → 直接 OOMKilled(杀)。
#   - CPU 超 limit: 可压缩资源 → CPU throttling(限速, 变慢, 不杀)。
#   → 内存是"硬"的(超了就死), CPU 是"软"的(超了就慢)。

# 底层是 cgroup(control groups):
#   - Linux 内核用 cgroup 来限制一个进程组能用的 CPU/内存等。
#   - 容器的 limit, 最终落实成 cgroup 的限制。
#   - 容器里读 /sys/fs/cgroup/... 能看到这些限制(运行时应该读它来自我配置)。

# 老 JVM 不感知 cgroup 的根源:
#   - 老 JVM 用 /proc/meminfo(宿主机总内存)来决定默认堆大小。
#   - 它"看不到" cgroup 的 limit → 以为有整机内存 → 堆设太大 → OOM。
#   - 新版本修复: 改为读 cgroup limit(UseContainerSupport)。

# QoS(服务质量)等级(影响被驱逐的优先级):
#   - Guaranteed(requests==limits): 最高, 最后被驱逐。
#   - Burstable(设了但 requests

这一补课,让我对容器的资源模型,有了体系化的理解。首先是 requests(请求)和 limits(上限)的区别:requests 是"我至少需要这么多",它是调度依据(调度器据此决定把 Pod 放到哪个节点);limits 是"我最多能用这么多",它是硬上限(超了:内存→被 OOMKilled,CPU→被 throttle)。一句话:requests 影响"放哪",limits 影响"超了怎么办"其次,一个关键区别:内存 limit 和 CPU limit,超限的处理完全不同——内存是"不可压缩"资源,超了就直接 OOMKilled(杀);CPU 是"可压缩"资源,超了就 CPU throttling(限速、变慢,但不杀)。内存是"硬"的(超了就死),CPU 是"软"的(超了就慢)。再往底层,是 cgroup:Linux 内核用 cgroup 来限制一个进程组能用的 CPU/内存;容器的 limit,最终落实成 cgroup 的限制;容器里读 /sys/fs/cgroup/... 就能看到这些限制(运行时应该读它来自我配置)。这也解释了老 JVM 不感知 cgroup 的根源:老 JVM 用 /proc/meminfo(宿主机总内存)来决定默认堆大小,它"看不到" cgroup 的 limit,以为有整机内存、把堆设太大,于是 OOM;新版本修复了这点(读 cgroup limit)。最后,还有 QoS 等级(影响被驱逐的优先级):Guaranteed(requests==limits,最高、最后被驱逐)、Burstable(中)、BestEffort(没设,最低、节点紧张时最先被驱逐)——所以,重要的服务,要把 requests/limits 设好(甚至相等),以提高 QoS归根结底:requests 管调度、limits 管上限;内存超限被杀、CPU 超限被限速;底层是 cgroup,而应用(尤其 JVM)要感知它,来配置自己。理解了这套模型,我才真正明白,为什么我的容器会被杀、以及该怎么从根上配置它。把内存和 CPU 超限的区别,整理成一张表:

维度 内存(memory) CPU
资源类型 不可压缩 可压缩
超过 limit OOMKilled(被杀,exit 137) throttling(被限速变慢)
现象 容器重启、日志无痕 应用莫名变慢、不崩
排查 describe pod 看 OOMKilled 看 CPU throttling 指标
应对 调 limit/治内存/感知 cgroup 调 CPU limit/优化性能

第五件事:应用要适配它的运行环境,别假设有"整机资源"

这次踩坑,在认知层面给了我最大的纠偏——它让我意识到,应用必须"知道并适配"自己所处的运行环境。我把这层反思,沉淀了下来:

认知纠偏: 应用要"感知并适配"它的运行环境, 别假设拥有整机资源

# 我的误解(错误的):
#   我(以及老 JVM)默认"应用拥有整台机器的资源", 想用多少内存用多少。
#   → 没意识到, 在容器里, 资源是被严格限制的; 应用却按"整机"来配置自己。

# 真相: 现代应用, 大多运行在"受限、隔离"的环境里(容器/云/Serverless)
#   - 你拿到的, 不是整台机器, 而是"一份被限定的资源配额"。
#   - 应用如果还按"整机"的假设来配置(堆大小、线程数、连接数), 就会和
#     实际的限制冲突 → OOM、被限速、被驱逐。
#   - 应用必须"感知"自己的限制(读 cgroup), 并据此"适配"自己的配置。

# 这是一个普遍的"环境适配"问题:
#   - JVM 堆: 要按"容器内存"设, 不是按整机(本文)。
#   - 线程池/连接池大小: 要按"分到的 CPU/资源"设, 不是按整机核数。
#   - GOMAXPROCS(Go): 容器里也该按 CPU limit 设(否则按整机核数, 调度抖动)。
#   → 应用的"自我配置", 要基于"它实际能用的资源", 而非"它所在物理机的资源"。

# 更普遍的智慧: 软件要"了解并尊重"它运行的边界
#   - 别假设"无限的资源"(内存/CPU/磁盘/连接都有限)。
#   - 别假设"独占的环境"(你和别人共享一台机器/节点)。
#   - 主动去"探测"环境的限制, 并让自己运行在限制之内。

# 核心: 应用要感知并适配它的运行环境(尤其容器的资源限制)。
#   别假设拥有整机资源——你只有"一份配额", 要在配额内, 配置好自己。

这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是:我(以及那个老 JVM),都默认"应用拥有整台机器的资源",想用多少内存就用多少;却没意识到,在容器里,资源是被严格限制的,而应用,却还按"整机"的假设,来配置自己。可真相是:现代应用,大多运行在"受限、隔离"的环境里(容器、云、Serverless)——你拿到的,不是整台机器,而是"一份被限定的资源配额";应用如果还按"整机"的假设,来配置自己的堆大小、线程数、连接数,就会和实际的限制冲突(OOM、被限速、被驱逐);所以,应用必须"感知"自己的限制(读 cgroup),并据此"适配"自己的配置而这,是一个普遍的"环境适配"问题:JVM 堆要按"容器内存"设(本文);线程池/连接池的大小,要按"分到的 CPU/资源"设,而不是按整机核数;Go 的 GOMAXPROCS,在容器里也该按 CPU limit 设(否则按整机核数,会引起调度抖动)——应用的"自我配置",要基于"它实际能用的资源",而不是"它所在物理机的资源"由此,我领悟到一个更普遍的智慧:软件,要"了解并尊重"它运行的边界——别假设"无限的资源"(内存、CPU、磁盘、连接,都是有限的)、别假设"独占的环境"(你在和别人共享一台机器/节点);要主动去"探测"环境的限制,并让自己运行在限制之内归根结底:应用,要感知并适配它的运行环境(尤其是容器的资源限制);别假设拥有整机资源——你只有"一份配额",要在配额内,配置好你自己。我那个被反复 OOMKilled 的应用,正是因为揣着"整机资源"的幻觉,撞上了容器"一份配额"的现实。把"假设整机资源"和"适配运行环境"对比成一张表:

维度 假设整机资源(踩坑) 适配运行环境(成熟)
对资源 以为想用多少用多少 知道只有一份配额
JVM 堆 按整机内存设(必 OOM) 按容器内存设
线程/连接池 按整机核数设 按分到的资源设
环境假设 无限、独占 有限、共享
配置依据 物理机的资源 实际能用的配额

一套"容器被反复重启该怎么查"的决策流程

把这次踩坑的全部教训,我浓缩成了一张"容器被反复重启、该怎么排查"的决策图,贴在了团队的 DevOps 规范里:

这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:容器反复重启,先 describe podLast State 的 Reason——是 OOMKilled(exit 137)就是内存超限、是 其它 exit 就看应用日志、是 CrashLoopBackOff 就查启动日志/探针。确认是内存超限后,再追问为什么超——运行时不感知限制(让 JVM 等感知 cgroup)、limit 太小(基于实测调大)、内存泄漏(profiler 排查)。而如果是莫名变慢但不崩,那是 CPU throttling、不是 OOM。这条"先看退出原因、再分内存/CPU、对症下药"的决策链,现在是我们团队排查每一个容器重启时的准则。

我立下的几条容器资源规矩

这次"OOMKilled 反复重启"的踩坑,让我把容器资源的注意事项,认真地立成了几条规矩:

  1. 容器反复重启先看 exit code 和 Reason。exit 137 + OOMKilled = 内存超限,不是应用自己崩;别只盯应用日志。
  2. 设合理的 requests/limits。基于实测内存曲线设,够用留余量;别不设(殃及邻居)、也别太大(掩盖问题)。
  3. 让运行时感知 cgroup。JVM 用 UseContainerSupport/MaxRAMPercentage,别按整机设堆;Node/Go 同理。
  4. 分清内存超限和 CPU 超限。内存超限被杀(exit 137),CPU 超限被限速(变慢不崩)。
  5. 排查治理内存。只涨不降是泄漏,用 profiler/heap dump 查;别盲目调大 limit 掩盖。
  6. 监控资源用量。内存 vs limit、CPU throttling、重启次数纳入监控,提前预警。
  7. 应用要适配运行环境。别假设整机资源,你只有一份配额,在配额内配置好自己。

写在最后

这次"我的容器无故反复重启、exit 137、应用日志却干干净净"的经历,是我在 DevOps 路上,一次很典型、也很受用的成长。它教给我的,远不止"设好内存 limit"这一条具体的运维经验,更是一个关于现代应用部署的根本认知——容器,是一个"资源受限、被隔离"的运行环境;应用,必须感知并适配它,而不能揣着"拥有整机资源"的幻觉。我那个被反复 OOMKilled 的服务,根源就在于,它(和那个老 JVM)默认自己能随意用整机的内存,却撞上了容器"一份配额"的硬限制,于是被内核一闷棍一闷棍地打死,连句遗言都来不及留。

所以,当你把应用部署到容器、云、Serverless 这类受限环境里时,请别再假设它"拥有整台机器"——而要清醒地认识到:它只分到了一份有限的资源配额;然后,做两件事:一是给它设好合理的资源 requests/limits(基于实测),二是让应用本身,感知到这个限制,并据此配置好自己(JVM 的堆、线程池、连接池,都要按"分到的资源",而非"整机资源"来设)。就像那个 OOMKilled 的容器,你只要让 JVM 感知到 cgroup 的内存限制、并设好合理的 limit,就绝不会再经历那种"无故被反复重启、还查不到原因"的困惑。从"假设拥有整机资源"到"感知并适配受限环境",从对着 exit 137 一头雾水到"看退出原因、对症下药",是从一个"会把容器跑起来"的开发,走向一个"懂容器、能驾驭受限环境"的工程师,必经的修炼。愿你的每一个容器,都在它的资源配额内,稳稳地运行;也愿你我,让每一个应用,都学会了解、并尊重,它所运行的那片边界。共勉。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我图省事每次请求都新建一个 HTTP 客户端,平时跑得好好的,流量一上来就连接耗尽、TIME_WAIT 堆成山、还报 too many open files,我查了好几天才懂连接要复用的深度复盘

2026-6-2 0:27:03

技术教程

我的 RAG 检索回来的片段总是缺头少尾、答非所问,我一直以为是检索算法不行,最后才发现是文档切块的策略从一开始就错了的深度复盘

2026-6-2 0:40:26

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索