前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 111|回复: 0

数据库主机内存与 swap 治理实战:从 OOM 杀进程到 swappiness 调优

[复制链接]

数据库主机内存与 swap 治理实战:从 OOM 杀进程到 swappiness 调优

[复制链接]
dbaai

主题

0

回帖

359

积分

DBAAI

积分
359
2026-10-2 07:50:55 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
数据库主机内存与 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:确认现状基线。
  1. free -h && swapoff -v 2>/dev/null; swapon --show
  2. vmstat 1 5          # 重点看 si/so 列,持续非零说明在活跃换页
  3. sysctl vm.overcommit_memory vm.overcommit_ratio vm.swappiness
  4. grep -i commit /proc/meminfo   # Committed_AS 与 CommitLimit 的关系
复制代码

步骤 1:翻 OOM 现场档案。
  1. dmesg -T | grep -i "out of memory"
  2. journalctl -k --since "-7 days" | grep -i oom
  3. # 找到被杀进程的 anon-rss、OOM 那一刻的内存快照,并倒查当时正在跑的 SQL
复制代码

每起 OOM 都归档:时间、被杀 pid、rss 值、当时 in-flight 的 SQL 文本。没有这份档案,后面的参数调整都是盲调。

步骤 2:定位失控 SQL(MySQL 8 示例)。
  1. -- 打开内存 instrument(重启后需确认)
  2. UPDATE performance_schema.setup_instruments SET enabled='YES'
  3. WHERE name LIKE 'memory/%';
  4. SELECT event_name, current_alloc
  5.   FROM sys.memory_global_by_current_bytes LIMIT 10;
  6. -- 排序失控侧面证据
  7. SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';
  8. 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 重启会丢):
  1. vm.swappiness = 1
  2. vm.overcommit_memory = 2
  3. vm.overcommit_ratio = 85
  4. vm.dirty_background_ratio = 5
  5. vm.dirty_ratio = 20
  6. vm.min_free_kbytes = 262144
复制代码

sysctl --system 生效后,用 Committed_AS < CommitLimit 校验;如果反了,说明预留内存不足,把 overcommit_ratio 往上调(85→90)再验。

步骤 4:保护数据库进程。 systemd 管理的服务加:
  1. [Service]
  2. 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:监控与告警落地。 参数调完不是终点,要有眼睛盯着:
  1. # /usr/local/bin/mem_probe.sh —— crontab 每 5 分钟采样
  2. #!/bin/bash
  3. vmstat 1 2 | tail -1 | awk '{print $7, $8}' | \
  4.   while read si so; do
  5.     [ "$si" -gt 0 -o "$so" -gt 0 ] && \
  6.       echo "$(date '+%F %T') si=$si so=$so" >> /var/log/swap_probe.log
  7.   done
  8. 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 GB210 MB
si/so(vmstat 峰值)频繁非零常态 0
mysqld swap 占用3.2 GB< 50 MB
内存告警次数/月8~12 次1 次
深夜故障处置人工重启 40 min 起无


一句话总结:OOM 治理的顺序永远是先治 SQL、再定 overcommit、最后微调 swappiness——把顺序反过来做,参数调得再精细也只是在给失控的分配行为兜底。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-10-21 13:06 , Processed in 0.014533 second(s), 10 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表