上一篇 下一篇 分享链接 返回 返回顶部

如何在香港服务器的CentOS 8中优化内存回收机制(slab reclaim),避免高负载时的OOM Killer问题?

发布人:Minchunlin 发布时间:2025-08-24 09:33 阅读量:556


凌晨 2:07,我在葵涌机房的冷风口蹲着看串口日志。值班电话里同事说:“又被 OOM 杀了,这次是 Nginx 的一个 worker。”

那台机器的配置其实不差:

  • 位置:香港机房(单路供电,A/B 两路网络)
  • 服务器:Dell R740xd,CPU 2×Xeon Silver 4210R,内存 256 GB,系统盘 RAID1(SAS 480 GB),数据盘 2×NVMe(1.6 TB,软 RAID0),网口 2×10 GbE
  • OS:CentOS 8 (kernel 4.18.x,SLUB allocator)

业务:Nginx + 两个 Go 服务(大量小文件元数据访问,目录遍历密集),旁边还有 rsync/备份任务扫盘

从 dmesg 和 journalctl -k 里能看出典型的 OOM 现场:Out of memory: Killed process ... (nginx) total-vm:..., anon-rss:...。
但奇怪的是,free -h 显示的“已用内存”很高,buff/cache 也不低;/proc/meminfo 的 SReclaimable 出奇地大。这意味着大量内存卡在 slab(尤其是 dentry/inode)里,回收不够积极,高峰期就把业务顶进了 OOM。

一、问题画像:怎么确认是 slab/目录项缓存顶爆的?

先把现场证据采全:

# 1) 总览
free -h
cat /proc/meminfo | egrep 'MemTotal|MemFree|MemAvailable|Slab|SReclaimable|SUnreclaim'

# 2) 哪些 slab 在长大
slabtop -o -s c | head -40     # 按 cache 名称排序,常见 dentry、inode_cache、kmalloc-* 等

# 3) VFS 回收倾向
sysctl vm.vfs_cache_pressure

# 4) 压力观察(PSI)
cat /proc/pressure/memory

一次高峰采样(真实机器上的量级,略做脱敏):

指标 数值(Before) 备注
MemTotal 256 GB 物理内存
MemAvailable 6.1 GB 危险地偏低
Slab 34.8 GB slab 总量
SReclaimable 27.9 GB 可回收的 slab(重点)
SUnreclaim 6.9 GB 不可回收 slab
vm.vfs_cache_pressure 100 默认值
PSI memory some avg10 21.3% 10 分钟内“有人等内存”的时间占比
slabtop 前 3 dentry, inode_cache, kmalloc-64 dentry/inode 远高于平日

结论:SReclaimable 巨大、PSI 告警,目录项/索引节点缓存(dentry/inode)在高并发目录遍历下快速膨胀,shrinker 回收节奏跟不上,导致可用内存见底,触发 OOM Killer。

二、原理飞线图(快速但不浅)

  • slab(SLUB):内核用来管理小对象(dentry/inode/kmalloc-*)的分配器。
  • SReclaimable / SUnreclaim:可回收/不可回收 slab,前者在压力大时可被 shrinker 回收。
  • VFS 缓存:dentry/inode cache 大量存在于 slab;查找目录、小文件遍历会狂增。
  • shrinkers:内核在内存紧张时调用 shrinker 回收可回收对象。
  • vm.vfs_cache_pressure:告诉内核“对 VFS 类缓存(dentry/inode)回收要多积极还是保守”。默认 100;越大越积极。
  • PSI(Pressure Stall Information):内核暴露的资源压力“卡顿占比”。用它能更精确地在“快要出事”而不是“已经出事”时触发措施。

三、先止血:两段式应急回收

生产上,不要一上来就 echo 3 > /proc/sys/vm/drop_caches(那会同时丢 pagecache,容易引发抖动)。我一般分两步:

应急 Step 1:仅回收 dentry/inode

# 仅丢弃 dentries 与 inodes,不动 pagecache
sync
echo 2 > /proc/sys/vm/drop_caches

应急 Step 2:定向 shrink 某些 slab cache(更细粒度)

SLUB 下每个 cache 都有一个 shrink 接口:

# 观察有哪些 cache
ls /sys/kernel/slab | egrep 'dentry|inode'

# 触发 shrink(对每个 cache 写入 1 即可请求回收)
echo 1 > /sys/kernel/slab/dentry/shrink
echo 1 > /sys/kernel/slab/inode_cache/shrink

这两步把我们机器的 SReclaimable 从 28 GB 级别拉回到 7 GB 左右,业务勉强挺住了。当夜我没有重启任何服务。

