Linux内存回收机制与OOM排查实战
发布日期: 2026/08/18 阅读总量: 0

事故现场:一次把我从凌晨3点拉起来的故障

2024年6月14日凌晨2:47,值班手机连续弹出12条告警。宿主机IP 10.12.3.18(CentOS 7.9,内核3.10.0-1160,128G内存)上的MySQL 5.7.38从库实例挂了。

SSH登上去发现SSH能连但任何命令都卡死超过3秒才能返回,MySQL进程已经消失。查看dmesg:

[2846179.234601] Out of memory: Kill process 16283 (mysqld) score 932 or sacrifice child
[2846179.234605] Killed process 16283 (mysqld) total-vm:28934716kB, anon-rss:106611384kB, file-rss:2240kB, shmem-rss:0kB
[2846179.234606] oom_reaper: reaped process 16283 (mysqld), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

注意anon-rss:106GB,这个从库分配这么多匿名内存。但这台机器128G内存,当时还跑了主库、Redis、监控agent,其他进程加一起也就8G左右。理论上不该OOM。

最后查明白:这台机器跑了一周之后,page cache被只读查询的预读机制吃到67G,刚好遇到dirty pages写盘高峰,直接触发OOM。问题出在内存回收逻辑,不是MySQL本身。

这篇文章把从原理到排查、从参数调到内核观测的完整过程写清楚。文章涉及的内核版本是3.10.0-1160(CentOS 7.9)和5.15.0(Ubuntu 22.04),MySQL版本5.7.38,压测工具sysbench 1.0.20,内核监控工具bpftrace 0.18.1。

Linux内存管理的核心:不是"不够用",是"回收不掉"

你们可能听过这种说法:Linux内存管理就是把物理内存分给进程。对,但这只是最表层的事。

内存到底去哪儿了

Linux物理内存只有三块流向:

  • 用户态匿名内存(anon_rss):进程自己的堆、栈、映射页面。MySQL的buffer pool大部分属于这类。
  • 内核本身:内核栈、slab这些,通常占不到2G。
  • page cache:管理磁盘文件缓存,读过的文件都会留在里面,理论上可以被回收。

数据从磁盘读出来不会马上丢,缓存在page cache里给后续使用。所以一台机器跑得越久,page cache占的内存越大,这属于正常情况。

问题的关键是:page cache是"可回收"内存,为什么在OOM的时候没有先回收它?

内存回收的触发机制

Linux内核在每个内存节点上维护水位(watermark):

$ cat /proc/zoneinfo | grep -A 5 "Node 0, zone   Normal"
Node 0, zone   Normal
  pages free     84632
        boost    0
        min      38726
        low      48407
        high     58088
        spanned  3114240
        present  3114240
        managed  3019520

水位含义:

  • min:最低保障水位,free pages低于这个值会进行直接回收
  • low:当free pages低于low水位,kswapd内核线程启动异步回收
  • high:kswapd回收的目标,回收到high水位就停止

每个zone的min水位计算公式在Linux 3.10里是:

min_free_kbytes = sqrt(lowmem_kbytes * 16)
zone_protection (各zone按比例分配)

128G内存的机器默认min_free_kbytes是67584,也就是说系统至少保留约67584KB的free pages,实际因为zone protection,顶部zone(DMA32/Normal)可用的free空间远比这个大。

每次物理页面分配有三个路径(Linux 3.10内核 mm/page_alloc.c):

// 伪代码,展示分配逻辑
static struct page *get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask,
        unsigned int order, int alloc_flags)
{
    // 判断水位
    if (zone_watermark_ok(zone, order, mark, alloc_flags))
        return buffered_rmqueue(zone, order, gfp_mask); // 水位OK,直接分配
    
    // 水位不够,先走慢路径
    if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
        // 唤醒kswapd异步回收
        wake_all_kswapds(order, gfp_mask, zone);
        // 如果仍然失败,进入direct reclaim
        if (!zone_watermark_ok(zone, order, min, alloc_flags))
            return __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, &did_some_progress);
    }
}

重点在慢路径(slowpath)。当内存分配无法从水位OK的zone直接拿到页面时,Linux进入慢路径,这个路径在内存快耗尽时可能表现为三个分支:

  1. 异步回收(wakeup_kswapd):唤醒kswapd内核线程异步回收page cache,不阻塞当前分配者。这是最常见的回收方式。
  2. 直接回收(direct reclaim):如果异步回收跟不上,当前进程进入同步回收路径,进程卡在内存分配上。系统变慢,看起来像"假死"。
  3. OOM Killer:直接回收也搞不到内存,触发OOM,干掉一个进程释放内存。

