
我先说个结论MySQL内存占用失控基本不是某个参数“设置错了”这么简单而是多种内存组件叠加的结果。如果你一上来就百度innodb_buffer_pool_size调小大概率治标不治本。这篇文章我结合自己几次通宵排查的经历把思路、命令、参数计算和真正的坑一次说清。1. 先说结论MySQL到底把内存花在哪儿了1.1 你以为的MySQL内存 vs 实际的MySQL内存很多朋友查内存占用下意识就看free -h里的used百分比然后立刻怀疑innodb_buffer_pool_size设太大了。这个方向没错但不全面。MySQL的内存开销分四大块全局缓冲、线程级缓冲、性能监控缓冲、以及“看不见”的内存碎片和元数据。用top看mysqld的RES内存实际上是所有这些的累加。我见过一个非常典型的案例一台16G内存的服务器mysqld RES占用14G。查innodb_buffer_pool_size只有6G按说还有8G在别处。后来拆解发现performance_schema吃了1.8Gtable_open_cache相关的字典占了几百MB连接线程堆叠了2G多剩下的全是被glibc的arena机制裁掉的内存碎片。所以排查的第一步是先搞清楚钱花在哪个账户上。1.2 常见的内存黑洞排行榜我按实战中出现频率从高到低排了个序你可以在心里先对个号InnoDB Buffer Pool主存储引擎的缓存池默认128M但生产库一般会调到物理内存的50%-70%这是最大的大头。Performance Schema5.7及以上默认开启专门记录性能事件。表多、连接多、历史统计维度多它轻松吃1-3G不是新闻。连接线程内存每个连接独立占用包括线程栈默认256KB和一堆排序/连接/读写的临时缓冲区。临时表内存排序、去重、子查询都可能在内存里建临时表tmp_table_size和max_heap_table_size共同限制。InnoDB Adaptive Hash Index自适应哈希索引命中高的时候占用会在buffer pool之外额外增加5.7和8.0上都不能忽略。数据字典与元数据8.0将数据字典统一放在InnoDB中表数量上了几千几万张这部分内存相当可观。Glibc内存碎片频繁分配释放小内存之后进程RSS只涨不降这是最常见的“内存膨胀”假象。提示这几个内存黑洞里真正能用简单参数控制住的只有前四个。后面的碎片和字典需要从操作系统层面或者架构层面解决。2. 一上来别乱调参先按这套思路走2.1 第一件事先分清楚是“占”还是“涨”“占”是指MySQL启动后稳定在高位所有缓存都逐步预热到满“涨”是指运行一段时间后RSS持续上升甚至逼近物理内存上限。这两个性质完全不同。如果是“占”通常是buffer pool配置较大加上各类缓存预热到最大属于正常现象只要不发生swap或OOM一般不用管。如果是“涨”说明存在内存泄漏、碎片失控、临时表膨胀或者连接数在一段时间内被大量建立。怎么分辨很简单记录启动时的RSS和时间点之后每隔1小时看一次free -h和pidstat -r -p mysqld_pid 1连续观察24小时。如果RSS曲线是一条随时间单调上升不回落的线那就是典型的“涨”。我之前遇到过一台测试库重启后第一天RSS只有4G三天后到了9G查代码发现是应用的连接池反复执行大排序却不释放会话线程级缓冲被来回撑大。这种问题不查增长趋势光调参数是发现不了的。2.2 用sys库和performance_schema做一次“内存体检”当mysql版本在5.7.9及以上时sys库大概率已经安装。即使没装也可以手动安装。我建议先查两个最核心的视图。第一查全局内存占用按事件统计SELECT * FROM sys.memory_by_host_by_current_bytes WHERE host IS NOT NULL ORDER BY current_bytes DESC LIMIT 10;这个视图按host统计了当前连接累计占用的内存。如果某个业务IP占了绝对大头说明连接数或连接行为有异常。第二查更底层的事件维度SELECT event_name, current_alloc, high_alloc, high_num FROM sys.memory_global_by_current_bytes ORDER BY current_alloc DESC LIMIT 20;current_alloc是当前仍被分配且未释放的内存high_alloc是历史最高水位。如果current和high差距非常大说明曾经发生过大量内存申请之后只回收了一部分碎片或延迟回收问题就藏在这里。如果sys.memory_global_by_current_bytes查出来发现performance_schema占了最高的字节数你还可以用下面的语句盯着p_s内部缓冲SELECT * FROM performance_schema.memory_summary_global_by_event_name WHERE event_name LIKE %performance_schema% ORDER BY current_alloc DESC LIMIT 10;2.3 使用前必读先做一次参数快照不要急着调参数。修改之前先把关键参数用下面的SQL导出做快照备份SHOW VARIABLES WHERE Variable_name IN ( innodb_buffer_pool_size, innodb_log_buffer_size, innodb_buffer_pool_instances, performance_schema, performance_schema_max_instances, table_open_cache, table_definition_cache, max_connections, thread_cache_size, sort_buffer_size, join_buffer_size, read_buffer_size, read_rnd_buffer_size, tmp_table_size, max_heap_table_size, temptable_max_ram );这些参数就是内存账本上的每一笔大额消费记录。没有快照你后面调优根本不知道改了哪个变量产生了什么影响。注意8.0里部分参数如table_open_cache默认值较大4千起步并且在运行时动态变化不要用5.6的旧经验直接套用。3. 定位到具体元凶后怎么处理3.1 内存黑洞处理对照表我整理了一张处理对照表方便你按实际情况对症下药。这不是最终方案每个值都要结合你的服务器总内存和业务模型再验证。内存来源直观表现最优处理方向InnoDB Buffer PoolRES中最大单块稳定占物理内存约一半以上一般不动除非发生swap。若swap则先看其他部分是否异常Performance Schemamysqld高占用但SQL执行并不慢关闭不必要的p_s维度或者调大消耗承担度线程缓冲累积current_alloc不高但RSS高监控显示线程很多检查连接池配置限制max_connections回收空闲连接临时表膨胀慢日志出现大量Sort rows、Using temporary磁盘tmp目录增长调大max_heap_table_size、优化SQL减少临时表自适应哈希索引buffer pool看不算大但RSS超预期如果命中率低考虑关闭innodb_adaptive_hash_index内存碎片RSS高但所有统计加起来对不上账重启一次或换jemalloc/设置MALLOC_ARENA_MAX3.2 针对高频场景的配置调整参考如果排除了碎片膨胀源头还是在连接线程和临时表内存上我一般这样调整。连接线程方面关注三个值max_connections 300 thread_cache_size 64 max_execution_time 30000max_connections不是越大越好。每个连接至少2MB的潜在开销在8.0里很常见300个连接已经吃掉600MB。thread_cache_size是空闲线程缓存不是限制连接数但设置太小会让线程反复创建间接带来短暂内存峰值。max_execution_time是兜底防止长连接执行大SQL把排序缓冲撑到极限。临时表方面别无脑调大tmp_table_size。之前一台业务库因为tmp_table_size调到512M结果一个大查询直接分配500M临时表内存差点OOM。正确做法是先优化SQL把Using temporary的语句拆掉再设置一个合理上限比如128M。max_heap_table_size 128M tmp_table_size 128M temptable_max_ram 512M注意max_heap_table_size是硬上限tmp_table_size是上限之一两者取小值生效。8.0的temptable引擎还有独立上限专门控制内存临时表的总内存用量。4. 三次实战排查复盘4.1 案例一innodb_buffer_pool_size设置过大导致频繁swap某业务服务器32G内存mysqld配置文件里写了22G的buffer pool。平时业务量不大时还能运行大促一来连接数一上来整个系统直接进入swap响应时间从2ms飙到2秒。排查过程是这样的先用free -g发现swap已经使用了接近10G。用cat /proc/pid/status查看mysqld的VmPeak和VmRSS确认RSS已经接近26G。用SHOW ENGINE INNODB STATUS\G查看BUFFER POOL AND MEMORY部分发现Free Frames接近0。查看系统其他工具占用发现Tomcat占了4Gredis占了2G磁盘缓存也需要空间。结论是innodb_buffer_pool_size设置没有预留共享buffer以外的其他进程内存也没有考虑连接线程和p_s的额外开销。后来我按“总内存的三分之二减掉其他组件占用再留1-2G余量”重新计算改成了18G之后swap立刻缓解。这类问题的核心教训是MySQL内存配置不是只见树木不见森林要先算整机内存账。4.2 案例二performance_schema把内存吃成了第一大项又是一台8G内存的MySQL5.7运行半年后占用9G开始出现OOM。查sys.memory_global_by_current_bytes第一名竟然是performance_schema.mutex和performance_schema.rwlock。原因是一开始安装时用了一个网上公开的全功能监控脚本把p_s所有维度全部打开包含了大量按线程、按账号、按对象统计的events。在连接数最高峰时p_s内部需要存储上千个连接最新的状态事件内存叠加得飞快。处理方案很简单先关闭部分不常用的统计项performance_schema_events_statements_history_size 1000 performance_schema_events_transactions_history_size 100 performance_schema_max_cond_instances 2000 performance_schema_max_rwlock_instances 1000改完后重启验证p_s的内存降到了原来的一半。再把不必要的consumer关掉UPDATE performance_schema.setup_consumers SET ENABLEDNO WHERE NAME LIKE %history%;4.3 案例三connection级buffer太多导致内存只涨不降这个案例也是最容易误判成泄漏的。DB的RSS从5G慢慢涨到11G然后稳定。因为是在业务高峰时段涨的我一开始怀疑是连接泄露但SHOW PROCESSLIST显示连接数并没有上涨。用performance_schema.memory_summary_global_by_event_name查线程级缓冲发现memory/thread/sql/main和memory/thread/sql/user历史最高值非常大但当前分配已经降下来了。也就是说存在大量的内存占用曾经被分配到很高的水位之后释放但RSS没有降下来。踩过这个坑后我明白了一件事对于线程级缓冲不能只看current_alloc还要看high_alloc和high_num。当high_alloc远大于current_alloc说明内存申请水位很高后续反复创建连接会反复把这个水位拉高mac层申请了多少就实际占用多少。处理办法是把连接池里的maxActive调小并且设置连接池的idleTimeout让空闲连接主动断开。进程RSS虽然没立刻降下来但再次重启后高峰也不会再冲破阈值了。5. 两招控制“看不见”的内存碎片和元数据开销5.1 用jemalloc替代glibc malloc先说明这不是一个运营忌讳的越线操作而是一个非常成熟的方案。MySQL高频分配小内存glibc的arena机制每个线程会预申请大块内存导致RSS虚高且不释放。这种情况下把mysqld使用的内存分配器换成jemalloc常常能节省20%-30%的物理内存。具体有三种做法在环境变量里加载动态库。mysqld_safe脚本启动时在LD_PRELOAD中指定/usr/lib64/libjemalloc.so.1。更可控的方式是修改/etc/my.cnf中mysqld_safe段加上malloc-lib/usr/lib64/libjemalloc.so。8.0部分发行版在启动脚本内支持MALLOC_LIB参数。我试过第一种方式最快但升级方式更新库时需要重启MySQL才能生效第二种最稳妥。注意务必先验证mysqld是否能正常加载新分配器。5.2 表数量庞大时额外压缩数据字典开销如果你管理的实例上表数量超过2万张请留意数据字典内存。MySQL 8.0的字典被放在InnoDB下每次打开表都会缓存元数据不能简单用table_open_cache限制。表多了以后可以这样做table_open_cache 2000 table_definition_cache 2000同时监控performance_schema.memory_summary_global_by_event_name里memory/innodb/dict相关的段。如果这一项居高不下说明业务建的临时表太多或者每个连接的auto reopen逻辑导致字典频繁重载。另一个常见的误解是盲目调大table_open_cache来提升性能。实际上只要表数不超过一万默认2000基本够用调大只会增加无效的缓存内存。6. 排查收尾别忽略了这四项长期监控内存排查结束后只‘解决’眼前的报警远远不够。你缺的是一套持续可观测的基线。下面这四项你至少要有一个模板长期跑着。RSS趋势监控每5分钟采集一次pidstat -r -p mysqld_pid的RSS数据存到一个监控图表里观察日环比、周环比。swap监控cat /proc/pid/status里Vwswap项直接反映进程是否在换页。发生swap的第一时间必须报警。current_alloc与high_alloc对比建一个定时任务把sys.memory_global_by_current_bytes里current和high差距最大的Top10记下来。连接数与线程缓冲关系采集SHOW GLOBAL STATUS LIKE Threads_connected;和Threads_running和RSS做关联曲线。很多内存突增追到最后都是连接数短时上升造成的。我之前遇到过一次半夜报警最终答案不是MySQL参数而是监控python脚本没做连接池疯狂建立短连接。如果当时有第4项监控曲线立刻就能从线程数上发现问题。说句实在话排查MySQL内存问题最怕的不是技术复杂而是定位错误之后瞎调参数。你按照“先分清占还是涨再做内存体检再对照处理最后长期监控”这条链路走绝大多数内存问题都能在一个小时内找到真凶。至于那些找不出来确凿原因的RSS高水位在确认没有swap且SQL无异常的前提下建议先不做任何激进调整保持观察别为了一次报警把buffer pool砍掉一半那才是真正的性能事故开端。