四、系统化优化:让“险情”变成“自愈”

4.1 Sysctl:让内核更愿意回收 VFS 缓存

我在这台 256 GB 内存的机器上用的是下面这组(按需调整):

/etc/sysctl.d/99-mem-vfs.conf

# 让 VFS 缓存(dentry/inode)更积极进入回收
vm.vfs_cache_pressure = 250

# 保持更充裕的“自由水位”,避免频繁进入直接回收
# 建议设为内存的 0.5% ~ 1.5%,我这里 1.0% ≈ 2621 MB
vm.min_free_kbytes = 2680000

# 有 swap 就让它更保守点用,避免匿名页过早换出(视业务决定)
vm.swappiness = 10

# 写回阈值,防止积压太多脏页导致内存被脏页占满
vm.dirty_background_ratio = 5
vm.dirty_ratio = 20

生效并复核:

sysctl --system
sysctl vm.vfs_cache_pressure vm.min_free_kbytes vm.swappiness vm.dirty_ratio vm.dirty_background_ratio

经验:vfs_cache_pressure 我在 150300 之间试了几轮。目录遍历型负载偏大时,200300 更稳,但会略微增加元数据命中失效率(多一次磁盘/SSD 访问),要结合业务的 IOPS 裁剪。

4.2 cgroup 限流:别让单个服务把全机拖死

CentOS 8 上 systemd 支持 cgroup v2(视你的 Docker/容器方案,可能在 hybrid/v1)。我们用 systemd slice 对“巡检/备份类”的进程设 MemoryHigh/MemoryMax,让它们先被内核温柔地“挤一挤”,而不是把全机推向 OOM:

示例:限制 backup.service:

# /etc/systemd/system/backup.service.d/override.conf
[Service]
MemoryHigh=32G
MemoryMax=48G
# oom 发生时整组杀,别误杀关键业务的单个线程
OOMPolicy=kill

应用:

systemctl daemon-reload
systemctl restart backup.service
systemctl show backup.service | egrep 'MemoryHigh|MemoryMax|OOMPolicy'

MemoryHigh 会触发回收与压制,让进程群体减速;MemoryMax 是硬上限,防止失控。这样做的副作用是备份速度可能下降,但换来全机稳定。

4.3 PSI 触发的自动 slab 回收(推荐实战)

我做了一个轻量的守护脚本:当 PSI memory some 在指定窗口内持续超过阈值,先定向 shrink dentry/inode,若缓解不够,再执行 drop_caches=2。脚本避免了“无脑每分钟清 cache”的粗暴方案。

/usr/local/sbin/slab-reclaimer.sh

#!/usr/bin/env bash
# 简易 PSI 触发器 + 定向 slab 回收
# 适配 CentOS 8 / SLUB

set -euo pipefail

PSI_FILE="/proc/pressure/memory"
# 触发阈值:'some avg10' 超过 12% 视为系统处于持续内存压力
THRESHOLD=12.0
LOGTAG="slab-reclaimer"

get_psi() {
  # 输出 some avg10 的浮点数
  awk '/some/ {for(i=1;i<=NF;i++){ if($i ~ /^avg10=/){ split($i,a,"="); print a[2]; break; }}}' "$PSI_FILE"
}

shrink_slab() {
  for c in dentry inode_cache; do
    local p="/sys/kernel/slab/${c}/shrink"
    if [[ -w "$p" ]]; then
      echo 1 > "$p"
      logger -t "$LOGTAG" "shrink ${c} done"
    fi
  done
}

drop_dentry_inode() {
  sync
  echo 2 > /proc/sys/vm/drop_caches
  logger -t "$LOGTAG" "drop_caches=2"
}

main() {
  local psi
  psi=$(get_psi)
  # 去掉小数比较
  psi_int=${psi%.*}
  if (( $(echo "$psi > $THRESHOLD" | bc -l) )); then
    logger -t "$LOGTAG" "PSI some avg10=${psi} > ${THRESHOLD}, start reclaim"
    shrink_slab
    sleep 2
    # 再看一眼 PSI,如果仍然很高再丢 dentry/inode
    psi2=$(get_psi)
    if (( $(echo "$psi2 > $THRESHOLD" | bc -l) )); then
      drop_dentry_inode
    fi
  fi
}

main

Systemd 定时器每 30 秒跑一次:

/etc/systemd/system/slab-reclaimer.service

[Unit]
Description=PSI-driven slab reclaimer

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/slab-reclaimer.sh
/etc/systemd/system/slab-reclaimer.timer

[Unit]
Description=Run slab reclaimer periodically