这里有个致命设计:kswapd回收时优先回收page cache,因为page cache属于"干净可回收"的页面。但如果page cache里有大量dirty pages(脏页),kswapd必须先把这些脏页写回磁盘,这个IO过程极慢,远慢于应用分配内存的速度。

在3.10内核中,dirty pages的回收逻辑大概是这样:

// mm/vmscan.c shrink_page_list 中处理脏页的逻辑(简化版)
if (PageDirty(page)) {
    // 若页面被文件系统映射,需要先写回
    if (page_is_file_cache(page) && mapping) {
        // 问题:如果此页面正在被IO写回,这里会等待
        wait_on_page_writeback(page);
        // 释放page cache,计入可回收
    }
    // 若为匿名页,需要swap-out
    else {
        if (!add_to_swap(page)) {
            // swap空间不足,直接跳过
            goto keep_locked;
        }
    }
}

3.10内核对脏页回收有两个特点,在内存紧张时是灾难:

  • wait_on_page_writeback()是同步等待,这意味着进程在内存分配路径上直接等待磁盘IO完成
  • 如果swap分区或swapfile空间不足,匿名页swap-out失败,只能跳过这些页面

在CentOS 7.9的3.10内核里,这个等待发生得比较频繁(4.x/5.x内核对这个路径做了明显的并发优化)。

两个参数决定了dirty pages的回收时机

# 数据来自出问题那台机器
$ sysctl vm.dirty_ratio
vm.dirty_ratio = 30
$ sysctl vm.dirty_background_ratio
vm.dirty_background_ratio = 10
  • vm.dirty_background_ratio = 10:dirty pages占内存总数10%(约12.8G)时,内核pdflush/flusher线程开始后台写回。后台写回不阻塞应用。
  • vm.dirty_ratio = 30:dirty pages占内存总数30%(约38.4G)时,所有进程的写操作进入同步等待,直到脏页落盘。这个会直接卡住业务写入

这台机器上同时跑了一个批量任务(压缩备份文件往磁盘写),写文件时数据先进page cache,变成dirty pages,再通过pdflush异步刷盘。当dirty pages快接近dirty_ratio的30%时,批量任务把写IO占满,大量进程阻塞在写盘,内存分配等不到足够可回收的干净页,直接进入OOM路径,把MySQL给杀了。

方案一:设置回收倾向参数(sysctl配置)

先明白OOM场景:

MySQL的buffer pool是分配多少就常驻多少的。它即使空闲,也不会把buffer pool的内存交还给OS。当page cache(含dirty)、其他进程、MySQL自身,加起来超过物理内存时,内核就要在"回收dirty page cache"和"杀进程"之间做选择。默认选择是:先回收page cache,但如果回收速度赶不上,就杀分最高的进程。

在不改业务的情况下,可以做三件事:

第一步:降低脏页比例

# 修改 /etc/sysctl.conf
vm.dirty_background_ratio = 5
vm.dirty_ratio = 15

原理:把触发后台写回的阈值降到5%(约6.4G),让pdflush更早开始刷脏页。把同步等待的硬阈值降到15%(约19.2G),避免dirty pages堆积到38G还不触发强制写回。

效果:写盘更频繁、单次刷盘量减少,IO的峰值降低但均值上升。

这在IO压力不大时有效。但如果你本来磁盘IO就是瓶颈,降低dirty_ratio会让IO频繁刷写,整体吞吐下降。

第二步:把MySQL的buffer pool限死

修改MySQL配置(/etc/my.cnf):

[mysqld]
innodb_buffer_pool_size = 64G
innodb_buffer_pool_instances = 8
innodb_max_dirty_pages_pct_lwm = 10
innodb_max_dirty_pages_pct = 75

把buffer pool从默认的128G(或实际进程RSS 106G)限缩到64G,腾出物理内存给page cache。额外加innodb_max_dirty_pages_pct_lwm=10让InnoDB更早开始刷自己的脏页,避免内存里的InnoDB脏页长时间堆积。

第三步:swap空间调整(治标)

# 在系统盘上创建swapfile(注意别选SSD当swap,损耗快)
$ fallocate -l 32G /swapfile
$ chmod 600 /swapfile
$ mkswap /swapfile
$ swapon /swapfile
# 设置系统在free内存低于10%时才开始使用swap
$ sysctl vm.swappiness=10

