
前阵子朋友的项目又遇到“服务器总是 OOM”的问题登录上一看mysqld 进程已经消失/var/log/messages 里贴着一行刺眼的记录Out of memory: Kill process 29345 (mysqld)。这台机器 32G 内存业务量不算大可 MySQL 就是扛不住。我用 MySQLTuner 跑了一遍内存诊断又交叉比对系统日志半个小时就定位到了问题根源。这篇文章就把这个完整排查过程拆开讲用 MySQLTuner 看什么指标、怎么读报告、报告里哪些信号是在提前预警 OOM以及确认之后怎么调参数。适合刚遇到 MySQL 被 kill、内存持续偏高、或者想建立一套内存巡检机制的运维同学。1. 先搞明白MySQL 为什么是 OOM 的“头号受害者”很多人一遇到 OOM 就急着调参数结果越调越乱。我建议先花五分钟理解 Linux OOM Killer 的裁决逻辑后面所有调整才会有方向。1.1 OOM Killer 的裁决逻辑谁内存多谁先死Linux 系统内存耗尽时内核会触发 OOM Killer它遍历所有进程根据每个进程的 oom_score 挑一个杀掉把内存释放出来。oom_score 的算法不只看当前进程占用了多少物理内存还会参考 swap 使用量、运行时长、进程优先级等因素。实际经验里最常见的被选中的对象就是那个 RSS 最高的进程——而一台专门跑 MySQL 的服务器上RSS 最高的必然是 mysqld。你可以实际看一下这台进程的分数cat /proc/$(pgrep mysqld | head -n 1)/oom_score如果这个数接近 1000说明它几乎在每次 OOM 时都会被优先干掉。跑一个不恰当的类比宿舍管理员接到限电通知第一反应肯定是先把占地方最大的房间电掐了不管这个房间是不是在给整栋楼供暖。MySQL 的 InnoDB Buffer Pool 天生就是内存大户所以它常常躺着中枪。另一个关键点是OOM 不只是发生在“物理内存全部用完”的那一刻。如果开启了 swap系统会先疯狂换页等 swap 也吃紧才触发 OOM如果开启了 overcommit内核在进程申请虚拟内存时会放行但申请后并不保证物理内存够用最终压力仍会落到 OOM Killer 头上。这也是为什么后面调系统参数时swap 和 overcommit 都要照顾到。1.2 MySQL 的四大内存大户Buffer Pool 只是明面上的如果只知道 Buffer Pool那排查 OOM 会很吃力。只盯一个指标调完可能还是崩。MySQL 的内存占用可以拆成四块InnoDB Buffer Pool缓存数据页和索引页也是最大的内存消耗点。生产环境动辄配到 8G 甚至 32G它启动时就会按配置预留内存是“明面上的大头”。连接级缓冲区每个客户端连接都会分配 sort_buffer_size、join_buffer_size、read_buffer_size、read_rnd_buffer_size 等私有缓冲区。这类内存按连接数翻倍连接一多累加起来的量非常惊人。全局缓存与内部结构table_open_cache、key_buffer_size仅 MyISAM、query cache5.6/5.7 遗留问题、performance_schema 内存表等。临时表与排序内存tmp_table_size、max_heap_table_size以及复杂的 GROUP BY、ORDER BY 在内存中产生的临时结构。重点说一下 Buffer PoolMySQL 设计上确实希望它尽量大因为缓存命中率高就能减少磁盘 IO。但它必须给操作系统内核、MySQL 自身运行、其他进程和连接级缓冲区留出余地。如果一个 32G 的机器把 Buffer Pool 配到 28G剩下 4G 要同时满足系统、连接和临时排序那 OOM 基本就是倒计时。2. MySQLTuner 的安装与首次运行四个容易翻车的细节MySQLTuner 是个 Perl 脚本不需要编译下载完就能跑。但它有几个实操坑我第一次用的时候就踩过。2.1 下载脚本wget 和 git clone 两条路都行推荐直接到 GitHub 项目 major/MySQLTuner-perl 下载最新版cd /usr/local/src wget https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl chmod x mysqltuner.pl如果服务器访问 GitHub 慢也可以先在自己的电脑上下载再传上去或者用 git clonegit clone https://github.com/major/MySQLTuner-perl.git cd MySQLTuner-perl perl mysqltuner.pl --version脚本依赖 Perl 的 DBD::mysql 驱动。CentOS 7 上经常缺这个yum install perl-DBD-MySQL -yDebian/Ubuntu 则执行apt install libdbd-mysql-perl -y我第一次在 CentOS 7 上运行新版脚本时报了一串依赖错误提示找不到 DBD/mysql.pm。装完驱动之后又遇到 Perl 版本太老的问题。这类老系统建议装 epel-release 后升级 perl或者直接用 Docker 跑一个带了 Perl 的巡检容器避免污染宿主机环境。2.2 运行前的账号准备权限太严格会读不到指标MySQLTuner 需要连接数据库去读变量和状态建议单独建一个专用账号不要用 root。但注意权限别卡得太死至少要给 PROCESS、SELECT、SHOW DATABASES 等权限否则报告里很多指标会变成 N/A尤其是 InnoDB 状态和复制状态。CREATE USER tunerlocalhost IDENTIFIED BY 你的密码; GRANT SELECT, PROCESS, SHOW DATABASES ON *.* TO tunerlocalhost;如果你希望它连 replication 信息也扫描还需要GRANT REPLICATION CLIENT ON *.* TO tunerlocalhost;运行方式有几种。直接执行最省事./mysqltuner.pl --host 127.0.0.1 --user tuner --pass 你的密码 --port 3306如果不想在 shell 历史里留下明文密码可以这样./mysqltuner.pl --host 127.0.0.1 --user tuner脚本会交互式提示输入密码。另一个容易忽略的坑--host 尽量写 127.0.0.1而不是 localhost。某些系统上 localhost 会强制走 Unix socket如果 socket 路径对不上就会出现类似 “ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock” 的报错。换成本机 TCP 地址能绕开这个常见问题。2.3 首次运行实测一份报告到底说了什么脚本跑完后会在终端输出一整页报告一般分三块System/Environment操作系统版本、CPU、内存、磁盘等基础信息。Performance Metrics / Seized Requirements各类 MySQL 运行指标的检查结果用 [OK]、[!!] 和 [WARN] 标记。Recommendations最后的建议列表。我第一次跑的时候满屏都是 [OK]直到看到这行才意识到问题[!!] Maximum possible memory usage: 34.5G (107% of installed RAM)[!!] 是脚本认为存在配置风险[OK] 是正常[WARN] 是需要注意但不一定有问题。我们排查 OOM 时要重点盯住所有 [!!] 开头的行尤其是和 memory 相关的条目。3. 从 MySQLTuner 报告里读出“内存超标”的信号MySQLTuner 的输出看起来密密麻麻其实真正和 OOM 相关的就集中在少数几项。掌握它的计算逻辑之后你就能把报告反推成自己的配置问题。3.1 Maximum possible memory usage最坏情况是怎么被估算出来的MySQLTuner 在计算内存时不是简单加几个配置它会按最坏情况估算。核心思路是把全局缓存加上所有潜在的连接级内存总内存估算 InnoDB Buffer Pool InnoDB Log Buffer Key BufferMyISAM Query Cache tmp_table_size max_connections × (sort_buffer_size read_buffer_size read_rnd_buffer_size join_buffer_size thread_stack) table_cache / table_open_cache 相关内存 系统自身预留注意这里的关键词是“上限”它假设所有连接都同时达到各自缓冲区的最大占用。实际上大部分连接并不会同时跑复杂排序所以真实占用往往比这个估算值低。但 OOM 往往就发生在极端情况——大量连接同时发起大查询、排序、临时表操作此时所有缓冲区一起疯长最坏估算就变成了真实值。举个例子一台 32G 机器配置 Buffer Pool 24Gmax_connections 600sort_buffer_size 4Mjoin_buffer_size 4M不算其他光连接缓冲区就有 600 × 8M ≈ 4.7G。加上 Buffer Pool 和系统内存报告算出 34.5G 超过物理内存OOM 根本不用等业务高峰随便来一波连接就会崩。3.2 最容易爆雷的几项指标一张表对照我建议每次跑完 MySQLTuner先扫一遍这几个关键位置检查项含义危险信号Maximum possible memory usage最坏情况下的总内存需求达到物理内存 80% 以上就要警惕超过 100% 基本必炸InnoDB Buffer Pool Size数据缓存大小超过物理内存 60%且没有 cgroup/swap 兜底时风险高Max connections最大连接数上限配置值远大于 Max_used_connections 时内存白白预留Sort/Join/Read Buffer每个连接私有的内存缓冲区数值偏大且连接数高相乘后很吓人Table Cache表缓存open_tables 长期很低却配置很大内存浪费Query Cache查询缓存5.6/5.7 开启但命中率低纯属吃内存Temporary Table临时表内存限制tmp_table_size 过大而内存不足时风险高这些指标单一拿出来可能都不起眼但组合起来就是 OOM 的配方。MySQLTuner 好就好在它帮你做了乘法直接输出一个总账。你只要看最后那个百分比就能判断配置是不是已经超出物理内存承受范围。3.3 用报告反推为什么我的配置连 32G 都不够躺实际处理过的案例里最典型的一种是“Buffer Pool 太大 连接级缓冲区没控制”。有人觉得 32G 机器配 24G Buffer Pool 挺合理却忽略了一个事实MySQL 自己运行时的代码、glibc 内存分配器、线程栈、网络缓冲、performance_schema 都要吃内存系统还要留出一部分给 page cache。真正留给连接级缓冲区的空间可能只有 2-3G。而一个 4M sort_buffer 4M join_buffer随手开 300 个连接就是 2.4G再加临时表内存马上穿墙。所以拿到报告后我会依次做三件事看 Maximum possible memory usage 是否超过物理内存看 max_connections 和实际最大连接数的差距看 sort_buffer_size、join_buffer_size、read_buffer_size 是不是被“随手设了个大值”。这三步解决掉百分之七八十的 OOM 都能免于复发。4. 坐实 OOM 真相系统日志与进程层面的交叉验证MySQLTuner 只是诊断工具它告诉你“配置可能超了”但真正的 OOM 案发现场还得靠系统日志和进程快照来验证。这两者互相印证才能百分百确认是内存耗尽导致的 mysqld 被杀而不是别的原因。4.1 dmesg 日志OOM Killer 的“案发现场”最直接的操作是翻内核日志dmesg -T | grep -i -B2 -A4 killed process或者用 journalctl 查最近的内核消息journalctl -k --since yesterday | grep -i oom如果是 CentOS 6/7 这类老系统日志通常在 /var/log/messagesgrep -i Out of memory /var/log/messages成功落地之后你通常会看到类似这样的片段[1234567.890123] Out of memory: Kill process 29345 (mysqld) score 875 or sacrifice child [1234567.890130] Killed process 29345 (mysqld) total-vm: 10485760kB, anon-rss: 785432kB, file-rss: 0kB这里的 score 875 表示该进程被选中概率很高total-vm 是虚拟内存总数anon-rss 是匿名物理内存页。看到 mysqld 被 Killed时间点和 MySQL 崩溃时间吻合基本就可以定罪了。有个小技巧把 dmesg 时间与 MySQL error log 的崩溃时间对齐。MySQL 被 OOM Kill 后error log 通常会有异常中断的记录比如版本小版本升级后常见的 “[ERROR] Aborting” 或者 InnoDB 的恢复日志。两边时间对得上说明确实是被系统杀掉的而不是自身 crash。4.2 从 top 到 /proc进程与线程的内存证据光看日志还不够最好找到 OOM 发生前后的内存证据。如果机器还没重启可以这样查当前内存占用最大的进程ps aux --sort-rss | head -n 20top 里按大写 M 也可以按内存排序。很多人忽略的是 MySQL 是多线程架构mysqld 下面的线程同样会消耗内存。想观察线程级别top -H -p $(pgrep mysqld)进程的内存细节可以看 /proc 目录cat /proc/$(pgrep mysqld | head -n 1)/status | grep -E VmRSS|VmSwap|VmSizeVmRSS 是当前常驻物理内存VmSwap 是已经交换到 swap 的部分。如果 VmSwap 一直涨说明系统内存早就顶不住了。与此同时可以用 vmstat 观察交换情况vmstat 1如果 si 和 so 两列持续有数值说明系统正在频繁 swapping这时候 OOM 只是时间问题。我见过不少场景服务器 swap 被耗尽之后下一秒 mysqld 就被 OOM Kill 了——所以“内存还有 swap 空间”并不代表安全。4.3 MySQL 自身日志错误日志与慢查询的不在场证明MySQL 侧一定要看两样东西错误日志和慢查询日志。错误日志里常见的关键字包括“Out of memory”“InnoDB: Cannot allocate memory for the buffer pool”“[ERROR] Access denied for user ... using password: NO” 这类如果刷屏说明连接配置有问题也可能推高内存。慢查询日志则能帮你找到谁在“吃内存”。如果某条 SQL 在数据量很大的表上做 ORDER BY、GROUP BY 或无索引 JOIN它可能会在内存里构建很大的排序缓冲区或临时表。把慢查询日志中 rows_examined 特别大、且执行时间很长的 SQL 捞出来基本能锁定放大了 sort_buffer 和 join_buffer 的真凶。另外也可以用这条 SQL 快速看连接情况SHOW GLOBAL STATUS LIKE Max_used_connections; SHOW GLOBAL STATUS LIKE Threads_connected;对比 max_connections 的配置如果 Max_used_connections 长期只有 80 而 max_connections 配置到 800那 800 个连接对应的内存就是纯粹的浪费。5. 对症下药按 MySQLTuner 诊断结果调整参数诊断完成确认 OOM 是因为内存配置超限接下来就是调参。调整的原则是先砍大头再清理小头一次只改一两个参数观察一段时间。5.1 第一刀永远砍在 InnoDB Buffer Pool 上Buffer Pool 是内存占用的绝对主力OOM 只要和内存有关第一刀基本都落在它身上。一般建议 Buffer Pool 不超过物理内存的 50%-70%。如果机器上还跑着监控、备份、Nginx 等其他进程建议保守一点取 50%-60%。比如一台 32G 内存的机器预留 4G 给系统3G 给连接缓冲区和临时表那 Buffer Pool 配到 20G 左右比较安全。配置示例[mysqld] innodb_buffer_pool_size 20G innodb_buffer_pool_instances 8 innodb_buffer_pool_chunk_size 128M这里有个容易踩的细节innodb_buffer_pool_chunk_size 默认 128M修改它时需要保证 chunk_size 能被 pool_size 整除否则 MySQL 会做自动调整。instances 的设置建议是 1G 以上再拆成多个实例减少并发访问时的锁竞争。另外5.7 和 8.0 都支持动态调整 Buffer PoolSET GLOBAL innodb_buffer_pool_size 20 * 1024 * 1024 * 1024;可以先在线调整观察一两天稳定之后再写进 my.cnf。但要注意动态调大或调小 Buffer Pool 都涉及 buffer pool 的重新分配会产生一段时间的 IO 活动不要在业务高峰做。5.2 线程私有缓冲区最容易被忽略的隐性刺客Buffer Pool 砍下来之后第二刀落在连接级缓冲区。这类参数很多人容易手滑设成 8M、16M以为越大查询越快忘了它是“每个连接”各一份。连接一多内存直接翻倍。以常用的 MySQL 5.7/8.0 为例参考方案参数作用建议值sort_buffer_size排序操作的内存缓冲2M - 4Mjoin_buffer_size无索引 JOIN 的内存缓冲2M - 4Mread_buffer_size顺序扫描的数据缓冲1Mread_rnd_buffer_size随机读排序缓冲1M先把配置里的值调回这个区间大幅降低乘法基数[mysqld] sort_buffer_size 2M join_buffer_size 2M read_buffer_size 1M read_rnd_buffer_size 1M如果你的业务确实有大排序、大 JOIN 场景不要把这些值设成全局大值。正确做法是保持全局偏小在会话里对特定 SQL 临时调大SET SESSION sort_buffer_size 64M; -- 执行那条复杂的排序 SQL -- 等连接关闭内存自动释放这样既保证大查询能跑又不会让所有连接都背上高内存配额。实测下来很多 OOM 都是这类“顺手调大的局部缓冲”引发的。5.3 连接数、表缓存和查询缓存能减就减连接数相关的参数同样影响内存。查看历史最大连接数SHOW GLOBAL STATUS LIKE Max_used_connections;如果 Max_used_connections 长期低于 200而 max_connections 配的是 1000那就是在给 MySQL 预留大量虚拟内存。可以把 max_connections 降到 300 或 400既满足高峰又减少内存焦虑。同时把 wait_timeout 和 interactive_timeout 调小一点比如 60 到 300 秒之间让 Sleep 连接尽快退出释放对应的线程栈和缓冲区。表缓存也是容易被忽视的点。table_open_cache 如果配得很大但实际 open_tables 很低纯属浪费内存。推荐先看现状SHOW GLOBAL STATUS LIKE Open_tables; SHOW GLOBAL STATUS LIKE Opened_tables;如果 Opened_tables 在快速增长说明表缓存不够如果 Open_tables 连缓存的一半都用不到就该调低 table_open_cache。query cache 是 5.6/5.7 的老坑。MySQL 8.0 已经彻底移除该功能5.7 里默认是关闭的但如果你的旧配置还开着 query_cache_type 1建议直接关闭它命中率低时纯属内存吞金兽。5.4 调参节奏别在业务高峰期搞大动作参数调整不是“改完重启”就完事的要尊重节奏修改 my.cnf 前先备份一份原文件用mysqld --validate-config做配置校验优先用在线SET GLOBAL改动确认稳定再落盘一次只改一到两个参数观察 24 小时重启尽量安排在低峰期。特别提醒OOM 之后如果立刻重启 MySQL刚启动时 Buffer Pool 重新预分配内存占用反而会有一个明显的波峰。如果你同时改了 Buffer Pool 和连接数最好在低峰期操作避免刚起来又挂在新的内存峰值上。6. 最后一道防线系统层面的 OOM 兜底与监控参数调完并不代表万事大吉。MySQL 是服务不可能永远不超预期所以系统层最好再上一道“安全阀”同时把巡检自动化。6.1 swap 与 overcommit系统层做减法很多生产服务器为了性能直接关掉 swap这在 OOM 场景下反而更危险。没有 swap 做缓冲内存一紧张 OOM Killer 马上下手。我建议保留至少 1G-2G swap并设置相对保守的 swappinesssysctl -w vm.swappiness10往 /etc/sysctl.conf 里写入持久化vm.swappiness10overcommit 参数也可以关注。默认 overcommit_memory0 是启发式内核会“适度”放行内存申请如果设置成 2表示禁止超额分配同时需要配置 overcommit_ratio。生产环境我一般不建议贸然设成 2因为可能导致 mysqld 启动时申请内存失败。更稳妥的做法是保持默认把风险控制放在 MySQL 参数和 cgroup 上。6.2 cgroup 内存上限为 mysqld 装一道“安全阀”systemd 管理的 MySQL 可以通过 drop-in 配置直接限制进程内存。以 mysqld.service 为例mkdir -p /etc/systemd/system/mysqld.service.d创建 /etc/systemd/system/mysqld.service.d/oom.conf[Service] MemoryMax24G MemorySwapMax0 OOMScoreAdjust-500然后重载并重启systemctl daemon-reload systemctl restart mysqld这里的 MemoryMax 表示 mysqld 最多用 24G 物理内存超过后由 cgroup 触发回收而不是立刻卷入整机 OOM。MemorySwapMax0 表示这个进程下禁止使用 swap避免 swap 被 MySQL 拖垮。OOMScoreAdjust-500 则是降低 mysqld 在系统 OOM 时的被选中概率等于给核心库加了一层保护。注意如果服务名不是 mysqld 而是 mysql文件路径和命令改成对应的服务名。另外MemoryMax 设得太低会导致 MySQL 在内存紧张时频繁触发 cgroup OOM影响正常业务所以这个值还是要比实际需求留出 10%-20% 余量。6.3 把 MySQLTuner 纳入巡检流程每天看一眼红灯最后建议把 MySQLTuner 做成定时任务每天自动跑一次并留存报告0 2 * * * root /usr/local/src/mysqltuner.pl --nobanner --nocolor /var/log/mysql_tuner/$(date \%Y\%m\%d).log 21简单点每天只看[!!]数量grep -c \[!!\] /var/log/mysql_tuner/$(date \%Y\%m\%d).log超过阈值就在监控里告警。不过要提醒一句MySQLTuner 给出的建议不全是真理少数情况下它会建议把 Buffer Pool 拉高或者开启 query cache旧版本这需要结合你的真实负载来判断。我的习惯是把它当作“内存体检报告”而不是“自动修复工具”。我个人的经验是每次排完 OOM 都把 MySQLTuner 报告和 dmesg 日志放一起存档日期作为文件名。几个月积累下来能明显看到内存配置和业务增长之间的关系。以后再遇到“服务器总是 OOM”先翻历史报告往往几秒钟就能定位到是哪次参数变更埋的雷。