[Timer]
OnBootSec=1min
OnUnitActiveSec=30s
AccuracySec=5s

[Install]
WantedBy=timers.target

启用:

chmod +x /usr/local/sbin/slab-reclaimer.sh
systemctl daemon-reload
systemctl enable --now slab-reclaimer.timer
systemctl list-timers | grep slab-reclaimer
journalctl -t slab-reclaimer -f

这个组合上线后,我基本没再看见 OOM;同时避免了过度清理 pagecache 带来的读放大。

4.4 旁路优化(可选但有效)

透明大页(THP):对我们这类大量小对象/随机访问负载,通常关闭更稳。

echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

持久化放在 /etc/rc.d/rc.local 并 chmod +x。

  • 确认未启用 slab_nomerge:防止 SLAB cache 合并被禁止导致内存碎片增大(CentOS 8 默认不加,检查内核参数)。
  • tuned:选择 throughput-performance,获得合理的 VM/IO 基线(再叠加我们自定义的 sysctl)。
  • tuned-adm profile throughput-performance

五、验证:对比指标与业务表现

调优/上线一周后的复盘数据(同一批量级业务,取高峰均值):

指标/现象 Before(调优前) After(调优后) 变化
MemAvailable 6.1 GB 42.7 GB ↑ 更安全的余量
Slab 34.8 GB 12.3 GB
SReclaimable 27.9 GB 6.6 GB ↓ 大幅压缩
PSI memory some avg10 21.3% 3.4% ↓ 几乎消失
OOM 事件/天 2~3 次 0 清零
Nginx P99 延迟(静态小文件) 62 ms 54 ms 稍有改善(更稳定)
NVMe 读取 IOPS 波动(±) 35% 12% 波动显著降低

六、上线踩坑与排障细节

vfs_cache_pressure 不是越大越好

设置到 400 以上时我们看到元数据命中率下降,导致 NVMe IOPS 波动加大;最终稳定在 250 比较折中。

min_free_kbytes 设置不当会“挤”业务

设太大(>2% 内存)会让系统常态下更频繁做回收,反而引发抖动;建议从 0.5%~1.0% 试。

定时器频率

30 s 一次基本够用;10 s 太频繁会让 PSI 刚回落又被触发,出现锯齿。PSI 是慢指标,用 avg10 更稳。

容器 cgroup 版本坑

Docker 老版本在 cgroup v1 上,MemoryHigh 不生效(那是 v2 的概念)。这时要用 --memory+--memory-swap 做硬限,或升级到支持 cgroup v2 的版本/配置 systemd unified。

drop_caches 的误用

不要把 echo 3(顺带清 pagecache)当成日常保洁;我们优先 定向 shrink + drop_caches=2,避免缓存抖动。

打开文件句柄会“钉住”对象

某些 slab 项被正在使用的对象引用,不可回收。那次发现一个内部巡检脚本递归 stat 后没及时释放句柄,导致 inode_cache 局部回收不动——改脚本后恢复。

备份/巡检的限速

给 rsync/自研扫描器加了 I/O 和并发上限(--bwlimit、goroutine 限制),配合 MemoryHigh,从源头上减轻 slab 膨胀速度。

七、可直接复用的落地清单(Checklist)

  •  抓取基线:free -h、/proc/meminfo、slabtop、PSI
  •  应急两步走:shrink dentry/inode → drop_caches=2
  •  sysctl:vfs_cache_pressure=150~300、min_free_kbytes≈0.5%~1.0%RAM、swappiness=10、dirty_* 合理化
  •  cgroup 限制高风险任务的 MemoryHigh/MemoryMax(或容器内 --memory)
  •  上线 PSI 触发的 slab-reclaimer 定时器(30 s)
  •  可选:关闭 THP、确认未 slab_nomerge、tuned profile
  •  一周复盘:观察 SReclaimable、PSI、P99、OOM 计数

一周后我又在同一条走廊走过。空调还是那么冷,但告警安静了。那台机器的 SReclaimable 常驻在 6~8 GB,PSI 几乎贴底。
我记得第一次处理这类问题的时候,总想着“一键清缓存”。后来才知道,真正的稳定不是“把水桶倒空”,而是“让水在桶里流动起来并且不溢出”。这也是我写下这些步骤的原因——在 CentOS 8 上,把 slab reclaim 做成体系化的“提前回收 + 柔性限流 + 事件触发”,才能在高负载时,稳稳地躲开 OOM Killer 的镰刀。

如果你的场景和我类似,照着做,一般一晚上就能见效;如果你的负载偏计算/大内存匿名页,再来聊聊另一套“匿名页为主”的打法。

目录结构
全文