效果:swap空间作为匿名页的"缓冲"。内存紧张时,内核把一部分不活跃的匿名页换到swap,给page cache腾出空间。MySQL的buffer pool是刚分配的内存页,一般不会被swap out,但其他空闲进程的页面可能被换出。

注意:如果swap落在磁盘上,换入换出是毫秒级。如果落在SSD上,好一些,但也远慢于直接读内存。不要在NFS上放swap。

方案一的整体效果数据

调整前(出事故时的状态):

指标调整前调整后(48h后观察)
dirty pages 最大量38.7GB(30%触发强制写回)7.6GB(15%触发强制写回)
page cache 平均54GB31GB
MySQL RSS106GB64GB(buffer pool限缩)
系统free内存最小值OOM前2分钟内低于2GB稳定在30GB以上
平均IO等待时间(iostat %util)92%78%
每5分钟触发OOM次数1次(最终杀进程)0次

这个方案有效,但是按"经验值"调的。说实话,把脏页阈值调低,真的一定好吗?不一定。如果是一台写多读少的机器,把dirty_ratio调低反而可能加重IO负担。所以必须上监控,看数据,不能只看"调了参数没炸就行"。

方案二:用bpf kprobe定位内存回收卡点(更精准)

如果只改参数,那跟玄学差不多。为了确定"回收到底卡在哪",用bpftrace往内核的shrink_page_list函数上加探针。

「shrink_page_list」是内核回收页面的核心函数,任何内存回收路径(kswapd、direct reclaim)最终都会走到这里。通过统计这个函数里各个分支的命中次数,可以看到回收卡在了哪一步:在等IO?在swap-out失败?在跳过脏页?

bpftrace脚本:观测内存回收详细路径

#!/usr/bin/env bpftrace
// 观察内存回收路径中的慢操作,适用于Linux 4.x/5.x/6.x
// 在CentOS 7.9(3.10内核)上需要 kprobe 挂载点,bpftrace 0.18.1

kprobe:shrink_page_list
{
    @start[tid] = nsecs;
    @calls++;
}

kretprobe:shrink_page_list
/ @start[tid] != 0 /
{
    $dur = nsecs - @start[tid];
    @dur_us = hist($dur / 1000);
    if ($dur / 1000000 > 100) {
        printf("slow shrink_page_list: %d ms\n", $dur / 1000000);
    }
    delete(@start[tid]);
}

kprobe:shrink_inactive_list
{
    @inactive_calls++;
}

kprobe:move_pages_to_lru
{
    @move_calls++;
}

// 观察dirty页在回收路径的遭遇
kprobe:wait_on_page_writeback
{
    @wait_writeback++;
    @wait_writeback_pid[tid] = comm;
}

kprobe:add_to_swap
{
    @swap_add++;
}

kprobe:swap_writepage
{
    @swap_writepage++;
}

interval:s:30 {
    print(@calls);
    print(@dur_us);
    print(@wait_writeback);
    print(@swap_writepage);
    clear(@calls); clear(@dur_us);
    clear(@wait_writeback); clear(@swap_writepage);
}

跑这个脚本的同时,模拟内存压力:

# 1. 用dd持续写一个5GB文件,制造dirty pages
dd if=/dev/urandom of=/data/testfile bs=1M count=5000 &

# 2. 用sysbench模拟MySQL查询负载,占内存
sysbench /usr/share/sysbench/oltp_read_only.lua \
    --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=sbtest \
    --mysql-password=password --tables=8 --table-size=1000000 \
    --threads=32 --time=120 --report-interval=5 run &

观察输出:

@calls: 5721
@dur_us:
[0, 1]                 418
[1, 2]                3922
[2, 4]                 824
[4, 8]                 317
[8, 16]                156
[16, 32]                73
[32, 64]                12
[64, 128]                7
[128, 256]               8
[256, 512]               6
[512, 1K]                2
@wait_writeback: 428
@swap_writepage: 0

看到wait_on_page_writeback被命中了428次,而swap_writepage是0。说明回收写回时大量页面在等待磁盘写回,swap完全没被使用。也就是说,环境里磁盘IO压力很高,回收page cache时通过writeback释放的速度跟不上内存分配。

同时看看@dur_us的分布,有8次回收耗时超过128ms,2次超过512ms。这就是"回收慢、分配快"的直接证据。

bpftrace脚本二:看看到底是谁在触发direct reclaim

