马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
数据库主机内存与 swap 治理实战:从 OOM 杀进程到 swappiness 调优
一、具体的问题
凌晨两点被告警叫醒:业务库所在主机上 mysqld 进程不见了,应用全量报连接失败。登上机器 dmesg 一看,Out of memory: Killed process 1234 (mysqld)——是内核 OOM killer 干的。
很多人对 OOM 的第一反应是"内存不够,加内存"。加完之后过半个月又来一次,因为根因根本没动。我在多套生产环境里反复见到的实际情况是:OOM 大多是三层问题叠加的结果——
- 某条失控 SQL 一次性申请了几个 GB 的排序/连接内存,把可用内存打穿;
- 内核 overcommit 策略是默认的宽松模式,分配时来者不拒,真正写内存时才爆;
- vm.swappiness 配置不当,要么 swap 被滥用拖垮性能,要么设成 0 导致内存一紧张内核就直接杀进程。
还有一个更常见的误判:看到 free 里 swap 有占用就慌,把 swappiness 一路调到 0。这个操作在 kernel 3.5 以后的语义变化,反而会让 OOM 更早发生。这篇文章把这三层问题一次讲透,给出可照做的排查命令和一套验证过的参数。
二、核心原理
OOM killer 的打分逻辑。 内存耗尽时内核不会随机杀进程,而是按 badness 评分选"最该死"的:占用 anon-rss 越大分越高。mysqld 常驻十几 GB,天然就是最高分选手——所以每次都是数据库被杀,不是巧合。评分可以用 /proc/<pid>/oom_score 查看,也可以通过 oom_score_adj(-1000~1000)人工干预,systemd 服务对应的是 OOMScoreAdjust=。
overcommit 的三档语义。 vm.overcommit_memory:0(默认,启发式宽松分配,malloc 几乎都成功,写入时才发现没内存,触发 OOM);1(完全允许,永远不拒绝);2(严格模式,CommitLimit = swap + overcommit_ratio% × RAM,分配超过就返回失败)。设成 2 的本质是把"分配成功但写入被杀"转成"分配直接报错",让数据库能拿到报错而不是被内核爆头。
swappiness=0 的陷阱。 swappiness 控制的是"回收匿名页(换出 swap)与回收文件页(丢弃缓存)的倾向"。设 0 在新内核里含义是尽量不换出匿名页——内存紧张时内核会疯狂回收文件页、加速触发 OOM,而不是像老内核那样"完全不 swap"。想要"最少化 swap 但保留兜底",正确值是 1。
失控 SQL 是内存压力源头。 sort_buffer_size、join_buffer_size 都是每连接每排序分配的,一条深分页 + 大结果集排序的 SQL 就能在瞬间吃掉几 GB。治理 OOM 如果只调内核参数不治 SQL,等于给漏水的水池换更大的池子。
三、实例参考(动手步骤)
步骤 0:确认现状基线。
- free -h && swapoff -v 2>/dev/null; swapon --show
- vmstat 1 5 # 重点看 si/so 列,持续非零说明在活跃换页
- sysctl vm.overcommit_memory vm.overcommit_ratio vm.swappiness
- grep -i commit /proc/meminfo # Committed_AS 与 CommitLimit 的关系
复制代码
步骤 1:翻 OOM 现场档案。
- dmesg -T | grep -i "out of memory"
- journalctl -k --since "-7 days" | grep -i oom
- # 找到被杀进程的 anon-rss、OOM 那一刻的内存快照,并倒查当时正在跑的 SQL
复制代码
每起 OOM 都归档:时间、被杀 pid、rss 值、当时 in-flight 的 SQL 文本。没有这份档案,后面的参数调整都是盲调。
步骤 2:定位失控 SQL(MySQL 8 示例)。
- -- 打开内存 instrument(重启后需确认)
- UPDATE performance_schema.setup_instruments SET enabled='YES'
- WHERE name LIKE 'memory/%';
- SELECT event_name, current_alloc
- FROM sys.memory_global_by_current_bytes LIMIT 10;
- -- 排序失控侧面证据
- SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';
- SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';
复制代码
我们当时的元凶是一条 ORDER BY 3200 万行的对账 SQL,单连接峰值吃掉 6.4 GB。改写成游标分批处理后,单连接峰值降到 210 MB。
步骤 3:内核参数落盘。 写进 /etc/sysctl.d/99-db.conf(直接 sysctl -w 重启会丢):
- vm.swappiness = 1
- vm.overcommit_memory = 2
- vm.overcommit_ratio = 85
- vm.dirty_background_ratio = 5
- vm.dirty_ratio = 20
- vm.min_free_kbytes = 262144
复制代码
sysctl --system 生效后,用 Committed_AS < CommitLimit 校验;如果反了,说明预留内存不足,把 overcommit_ratio 往上调(85→90)再验。
步骤 4:保护数据库进程。 systemd 管理的服务加:
- [Service]
- OOMScoreAdjust=-600
复制代码
systemctl daemon-reload 后重启 mysqld,cat /proc/<mysqld_pid>/oom_score_adj 确认是 -600。这样极端情况下内核会优先牺牲其他进程,给 DBA 留出处置时间。
步骤 5:数据库层收敛。 innodb_buffer_pool_size 控制在物理内存 60% 以内,给文件页缓存和突发分配留余量;sort/join buffer 保持默认或略降,靠治理失控 SQL 而不是调大 buffer 掩盖问题。
步骤 6:监控与告警落地。 参数调完不是终点,要有眼睛盯着:
- # /usr/local/bin/mem_probe.sh —— crontab 每 5 分钟采样
- #!/bin/bash
- vmstat 1 2 | tail -1 | awk '{print $7, $8}' | \
- while read si so; do
- [ "$si" -gt 0 -o "$so" -gt 0 ] && \
- echo "$(date '+%F %T') si=$si so=$so" >> /var/log/swap_probe.log
- done
- grep -q . /sys/fs/cgroup/memory/events 2>/dev/null || true
复制代码
三条告警线:vmstat 的 si/so 连续 3 个采样点非零(开始碰 swap);Committed_AS 达到 CommitLimit 的 90%(分配逼近上限);sys.memory_global_by_current_bytes 第一名周环比增长超过 50%(有 SQL 在悄悄膨胀)。三线中任何一条触发,都远早于 OOM 爆发,处置窗口从"凌晨两点救火"变成"白天从容查 SQL"。
四、实操检查清单
- [ ] 每起 OOM 都有档案:时间、被杀 pid、anon-rss、当时 SQL 文本
- [ ] vm.swappiness=1(不是 0),vmstat 采样确认 si/so 常态为 0
- [ ] vm.overcommit_memory=2,且 Committed_AS 稳定小于 CommitLimit
- [ ] 参数写在 /etc/sysctl.d/*.conf,sysctl --system 后复查生效
- [ ] mysqld/postgres 的 systemd unit 配置 OOMScoreAdjust,/proc/<pid>/oom_score_adj 已验证
- [ ] buffer pool ≤ 物理内存 60%,sort/join buffer 未被盲目调大
- [ ] sys.memory_global_by_current_bytes 每周巡检,最大常驻内存对象有归属
- [ ] 复现当年那条失控大 SQL:验证不触发 OOM、swap 占用不增长
- [ ] si/so 采样与 Committed_AS 告警线已接入监控,不是靠肉眼 vmstat
几个容易踩的坑
Redis 与 MySQL 的 overcommit 建议值相反,不要照抄。 Redis 官方建议 overcommit_memory=1(fork 出子进程做 RDB,CoW 瞬间需要大量"名义分配"),MySQL/PG 这类常驻型服务建议 2。一台机器混部两者时只能按主要负载取舍,并把另一方的内存上限卡死。
swappiness=0 不是"禁用 swap",而是"尽早 OOM"。 这是最广泛的以讹传讹。禁用 swap 的正确方式是 swapoff,但对数据库主机我反而建议保留一小块 swap(2~4 GB)+ swappiness=1,让极端尖峰有一个缓冲垫,好过直接杀进程。
min_free_kbytes 不要拍脑袋设大。 设得太大等于从可用内存里挖走一块,小内存机器上反而加剧压力;一般按总内存的 1% 左右、上限 1 GB 起步,配合 /proc/zoneinfo 的 min 值观察。
init.d 老式启动的服务不受 systemd OOMScoreAdjust 控制。 只能用 echo -600 > /proc/<pid>/oom_score_adj,且要放进启动脚本,否则重启即失效。
治理前后对比
| 指标 | 治理前 | 治理后 | | OOM 杀 mysqld | 每月 1~2 次 | 0 次(连续 60 天) | | 失控 SQL 单连接内存峰值 | 6.4 GB | 210 MB | | si/so(vmstat 峰值) | 频繁非零 | 常态 0 | | mysqld swap 占用 | 3.2 GB | < 50 MB | | 内存告警次数/月 | 8~12 次 | 1 次 | | 深夜故障处置 | 人工重启 40 min 起 | 无 |
一句话总结:OOM 治理的顺序永远是先治 SQL、再定 overcommit、最后微调 swappiness——把顺序反过来做,参数调得再精细也只是在给失控的分配行为兜底。 |