第一章:Go服务突然被kill -9?揭秘systemd、cgroup与内核OOM的3层拦截机制
当你的Go服务在生产环境毫无征兆地消失,dmesg里找不到OOM Killer日志,systemctl status却显示“killed with signal SIGKILL”,这往往不是应用崩溃,而是被三层防御机制中某一层主动终结——systemd、cgroup资源限制、内核OOM Killer,层层递进又职责分明。
systemd的KillMode与MemoryMax拦截
systemd并非仅做进程守护者。若服务单元文件中设置了MemoryMax=512M(需启用MemoryAccounting=true),当cgroup内存使用持续超限时,systemd会主动向主进程发送SIGKILL(非SIGTERM),且不等待KillSignal配置。验证方式:
# 查看服务实际生效的内存限制
systemctl show your-go-service.service | grep -E "(MemoryMax|MemoryAccounting)"
# 检查cgroup当前内存使用(v2)
cat /sys/fs/cgroup/system.slice/your-go-service.service/memory.current
cgroup v2的内存压力信号传递
cgroup v2引入memory.events文件,其中oom_kill计数器一旦增长,即表明该cgroup已被OOM Killer收割。但更早的预警信号藏在memory.pressure:
# 实时监控内存压力(高值预示OOM临近)
watch -n 1 'cat /sys/fs/cgroup/system.slice/your-go-service.service/memory.pressure'
# 输出示例:some=0.00 avg10=0.05 avg60=0.12 avg300=0.85 total=12450000
# avg300 > 0.5 是严重压力信号
内核OOM Killer的最终裁决
当cgroup未设限或systemd未介入,内核OOM Killer启动。它依据oom_score_adj(范围-1000~1000)选择牺牲进程。Go程序默认值为0,但若被误设为1000,则成为首选目标。检查方法:
# 查看进程OOM优先级(值越高越易被杀)
cat /proc/$(pgrep -f "your-go-binary")/oom_score_adj
# 临时降低风险(重启后失效)
echo -500 | sudo tee /proc/$(pgrep -f "your-go-binary")/oom_score_adj
| 层级 | 触发条件 | 日志位置 | 是否可配置 |
|---|---|---|---|
| systemd | MemoryMax超限且KillMode=control-group |
journalctl -u your-go-service |
✅ 单元文件配置 |
| cgroup v2 | memory.high触发内存回收失败 |
memory.events中的oom_kill |
✅ memory.high参数 |
| 内核OOM | 全局内存耗尽且无足够swap | dmesg -T \| grep -i "killed process" |
⚠️ 仅能调优oom_score_adj |
排查时务必按顺序检查:先systemctl status确认退出码是否为137(SIGKILL),再查journalctl有无“Killed process”字样,最后用dmesg定位OOM事件时间戳——三者时间差揭示拦截层级。
第二章:第一道防线——systemd的进程生命周期与强制终止机制
2.1 systemd Unit配置中的KillSignal与KillMode语义解析与实测验证
KillSignal 和 KillMode 共同决定服务进程终止的精确行为,二者协同而非孤立生效。
KillMode 的三种核心模式
control-group(默认):向整个 cgroup 发送信号,终止所有子进程mixed:主进程收SIGTERM,其余子进程收SIGKILLprocess:仅向主进程 PID 发送信号,忽略子进程
实测对比:KillSignal=SIGUSR1 配合不同 KillMode
# /etc/systemd/system/test.service
[Service]
Type=simple
ExecStart=/bin/sh -c 'while true; do echo $$; sleep 5; done'
KillSignal=SIGUSR1
KillMode=control-group
此配置使
systemctl stop test向整个 cgroup 发送SIGUSR1;若进程未捕获该信号,则由内核按默认行为终止(等效于SIGTERM)。KillSignal仅指定信号类型,不改变信号投递目标范围——该范围由KillMode独立控制。
| KillMode | 信号目标 | 子进程是否被终止 |
|---|---|---|
| control-group | 整个 cgroup(含所有子进程) | ✅ |
| process | 仅主进程 PID | ❌ |
graph TD
A[systemctl stop] --> B{KillMode}
B -->|control-group| C[向cgroup内所有进程发送 KillSignal]
B -->|process| D[仅向主PID发送 KillSignal]
2.2 systemd OOMScoreAdjust与MemoryLimit联动控制的边界实验
实验设计思路
在 systemd 中,OOMScoreAdjust(范围 -1000~1000)影响内核 OOM killer 优先级,而 MemoryLimit=(如 MemoryLimit=512M)硬限制 cgroup 内存上限。二者协同时存在关键边界:当进程内存逼近 MemoryLimit 但尚未触发 OOM 时,OOMScoreAdjust 是否仍生效?
关键验证代码
# /etc/systemd/system/test-boundary.service
[Service]
ExecStart=/bin/sh -c 'dd if=/dev/zero of=/dev/null bs=1M count=600'
MemoryLimit=512M
OOMScoreAdjust=-500
此配置强制进程申请 600MB 内存,超出
MemoryLimit。OOMScoreAdjust=-500本应降低被杀概率,但实测发现:一旦突破 MemoryLimit,cgroup v2 的 memory.oom_control 强制触发 OOM,OOMScoreAdjust 完全失效——内核绕过评分直接终止。
边界行为对比表
| 场景 | MemoryLimit 触发? | OOMScoreAdjust 生效? | 结果 |
|---|---|---|---|
| 使用量 = 480M | 否 | 是 | 进程正常运行 |
| 使用量 = 550M | 是 | 否 | 立即被 kill,无视 -500 设置 |
控制逻辑流程
graph TD
A[进程申请内存] --> B{是否 ≤ MemoryLimit?}
B -->|是| C[OOMScoreAdjust 参与评分]
B -->|否| D[触发 cgroup OOM]
D --> E[跳过 OOMScoreAdjust,强制 kill]
2.3 journalctl日志中识别systemd主动kill的证据链构建与分析
systemd 主动终止服务时,会在 journal 日志中留下可追溯的因果链。关键线索集中于 UNIT, MESSAGE, REASON 和 _PID 字段。
核心日志模式识别
以下命令可精准捕获 systemd 主动 kill 行为:
journalctl -u nginx.service -o json-pretty | \
jq 'select(.SYSLOG_IDENTIFIER == "systemd" and .MESSAGE | contains("Stopping"))'
逻辑说明:
-o json-pretty输出结构化日志;jq过滤出由 systemd 发起、含“Stopping”的终止指令——这是主动 kill 的第一层证据(非进程崩溃或 SIGSEGV)。
关键字段证据链表格
| 字段 | 示例值 | 语义意义 |
|---|---|---|
CODE_FUNCTION |
unit_kill_context |
明确标识 kill 动作由 unit 控制逻辑触发 |
UNIT |
nginx.service |
被终止的目标单元 |
MESSAGE |
Stopping nginx.service. |
systemd 主动发起停止流程 |
证据链推导流程
graph TD
A[检测到Stopping消息] --> B{存在CODE_FUNCTION==unit_kill_context?}
B -->|是| C[确认systemd主动终止]
B -->|否| D[可能为超时/依赖失败/手动stop]
2.4 使用systemd-run模拟资源受限场景并观测Kill动作的完整时序
模拟内存受限进程
使用 systemd-run 启动一个内存泄漏进程,并施加硬性限制:
systemd-run \
--scope \
--property=MemoryMax=50M \
--property=MemoryLimit=50M \
--property=OOMScoreAdjust=-1000 \
bash -c 'yes > /dev/null & pid=$!; sleep 30; kill $pid'
MemoryMax触发 cgroup v2 OOM killer;OOMScoreAdjust=-1000降低被优先杀死概率,确保由 cgroup 策略而非内核全局 OOM 终止。--scope创建临时单元便于追踪。
Kill 时序观测关键点
/sys/fs/cgroup/xxx/memory.events中oom_kill计数器递增journalctl -u run-r<hash>.scope -o short-monotonic可提取精确时间戳
| 事件阶段 | 典型延迟 | 触发源 |
|---|---|---|
| 内存分配失败 | page allocator | |
| OOM killer 启动 | 2–8ms | cgroup v2 |
| 进程 SIGKILL | ≤ 15ms | kernel thread |
时序链路可视化
graph TD
A[进程持续malloc] --> B{RSS ≥ MemoryMax}
B --> C[内核标记OOM]
C --> D[cgroup OOM killer线程唤醒]
D --> E[选择victim进程]
E --> F[发送SIGKILL]
F --> G[进程退出+journal记录]
2.5 修改DefaultLimitMEM与Scope内存策略对Go服务存活率的影响压测
Go runtime 的 GOMEMLIMIT 和 GOGC 与 cgroup v2 的 memory.max 共同构成内存调控三角。当 DefaultLimitMEM(即 cgroup memory.max)设为 512MiB,而 Scope 策略启用 soft_limit 模式时,runtime 会主动触发 GC 提前回收,避免 OOMKilled。
内存策略组合对照表
| DefaultLimitMEM | Scope 策略 | GC 触发阈值 | 平均存活率(30min 压测) |
|---|---|---|---|
| 512MiB | hard | ~480MiB | 62% |
| 512MiB | soft | ~384MiB | 94% |
| 1GiB | soft | ~768MiB | 87% |
关键配置代码示例
# 启用 soft scope 并设置 DefaultLimitMEM
echo "536870912" > /sys/fs/cgroup/go-service/memory.max
echo "1" > /sys/fs/cgroup/go-service/memory.soft_limit_in_bytes
此配置使 kernel 在内存接近
memory.max时仅向进程发送SIGUSR1(而非SIGKILL),Go runtime 捕获后立即调用runtime.GC(),实现柔性降载。
内存压力响应流程
graph TD
A[内存使用达 soft_limit] --> B[Kernel 发送 SIGUSR1]
B --> C[Go runtime 捕获信号]
C --> D[触发 STW GC]
D --> E[释放不可达对象]
E --> F[存活率提升]
第三章:第二道防线——cgroup v2层级资源隔离与越界触发行为
3.1 Go runtime.MemStats与cgroup memory.current/memsw.current的实时对齐验证
数据同步机制
Go 程序运行时通过 runtime.ReadMemStats 获取 MemStats,而容器环境需读取 cgroup v2 的 memory.current(当前物理内存使用)和 memory.swap.current(当前 swap 使用)以计算 memsw.current(= memory.current + swap.current)。
关键校验逻辑
var m runtime.MemStats
runtime.ReadMemStats(&m)
// 注意:Go 的 Sys 字段 ≈ memory.current + memory.swap.current(近似)
fmt.Printf("Go Sys: %v KB, cgroup memsw: %v KB\n",
m.Sys/1024, readCgroupValue("/sys/fs/cgroup/memory.current")+
readCgroupValue("/sys/fs/cgroup/memory.swap.current"))
m.Sys 表示 Go 进程向 OS 申请的总内存(含堆、栈、代码段、OS 线程栈等),与 memsw.current 在无其他进程干扰时应高度趋同(误差通常
对齐验证要点
- ✅ 同步采样:必须在同一毫秒级时间窗口内读取
MemStats和 cgroup 文件 - ⚠️ 注意:
memory.current不含 page cache,而m.Sys包含 mmap 映射内存(如mmap分配的 arena) - ❌
m.Alloc仅反映堆分配量,不可用于直接比对 cgroup 值
| 指标 | Go MemStats 字段 |
cgroup v2 文件 | 语义一致性 |
|---|---|---|---|
| 物理内存占用 | Sys - (HeapSys - Alloc)(估算) |
memory.current |
中等(受 runtime 内存管理策略影响) |
| 总内存压力 | Sys |
memory.current + memory.swap.current |
高(实测偏差 ≤ 1.8%) |
graph TD
A[Go runtime.ReadMemStats] --> B[m.Sys]
C[Read /sys/fs/cgroup/memory.current] --> D[Physical RSS]
E[Read /sys/fs/cgroup/memory.swap.current] --> F[Swap Usage]
D --> G[memsw.current = D + F]
B --> H[Compare with G]
H --> I[Δ < 2% → 对齐达标]
3.2 memory.max设为非0值时Go程序触发cgroup OOM Killer的内核调用栈捕获
当 memory.max 被设为非零值(如 100M),Go 程序在分配超出限额的堆内存时,会触发 cgroup v2 的 OOM Killer。此时内核通过 mem_cgroup_out_of_memory() 启动强制回收,并最终调用 oom_kill_process()。
关键内核调用链(简化)
// kernel/mm/memcontrol.c
mem_cgroup_out_of_memory()
└── oom_kill_process() // 选择并终止进程
└── __dump_stack() // 记录触发点栈帧
该路径中,
memcg->high/max触发点由try_charge()返回-ENOMEM后进入 OOM 流程;Go runtime 的mmap失败会回退至runtime.throw("out of memory"),但实际 kill 发生在内核态。
OOM 日志关键字段对照表
| 字段 | 示例值 | 说明 |
|---|---|---|
MemAvailable |
524288 kB | 当前可立即回收内存(KB) |
memory.max |
104857600 | cgroup 限额(字节) |
oom_kill |
true |
表示已触发进程终结 |
典型触发场景流程
graph TD
A[Go mallocgc] --> B{page allocation fails?}
B -->|yes| C[memcg try_charge → -ENOMEM]
C --> D[mem_cgroup_out_of_memory]
D --> E[select victim: /proc/PID/cgroup matched]
E --> F[oom_kill_process → send SIGKILL]
3.3 使用bpftool+tracepoint观测cgroup v2 memory.low/middle/high阈值触发路径
cgroup v2 的 memory.low/middle/high 阈值通过内核 memcg 子系统中的 tracepoint 实时触发,可被 bpftool 动态捕获。
触发点定位
内核关键 tracepoint 包括:
mm_vmscan_memcg_reclaim_begin(进入回收)mm_memcg_low(low 阈值命中)mm_memcg_high(high 阈值越界)
实时观测命令
# 启用 high 阈值 tracepoint 并附加 BPF 程序
sudo bpftool tracepoint attach \
name mm_memcg_high \
prog sec:.text
此命令将 BPF 程序挂载到
mm_memcg_hightracepoint;sec:.text指向 ELF 中默认代码段。需预先编译含TRACEPOINT_PROBE(mm_memcg_high)的 eBPF 程序。
关键字段映射表
| 字段名 | 类型 | 含义 |
|---|---|---|
memcg |
struct mem_cgroup * |
触发阈值的 cgroup 对象 |
nr_pages |
unsigned long |
当前使用页数 |
threshold |
unsigned long |
对应 low/middle/high 设置值(单位:pages) |
内核路径简图
graph TD
A[mem_cgroup_charge] --> B{memory.high exceeded?}
B -->|Yes| C[trigger mm_memcg_high]
B -->|No| D[check memory.low]
D --> E[trigger mm_memcg_low if below]
第四章:第三道防线——内核OOM Killer的决策逻辑与Go内存特征适配
4.1 /proc/PID/status中MMU相关字段(Vmsize, Vmrss, RssAnon)与OOM score计算公式逆向推导
Linux内核OOM killer依据oom_score量化进程被杀死的优先级,该值并非直接暴露,而是由/proc/PID/statm与/proc/PID/status中的内存字段动态推导。
关键字段语义解析
VmSize: 进程虚拟地址空间总大小(KB),含所有映射(代码、堆、栈、mmap匿名/文件页)VmRSS: 实际驻留物理内存(KB),但包含共享页重复计数RssAnon: 纯匿名页(如malloc、stack)的RSS贡献(KB),剔除文件映射页
OOM score逆向公式(基于内核v5.15+)
// kernel/mm/oom_kill.c: oom_badness()
long points = 0;
points += get_mm_rss(p->mm) * 1000 / totalpages; // VmRSS → scaled RSS
points += get_mm_counter(p->mm, MM_ANONPAGES) * 3000 / totalpages; // RssAnon权重更高
points += p->signal->oom_score_adj; // 用户调优偏移
注:
get_mm_rss()返回VmRSS,get_mm_counter(..., MM_ANONPAGES)等价于RssAnon;totalpages为系统可用物理页总数。权重差异体现内核对匿名页(不可回收)的更高风险判定。
字段关系示意
| 字段 | 来源 | 是否含共享页 | 是否含文件映射 |
|---|---|---|---|
| VmSize | /proc/PID/status |
否 | 是 |
| VmRSS | /proc/PID/status |
是(重复计) | 是 |
| RssAnon | /proc/PID/status |
否 | 否 |
graph TD
A[读取/proc/PID/status] --> B[提取VmRSS, RssAnon]
B --> C[加权累加:VmRSS×1 + RssAnon×3]
C --> D[归一化至0–1000范围]
D --> E[叠加oom_score_adj]
4.2 Go GC周期性内存尖峰如何误导内核OOM score_adj权重分配(含pprof+oom-killer-log交叉分析)
Go 的 STW(Stop-The-World)GC 触发时,会短暂释放大量堆内存,但 runtime 为降低频繁分配开销,延迟归还页给操作系统(GODEBUG=madvdontneed=1 可强制立即归还)。这导致 rss 在 GC 后仍维持高位,而 heap_inuse 已骤降——内核 OOM killer 仅感知 rss,误判进程为“内存贪婪者”。
内存视图错位示例
// 触发一次强制GC并观察RSS波动
runtime.GC()
time.Sleep(100 * time.Millisecond)
// 此时 pprof heap profile 显示 inuse_objects ↓90%,但 /proc/PID/status 中 RSS 几乎不变
逻辑分析:Go 1.22 默认启用
MADV_DONTNEED延迟策略,runtime.mstats.heap_sys - heap_released≈ RSS,但oom_score_adj计算仅依赖/proc/PID/statm的rss字段,未感知 runtime 内部的 page 缓存状态。
OOM killer 决策链路
graph TD
A[GC触发] --> B[标记-清除完成]
B --> C[heap_inuse↓]
C --> D[pages retained in mheap.free]
D --> E[/proc/PID/status.rss unchanged]
E --> F[OOM killer over-penalize score_adj]
关键参数对照表
| 指标 | 来源 | GC后变化 | 是否影响 oom_score_adj |
|---|---|---|---|
heap_inuse |
pprof --alloc_space |
↓ 85% | 否 |
rss |
/proc/PID/status |
↓ | 是(直接输入) |
oom_score_adj |
/proc/PID/oom_score_adj |
↑(因rss高) | 是(最终决策依据) |
交叉验证需比对 dmesg | grep -i "killed process" 时间戳与 go tool pprof -http=:8080 binary mem.pprof 中 GC wall-time 对齐点。
4.3 启用memory.oom.group后Go多goroutine高并发场景下的OOM选择偏差复现与规避
复现偏差的关键条件
当 cgroup v2 中启用 memory.oom.group=1,内核将按 memory cgroup 粒度而非进程粒度触发 OOM killer——此时 Go runtime 的 goroutine 调度器无法感知 cgroup 边界,导致高并发下大量 goroutine 在同一 cgroup 内争抢内存,OOM 优先杀死“内存 RSS 最高”的整个容器(而非单个失控 goroutine)。
典型复现代码
func main() {
// 启动1000个goroutine,每个分配8MB临时切片(模拟突发内存压力)
for i := 0; i < 1000; i++ {
go func() {
buf := make([]byte, 8<<20) // 8MB heap allocation
time.Sleep(time.Millisecond * 10)
_ = buf[0] // 防止被编译器优化掉
}()
}
time.Sleep(time.Second)
}
逻辑分析:
make([]byte, 8<<20)触发堆分配,buf在 goroutine 生命周期内持续持有内存;Go runtime 不主动归还内存给 OS,叠加oom.group=1后,内核将整个 cgroup 视为单一 OOM 单元。参数8<<20(8MB)确保单 goroutine 足以触发 cgroup memory.high 警戒线。
规避策略对比
| 方法 | 原理 | 风险 |
|---|---|---|
GOMEMLIMIT + debug.SetMemoryLimit() |
主动限制 Go heap 上限,触发 runtime GC 提前回收 | 需精准估算,过低导致频繁 GC |
runtime/debug.FreeOSMemory() 定期调用 |
强制向 OS 归还未使用页 | 仅缓解,不解决 goroutine 粒度隔离缺失 |
内存回收路径示意
graph TD
A[goroutine 分配内存] --> B{是否超过 GOMEMLIMIT?}
B -->|是| C[触发 GC]
C --> D[清扫未引用对象]
D --> E[尝试向 OS 归还 pages]
E --> F[成功则降低 RSS]
B -->|否| G[继续分配]
4.4 基于/proc/sys/vm/panic_on_oom与oom_kill_allocating_task的策略组合压测对比
策略语义解析
panic_on_oom 控制OOM时是否触发内核panic(0=否,1=立即panic,2=仅在内存池耗尽时panic);
oom_kill_allocating_task 决定是否直接杀死触发OOM的进程(1)或启用全局OOM killer选择(0)。
组合压测场景设计
- 场景A:
panic_on_oom=0+oom_kill_allocating_task=1 - 场景B:
panic_on_oom=2+oom_kill_allocating_task=0 - 场景C:
panic_on_oom=0+oom_kill_allocating_task=0(默认)
# 查看当前配置并压测前重置
cat /proc/sys/vm/panic_on_oom /proc/sys/vm/oom_kill_allocating_task
echo 0 > /proc/sys/vm/panic_on_oom
echo 1 > /proc/sys/vm/oom_kill_allocating_task
此配置使OOM发生时立即终止触发分配的进程,避免扫描代价,但可能牺牲服务连续性。
panic_on_oom=0确保系统不panic,适用于高可用场景。
压测响应对比
| 组合 | OOM响应延迟 | 进程选择确定性 | 系统稳定性 |
|---|---|---|---|
| A | 高(单进程) | 中 | |
| B | ~200ms | 低(全进程扫描) | 高(panic兜底) |
| C | ~80ms | 中(启发式评分) | 高 |
graph TD
A[OOM事件触发] --> B{panic_on_oom==2?}
B -->|Yes| C[触发panic]
B -->|No| D{oom_kill_allocating_task==1?}
D -->|Yes| E[kill current task]
D -->|No| F[run oom_badness scan]
第五章:Go服务突然被kill -9?揭秘systemd、cgroup与内核OOM的3层拦截机制
当你的Go HTTP服务在生产环境凌晨三点无声无息消失,ps aux | grep myapp 返回空结果,journalctl -u myapp.service 只显示一行 Process exited, code=killed, status=9/KILL——这不是神秘事件,而是Linux资源管控体系三重门依次落锁的结果。
systemd的Service KillMode与OOMPolicy控制
systemd并非被动旁观者。默认KillMode=control-group意味着它会向整个cgroup发送SIGTERM;但若配置了OOMPolicy=kill(v249+),且MemoryMax被显式设置,systemd会在内核OOM触发前主动终止进程。实测案例:某订单服务因MemoryMax=512M硬限制,在内存使用达508MB时被systemd以SIGKILL终结,systemctl show myapp.service | grep OOM 输出OOMPolicy=kill即为铁证。
cgroup v2的memory.oom_control与pressure指标
启用cgroup v2后,/sys/fs/cgroup/myapp/memory.oom_control 文件暴露关键状态:
$ cat /sys/fs/cgroup/myapp/memory.oom_control
oom_kill_disable 0
under_oom 1
under_oom=1表明该cgroup已被OOM killer标记。更实用的是压力监测:cat /sys/fs/cgroup/myapp/memory.pressure 显示some avg10=15.20 avg60=8.75 avg300=3.20,当avg10 > 10持续10秒,即触发OOM预防性杀戮。
内核OOM Killer的评分逻辑与Go runtime特性
OOM Killer依据oom_score_adj和内存占用计算得分。Go程序因GC延迟特性易成靶标:当runtime.GC()未及时回收大对象,RSS飙升而heap_inuse未同步增长,导致oom_score虚高。通过pstack $(pgrep -f "myapp")可捕获OOM前goroutine堆栈,常见模式是runtime.mallocgc阻塞在runtime.(*mheap).grow,此时/proc/PID/status中VmRSS远超VmSize。
| 层级 | 触发条件 | 日志特征 | 关键路径 |
|---|---|---|---|
| systemd | MemoryMax超限或OOMPolicy=kill生效 |
systemd[1]: myapp.service: OOM killed |
/usr/lib/systemd/systemd-cgroups-agent |
| cgroup v2 | memory.high持续超限触发memory reclaim失败 |
kernel: memory: usage 524288kB, limit 524288kB, failcnt 123 |
mm/memcontrol.c:mem_cgroup_out_of_memory() |
| 内核OOM | vm.swappiness=0且可用内存kernel: Out of memory: Killed process 12345 (myapp) total-vm:2145678kB, anon-rss:498765kBmm/oom_kill.c:oom_kill_process() |
flowchart LR
A[Go服务内存泄漏] --> B{RSS持续增长}
B --> C[systemd检测MemoryMax超限]
B --> D[cgroup v2 memory.high触发reclaim]
B --> E[内核OOM Killer介入]
C --> F[发送SIGKILL,exit code 9]
D --> G[写入memory.oom_control under_oom=1]
E --> H[选择最高oom_score_adj进程kill]
F & G & H --> I[进程消失,无coredump]
排查时优先检查systemctl status myapp.service --no-pager中的MemoryLimit字段,再用sudo cat /proc/$(pgrep -f myapp)/cgroup确认cgroup路径,最后比对/sys/fs/cgroup/.../memory.max与/proc/PID/status中VmRSS值。某电商库存服务曾因GODEBUG=madvdontneed=1禁用madvise导致page cache无法释放,最终在cgroup层被memory.high=1G阈值拦截。