#!/usr/bin/env bpftrace
// 统计谁触发了direct reclaim,方便锁定文件
kprobe:__alloc_pages_direct_reclaim
{
    @reclaim_pid[tid] = comm;
    @reclaim_count++;
}

interval:s:10 {
    print(@reclaim_count);
    print(@reclaim_pid);
    clear(@reclaim_count);
    clear(@reclaim_pid);
}

在内存压力期间,统计出来:

@reclaim_count: 23
@reclaim_pid: 
mysqld: 19
dd: 3
jbd2/dm-0-8: 1

mysqld占19次,因为innodb buffer pool的分配也是从系统内存拿页面,MySQL是最耗内存的进程,成为最频繁触发direct reclaim的进程也合理。这也说明一个事实:内存触顶时,任何进程分配内存都可能触发direct reclaim,不只是写文件的进程

完整代码实现:系统化配置方案

综合方案一和方案二的结论,给一台128G内存、跑MySQL 5.7 + Redis 6.2.6、混布批量任务的服务器做完整配置:

1. /etc/sysctl.conf 完整变更

# 内存相关 sysctl 配置(CentOS 7.9 / Ubuntu 22.04 通用)
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 3
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500

# 降低page cache回收倾向,优先回收dirty pages
vm.page-cluster = 3
vm.vfs_cache_pressure = 200

# 限制每个进程最大物理内存(取RAM 128G * 0.9 / 进程数估值,这里按mysqld:Redis:其他=8:1:1设)
# 可以通过systemd unit或ulimit实现,sysctl的overcommit也可以通过下面配置做兜底
vm.overcommit_memory = 0
vm.overcommit_ratio = 90

其中:

  • vm.dirty_expire_centisecs = 3000:dirty页面超过30秒必须写回,避免长期滞留内存。
  • vm.dirty_writeback_centisecs = 500:pdflush每5秒唤醒一次,加快刷盘节奏。
  • vm.vfs_cache_pressure = 200:让内核更激进地回收inode/dentry,减少slab内存。

生效:

$ sysctl -p

然后确认:

$ sysctl vm.dirty_ratio vm.dirty_background_ratio vm.swappiness vm.vfs_cache_pressure
vm.dirty_ratio = 15
vm.dirty_background_ratio = 3
vm.swappiness = 10
vm.vfs_cache_pressure = 200

2. MySQL 5.7 配置变更

-- 在 MySQL 中执行,确认 innodb 相关内存参数
SHOW VARIABLES LIKE 'innodb_buffer_pool_size%';
SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct%';
SHOW VARIABLES LIKE 'innodb_old_blocks_time%';

-- 建议值(在 /etc/my.cnf [mysqld] 段修改,重启MySQL生效)
-- innodb_buffer_pool_size = 64G
-- innodb_buffer_pool_instances = 8
-- innodb_max_dirty_pages_pct_lwm = 10
-- innodb_max_dirty_pages_pct = 50
-- innodb_old_blocks_time = 1000

关键点:innodb_max_dirty_pages_pct = 50,限制InnoDB buffer pool中脏页最多占50%。innodb_old_blocks_time = 1000,防止全表扫描的数据把热数据挤出buffer pool。

3. systemd 限制非核心进程内存(可选)

有些跑批任务吃内存不吐,用systemd限制一下。比如跑在daemon下的批量任务:

#/etc/systemd/system/batch-task.service.d/memory-limit.conf
[Service]
MemoryMax=8G
MemoryHigh=6G
OOMPolicy=kill
Restart=on-failure

或者不写systemd文件,直接在命令行启动时用ulimit:

# 对当前shell的进程限制
ulimit -v 8388608  # 限制虚拟内存8G
ulimit -m 8388608  # 限制RSS 8G
./my_batch_task

这样就算跑批程序内存泄漏,也不会把系统整体托死。

4. 用cgroup v2限制内存(Ubuntu 22.04 + 内核5.15)

# 给容器或systemd服务设置MemoryMax
systemctl set-property mysql.service MemoryMax=70G
systemctl set-property mysql.service MemoryHigh=60G
systemctl set-property redis.service MemoryMax=8G

# 查看当前生效值
systemctl show mysql.service | grep Memory

以上三套配置(sysctl+MySQL参数+进程/服务限制)同时生效后,机器再也没出现过OOM。两个多月的观察数据:

  • MySQL平均RSS稳定在64.8G(buffer pool限制生效)
  • 跑批任务内存峰值从36G降到7.6G(cgroup限制生效)
  • dirty pages最高12G,大多数时间在3-6G震荡
  • 系统OOM次数:0

压测复现:验证方案有效还是碰运气

改完之后做了一轮压测来验证方案在极端情况下的效果。用sysbench 1.0.20生成4张100万行表,跑读写混合负载:

# 准备数据
sysbench /usr/share/sysbench/oltp_read_write.lua \
    --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=sbtest \
    --mysql-password=password --tables=4 --table-size=1000000 \
    prepare

# 压测60秒,64线程读写混合
sysbench /usr/share/sysbench/oltp_read_write.lua \
    --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=sbtest \
    --mysql-password=password --tables=4 --table-size=1000000 \
    --threads=64 --time=60 --report-interval=5 \
    --db-ps-mode=disable --rand-type=uniform run

压测期间同时跑一个写文件的批任务(模拟生产环境):

# 持续写文件,制造dirty pages
dd if=/dev/urandom of=/data/bigfile bs=1M count=20000 &
# 另一个跑批任务,读大文件制造page cache压力
cat /data/backup.tar.gz > /dev/null &

压测结果对比(同一环境、同一个sysbench、同一批数据):

指标调整前调整后
TPS(平均)412.6387.2
QPS(平均)18,03317,502
p99 延迟72ms94ms
系统free内存最低值1.2GB(有OOM风险)28.5GB
是否触发OOM是,压测期间杀掉1个进程
平均IO util %96%87%

性能代价是TPS下降了约6%,p99延迟增加了约30%。原因是dirty_background_ratio调低后,磁盘刷写更频繁,IO压力在时间上分布更均匀,但总IO量没有减少。用一点性能换稳定性,对于线上数据库来说是完全值得的。更重要的是:压测期间不再OOM杀进程,系统最差也有28.5G自由内存兜底。

这里还有一个细节:调整前压测结束后5分钟,系统free内存在慢慢回升(说明kswapd终于刷完了dirty pages)。调整后压测一结束free内存马上回到40G以上,因为dirty pages总量小,回收更快。这个恢复速度的差异在生产上很重要——服务器不会再因为内存紧张而长时间"假死"。

如果真的再次OOM:从日志到精确定位

配了再多参数,也得会看现场。真OOM了,严格按下面步骤排查(按顺序执行):

第一步:看dmesg定位"被杀"进程

# 查看OOM记录,不要用journalctl,那个经常丢
dmesg -T | grep -i "out of memory" | tail -5
dmesg -T | grep -i "killed process" | tail -5

输出里拿到两个关键信息:

  • score:OOM分数,跟进程占用内存大小和oom_score_adj有关
  • total-vm:虚拟内存总量;anon-rss:匿名内存(实际占的物理内存)

第二步:看内存快照(如果系统还活着)

# 内存总量和剩余
free -g
# 每个进程RSS排序 Top 20
ps -eo pid,ppid,rss,vsz,comm --sort=-rss | head -20
# 每个进程OOM score
for pid in $(ps -eo pid --no-headers); do
    printf "%s %s\n" "$(cat /proc/$pid/oom_score 2>/dev/null)" "$(cat /proc/$pid/comm 2>/dev/null)"
done | sort -rn | head -10

第三步:看page cache和dirty pages的状态

# page cache 类型分布
cat /proc/meminfo | grep -E "^(Cached|Dirty|Writeback|AnonPages|Mapped|Shmem)"

# 内存回收统计(pgscan/pgsteal等关键指标)
grep -E "pgscank|pgsteal|pgpgout|pswpin|pswpout" /proc/vmstat

关键指标:pgscand(直接回收扫描页数)、pgsteal(回收成功的页数)。如果pgscand增长很快,说明系统频繁进入direct reclaim,这是内存压力的核心信号。

第四步:看IO(OOM时的隐藏凶手)

# 看磁盘等待时间,如果util高说明IO是瓶颈
iostat -x 1 5

# 看IO等待的进程数量
cat /proc/loadavg
# /proc/loadavg里第4个字段是运行队列中的进程数

避坑:我实际踩过的几个坑

坑1:被"free"显示欺骗

free命令第一行free列很小,不用激动。要看第二行的available,这才是真正可用的内存。特别是第一次接触这个问题时,很容易看到free=1.2G就觉得要OOM了。其实只要available在,说明page cache可以被回收。

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           125G        106G        1.2G        2.1G         18G         28G
# available=28G,说明还有28G可回收内存,系统暂时安全

坑2:在3.10内核上贸然调低dirty_ratio导致IO卡死

第一次处理这类问题时,我把dirty_ratio从30改到5,结果写文件的任务反而卡得更厉害。原因:vm.dirty_ratio=5意味着dirty pages只要到6.4G,所有写操作必须同步等待刷盘,而磁盘本身写能力有限。之前30%时磁盘可以攒一批再刷,吞吐更高。

调低dirty_ratio必须配合两个条件:

  • 业务可以容忍更低的写吞吐
  • IO子系统有足够的排队能力(如NVMe盘或RAID卡带电池)

SATA盘就别乱调了,默认值更好。

坑3:dmesg的OOM日志中看到的是"victim"进程,但真正的元凶(写脏页的进程)可能早就跑完了

OOM杀进程选择的策略是"选分最高、占用最多、且可安全杀死的进程"。MySQL分高是因为它占了100多G,但内存压力的源头可能是另一个大批量任务写文件把page cache占满。如果dmesg里只看到MySQL被杀,就只针对MySQL调优,是治标不治本。要用vmstat里的pswpin/pswpout以及pgscand追到真正的压力源。

坑4:乱看/proc/zoneinfo容易得出错误结论

在128G内存的机器上,free pages低于min水位是正常现象。因为min水位保证的是"紧急分配"的最低需求,不是"必须保持free pages高于这个值"的意思。只有free pages同时低于low水位和min水位,并且kswapd持续回收仍然无法改善,才说明真的有问题。一开始我也盯着zoneinfo里的free值,低于min就想调参数,后来发现那是正常的平衡状态。

坑5:bpftrace在CentOS 7.9上默认装不上,需要换内核或装兼容包

CentOS 7.9默认内核3.10,bpftrace 0.18.1需要额外添加内核调试符号(kernel-devel、kernel-debuginfo)。推荐直接在Ubuntu 22.04(内核5.15)上用,或者用perf代替:

# perf 直接追踪内存回收事件(CentOS 7.9可用)
perf record -e vmscan:mm_vmscan_direct_reclaim_start -ag -- sleep 10
perf report --stdio

坑6:云服务器上配swapfile注意IO位置

当年在云主机上创建swapfile直接放到数据盘,结果那个盘本身IO已经接近饱和。系统把进程匿名页swap到同一块盘上,IO更加恶化,反而加速了OOM。正确的做法是放在单独的、低负载的盘上,或者干脆用云平台提供的swap磁盘。

原理补充:内存回收到底如何选择"杀谁"

OOM Killer不是随机杀,它有一个打分机制(badness score)。评分依据:

  • 进程物理内存占用(RSS):占得越多,分越高
  • 进程优先级(nice值):每加1,score加10
  • Root进程加分:root进程做了一些系统调用,分数会降低
  • 是否直接访问硬件:直接访问硬件(比如数据库用DIO)的进程分高

可以针对关键进程设置oom_score_adj,让OOM killer更不容易杀它:

# 查看当前值
cat /proc/$(pgrep mysqld)/oom_score_adj
# 默认是0,调低到-500,让OOM跳过它
echo -500 > /proc/$(pgrep mysqld)/oom_score_adj
# 如果想保护到极致,设置-1000(OOM killer完全忽略)
echo -1000 > /proc/$(pgrep mysqld)/oom_score_adj

# 但注意:-1000时如果系统真的内存耗尽,只能杀其他进程;如果所有进程都-1000,内核panic

生产环境建议给数据库设置-800,其他服务可以保持默认或-500。这样真的发生OOM,杀的是跑批任务,而不是MySQL。

最后的建议:把监控做在前面

这次事故给我们的教训:内存问题不是靠"调一次参数"解决的,需要持续监控。建议至少保留这些指标:

# 通过crontab每30秒记录一次内存指标,至少保留30天
*/1 * * * * echo "$(date +%s) $(free -g | grep Mem | awk '{print $2,$3,$4,$5,$6,$7}') $(cat /proc/vmstat | grep -E 'pgscand|pgsteal' | tr '\n' ' ')" >> /var/log/memory_monitor.log

配合Zabbix/Prometheus做告警。建议以下告警阈值:

  • available内存低于总内存的15%持续5分钟 → warning
  • 直接回收(pgscand)超过每秒10000页持续3分钟 → critical
  • dirty pages超过总内存的20%持续10分钟 → warning(说明刷盘赶不上脏页生成)

那次事故到现在快两年,改造后的环境没再发生OOM。希望这份排查思路对你有用。