MySQL 8.0内存飙高?从performance_schema到会话缓冲的排查实战

发布时间:2026/9/17 5:30:07
MySQL 8.0内存飙高?从performance_schema到会话缓冲的排查实战 上周接了一台 MySQL 8.0 服务器的内存告警free -h一看 used 已经冲到 91%mysqld 一个进程的 RSS 就占了 4.7GB机器是 8G 内存的小规格OOM Killer 随时可能动手。查 MySQL 占用内存过大这类问题我处理过不止一二十次后台也经常有人贴同样的求助截图——大多数人第一反应是调小innodb_buffer_pool_size但很多时候真正的问题根本不在 buffer pool。这篇把我实际排查的一套流程完整梳理出来从系统层取证到 MySQL 内部定位再到改配置和验证适合被 OOM、内存告警困扰的 DBA、运维和后端开发照着走一遍。先说个反直觉的结论MySQL 吃掉大半内存在很多情况下是正常设计不是故障。真正要排查的是“超出合理基线的部分”从哪里来。1. 先把“内存过大”这件事定义清楚MySQL 正常就该吃这么多很多人看到 mysqld 占了物理内存的 60% 以上就慌了其实 InnoDB 的设计目标就是把热数据尽量留在内存里innodb_buffer_pool_size你给多大它就能用多大这是性能保障不是内存泄漏。所以排查内存问题前先分清楚你现在看到的“内存占用高”属于哪一类。1.1 从free输出判断真实压力Linux 下别只看 used 那一列重点看 available。看一个典型输出$ free -h total used free shared buff/cache available Mem: 7.6G 6.9G 142M 4.0M 583M 440M这里used 6.9G包含了 MySQL 的 buffer pool 预分配也包含了 page cache。available 440M才是真正能给新进程用的内存。如果 available 长期低于总内存的 10%或者开始频繁使用 swap那才是真的紧张。注意一个常见误判mysqld 刚启动时 RSS 可能只有 1G跑几天后涨到 4G这不代表泄漏。buffer pool 是启动后慢慢预热、慢慢填满的属于正常过程。真正要盯的是两个值总内存里available是否持续走低dmesg | grep -i oom有没有 OOM Kill 记录只要这两个没问题MySQL 吃再多内存你也别管它。1.2 MySQL 内存到底分成哪几块我在排查的时候喜欢把 MySQL 内存拆成两大块看全局内存所有线程共享。主要就是innodb_buffer_pool_size、innodb_log_buffer_size、key_buffer_sizeMyISAM 用8.0 下基本是摆设、表结构缓存、性能库占用的内存。会话内存每个连接独享。排序缓冲sort_buffer_size、连接缓冲join_buffer_size、顺序读缓冲read_buffer_size、随机读缓冲read_rnd_buffer_size、网络缓冲net_buffer_length等。全局内存是“定点定量”的配置多少就占多少比较好算。会话内存是“按需分配”的连接建立时不会一次全部分配只有真正执行到排序、join、顺序扫描时才会临时向内存要。这两个特性决定了排查思路完全不同全局内存超了去查配置会话内存爆了去查连接数和 SQL。1.3 先估算这台机器的合理基线拿 8G 内存、只跑 MySQL 单实例的场景举例。我给的一个粗略基线操作系统保留1G 左右InnoDB buffer pool3G~4G占总内存 40%~50% 相对稳妥其它全局缓存 线程内存 各种临时开销500M~1G峰值预期的会话缓冲500M~1G所以一台 8G 专机MySQL 的 RSS 在 4.5G~5.5G 之间都算正常。如果超过了这个范围或者 available 已经见底才需要进入下一步排查。我这次遇到的情况是 4.7G RSS从数字看似乎还好但 8G 的机器上总内存已经接近打满明显还有可压缩的空间于是开始一步步往里查。2. 现场取证三步走系统层、配置层、内部层各看什么排查内存问题最忌讳上来就改参数。我习惯按“系统层 → 配置层 → 内部层”的顺序取证每一步都有明确目标避免瞎猜。2.1 系统层先确认 mysqld 的真实占用用几条命令把底摸清# 确认 mysqld 的 PID 和 RSS $ pidof mysqld 23456 # 查看这个进程的内存占用 $ ps -o pid,rss,vsz,cmd -p 23456 PID RSS VSZ CMD 23456 4812456 29237824 mysqld # 或者按内存从高到低看全部进程 $ ps aux --sort-rss | head -20RSS 是常驻物理内存VSZ 是虚拟内存MySQL 的 VSZ 动辄 20G 很正常不要被它吓到。真正跟 OOM 相关的是 RSS。也可以实时观察内存变化$ pidstat -r -p 23456 1 10如果 RSS 在持续上涨涨的方向比涨的值更重要。比如涨到一定水位就稳定那是缓存预热如果沿着一条陡峭的线无限向上那大概率是会话内存泄漏或者 SQL 异常。2.2 配置层把关键内存参数一次拉全逐个查变量太慢我用一段 SQL 把所有内存相关的配置一次拿出来SHOW VARIABLES WHERE VARIABLE_NAME IN ( innodb_buffer_pool_size, innodb_log_buffer_size, key_buffer_size, max_connections, sort_buffer_size, join_buffer_size, read_buffer_size, read_rnd_buffer_size, net_buffer_length, thread_stack, thread_cache_size, table_open_cache, table_definition_cache, performance_schema, performance_schema_max_memory_classes, tmp_table_size, max_heap_table_size, internal_tmp_mem_storage_engine, temptable_max_ram );这一步是在建立“理论内存上限”的概念。每个会话缓冲都能算出一个最坏情况比如max_connections是 800sort_buffer_size是 2M理论上最坏全部连接同时排序就能吃掉 1.6G 只用于排序——虽然是极端情况但它决定了内存天花板在哪里。2.3 内部层用 performance_schema 定位内存大头配置层只能看到“可能用多少”看不到“实际用了多少”。排查大头必须看 performance_schema 的内存统计。两张表核心先看全局汇总SELECT EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_global_by_event_name ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC LIMIT 20;更省事的是直接查 sys 库的封装视图SELECT * FROM sys.memory_global_by_current_bytes ORDER BY current_alloc DESC LIMIT 20;这个视图会按“内存分类”帮你聚合输出里memory/innodb_buf_buf_pool就是 InnoDB buffer pool 的实时占用memory/performance_schema是性能库自己吃掉的内存memory/sql/...下面各个条目对应 SQL 层的缓存。拿到这张表内存大头基本就能锁定了。3. 排查实录一8.0 默认开启的 performance_schema 吃掉了 1.2G我这次排查的机器只跑了一个 MySQL 8.0.33连接数峰值也就 100 出头业务不重但 RSS 一直压在 4.7G 下不来。配置层看了一圈innodb_buffer_pool_size配置的是 3G理论上 3G 系统其它开销应该在 4G 左右才合理多出来的接近 1G 让我起了疑心。3.1 定位过程sys 视图里的大头一目了然执行SELECT * FROM sys.memory_global_by_current_bytes ORDER BY current_alloc DESC LIMIT 20;之后前几行的输出大概是这种感觉事件名称当前分配memory/innodb_buf_buf_pool3.0 GiBmemory/performance_schema/mutex_alloc412 MiBmemory/performance_schema/events_statements_history_long390 MiBmemory/performance_schema/events_statements_summary_by_digest231 MiBmemory/mysys/IO_CACHE86 MiBperformance_schema相关的几项加起来超过了 1.2G。这个很典型——MySQL 8.0 的performance_schema默认就是打开的而且默认会开启大量 instrumentation包括针对语句的完整历史记录events_statements_history_long这部分会在内存里保留每条 SQL 的完整执行信息。为什么 8.0 这么容易踩这个坑5.7 时代performance_schema虽然也默认开但默认的消费项相对克制。8.0 为了让新用户开箱即用就能看全诊断信息默认设置的采集维度更全、保存行数更多代价就是内存吃得更猛。很多云厂商的 RDS 默认关掉一部分 instrument正是为了省这点内存但自建实例可没人帮你调。3.2 处理手段关掉不需要的保存逻辑这里要区分两个层次instrument采集什么事件和 consumer采集后是否保存。临时验证可以先全局关闭所有采集看内存能不能降下来UPDATE performance_schema.setup_instruments SET ENABLED NO, TIMED NO;不过这种“一刀切”会影响你后续排查问题的能力我不建议长期用。更推荐按需保留-- 关闭 statement 的完整历史记录只保留当前语句 CALL sys.ps_setup_disable_consumer(events_statements_history_long); -- 缩小历史记录的保留行数降低内存上限 SET GLOBAL performance_schema_events_statements_history_long_size 1000;改完后观察 mysqld 的 RSS$ ps -o rss -p 23456我的实测结果是从 4.7G 降到 3.5G 左右掉了超过 1G。对被内存告警逼疯的人来说这 1G 是很可观的。如果你不需要数据库层面的深入性能诊断甚至可以直接在my.cnf里加一行performance_schemaOFF最省心。但代价是以后排查慢 SQL、锁等待的时候少了一双眼睛取舍要自己权衡。提醒一下performance_schema的参数有些是只读的改完必须重启 MySQL 才生效。改之前先看SHOW VARIABLES LIKE performance_schema%;里的变量是不是动态可调。4. 排查实录二100 个“看似正常”的连接如何偷偷吃光内存降完 performance_schema我又复现了一次高峰期的内存走势发现一个更微妙的问题RSS 会在某个时刻突然再往上跳 600M~800M过一会儿又回落。这种“尖峰型”的内存增长和 buffer pool 那种“填满后稳定”完全不一样指向的是另一个元凶——会话级缓冲。4.1 per-thread buffer 的分配时机MySQL 对每个连接会分配一组私有的内存缓冲主要包括缓冲名称默认值8.0主要用途分配时机sort_buffer_size256K排序操作执行 ORDER BY / GROUP BY 时join_buffer_size256K无索引 join 缓冲执行 JOIN 且没有可用索引时read_buffer_size1M顺序扫描表全表扫描时read_rnd_buffer_size256K排序后的随机读取使用 filesort 时net_buffer_length16K客户端连接收发缓冲连接建立时初始thread_stack256K线程栈线程创建时关键点在最后两列这些缓冲大多数不是连接建立时就真金白银地吃内存而是等到执行到对应操作时才从内存里分配。所以连接数高本身不可怕可怕的是大量连接同时在做排序、join、全表扫描。4.2 一个案例连接池风暴如何制造内存尖峰我排查的这台机器配置里sort_buffer_size2M、join_buffer_size2M、read_buffer_size1M、read_rnd_buffer_size2M都不算夸张但乘上并发数就很可观。高峰期我用下面的 SQL 查过 ThreadsSHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Max_used_connections;Threads_connected一度到了 280而业务侧有一个定时任务会同时跑一批报表 SQL这批 SQL 大量使用多表 JOIN 和 ORDER BY。假设其中有 100 个线程同时触发排序那么sort_buffer 部分100 × 2M 200Mjoin_buffer 部分100 × 2M 200Mread_buffer read_rnd_buffer100 × 3M 300M再加上 net_buffer、临时表开销、线程栈等零碎一次并发高峰轻松多出 700M~1G 的内存占用。这就是我观察到的那个 600M~800M 内存尖峰的来源。它不会像泄漏那样一直涨但高峰期一旦和系统其它进程抢内存很容易把 available 打到个位数。4.3 用 performance_schema 验证会话内存你可以按线程看内存占用把大户捞出来SELECT THREAD_ID, EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_by_thread_by_event_name ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC LIMIT 20;再关联performance_schema.threads表看看这个线程对应哪条连接、在跑什么 SQL。如果发现大量线程的memory/sql/sort_buffer、memory/sql/join_buffer都很高那答案基本就坐实了。4.4 怎么压内存又不伤正常查询这里有个取舍问题。有人一看到内存高就把sort_buffer_size从 2M 降到 256K这其实有风险。如果业务里确实有需要排序的大查询buffer 太小会导致 MySQL 频繁把中间结果刷到磁盘临时文件排序变慢IO 飙升。最后的结局是内存省下来了SQL 慢到用户投诉。我的建议是分三步走先用连接池把应用侧并发压住。大多数“连接数暴涨”都不是业务真的需要这么多并发而是连接池配置太奔放或者存在连接泄漏。把max_connections从 800 调到 300同时让应用侧连接池最大连接数和它对齐效果立竿见影。缩小单连接上限而不是直接砍半。比如sort_buffer_size2M 降到 1M影响通常可控。给不产生明显收益的缓冲设置更小值。read_buffer_size用于全表扫描如果业务 SQL 基本走索引它大多数时候用不上从 1M 降到 512K 问题不大。注意MySQL 8.0 的这些 per-thread buffer 是按需分配、用完即释放的。你看到连接数高不用担心但要盯住“同时执行排序/join 的线程数”。这比单纯看连接数更有价值。5. 大排序 / 大 JOIN / 临时表SQL 层面的内存尖峰容易误判排查完连接和 performance_schema我还遇到过一个更难定位的场景内存占用总体不高但会周期性出现非常尖锐的暴涨持续时间几秒到几十秒不等。最后查下来既不是配置问题也不是连接风暴而是一条 SQL 的临时表把内存顶上去的。5.1 内存临时表是怎么产生的MySQL 在执行GROUP BY、DISTINCT、ORDER BY、子查询、部分UNION时可能需要在内存里建立临时表来存放中间结果。这个临时表的上限受两个参数共同限制tmp_table_size默认约 16M~64M取决于版本和发行版max_heap_table_size默认 16M~64M实际生效的上限是两者取较小值。超过上限之后内存临时表会转到磁盘临时表8.0 默认引擎是 InnoDB on-disk temporary table。这里还有一个 8.0 特有的参数容易被忽略internal_tmp_mem_storage_engine。8.0 默认是TempTable对应的内存上限不是单纯由tmp_table_size控制还受temptable_max_ram限制。这个值的默认是 1G也就是说 8.0 的 TempTable 引擎最多能用约 1G 内存来缓存临时表数据超过部分会转入内存映射文件或磁盘临时表。5.2 一条 GROUP BY 把内存干上去的现场我之前处理过一个案子某张日志表有 3000 万行业务侧跑了一个定时统计SELECT user_id, COUNT(*) FROM order_log WHERE create_time BETWEEN 2024-01-01 00:00:00 AND 2024-01-31 23:59:59 GROUP BY user_id;这个 SQL 要扫描的数据量非常大分组结果可能有几十万行。执行时中间结果会先放进内存临时表如果统计的维度再复杂一点内存占用直接奔着几百 M 去。如果同一时间这类 SQL 有好几条并发内存尖峰立刻出现。排查的时候可以看状态变量SHOW GLOBAL STATUS LIKE Created_tmp%;输出示例变量名值Created_tmp_tables145231Created_tmp_disk_tables2034Created_tmp_disk_tables / Created_tmp_tables如果比例太高说明很多临时表落盘了是性能问题这个比例如果接近 0%则说明临时表都在内存里完成内存压力就可能从这里来。5.3 SQL 侧和配置侧的协同优化我的处理经验是先优化 SQL再考虑调参数。同一个报表需求如果加上合理的索引让GROUP BY走覆盖索引临时表的数据量会小一个数量级。比如上面的例子给create_time建索引、把user_id加入索引做覆盖MySQL 扫描的行数会显著降低临时表内存自然就下来了。如果 SQL 已经没法改再考虑配置侧tmp_table_size 32M max_heap_table_size 32M # 8.0 专用控制 TempTable 引擎最大内存 temptable_max_ram 1G把tmp_table_size调大能减少落盘、提升性能但代价是单个大查询可能吃更多内存调小则反之。这里没有绝对正确的值取决于你的业务是 OLTP 还是偏 OLAP。我的偏好是在 OLTP 系统里保持 16M~32M让超限的大查询尽早走磁盘临时表宁可慢一点别让一个查询把实例内存打爆。6. 重建内存基线一套可直接抄的调优方案排查到这里这台 8G 机器的问题基本定位完了performance_schema 贡献 1.2G、会话缓冲在高峰期贡献 600M~1G、临时表偶尔来一个尖峰。接下来是重新建立内存基线把配置落到一个“够用且稳”的状态。6.1 参考配置块针对一台 8G 物理内存、跑 MySQL 8.0 单实例、连接数峰值 200 以内的业务场景我给出的一个可直接参考的配置[mysqld] # InnoDB 核心 innodb_buffer_pool_size 3G innodb_log_buffer_size 16M innodb_buffer_pool_instances 4 # 连接与线程 max_connections 300 thread_cache_size 64 sort_buffer_size 1M join_buffer_size 1M read_buffer_size 512K read_rnd_buffer_size 1M # 表缓存 table_open_cache 1024 table_definition_cache 1024 # 临时表 tmp_table_size 32M max_heap_table_size 32M internal_tmp_mem_storage_engine TempTable temptable_max_ram 1G # 性能库按需保留避免默认全量采集 performance_schema ON performance_schema_events_statements_history_long_size 1000这套配置跑下来RSS 稳定在 3.5G~4G 之间available 长期保持在 1.5G 以上OOM 警报彻底解除。6.2 改完如何验证不反弹改配置不是“改完重启就完事”至少要观察一个业务周期。我习惯盯三个指标# 1. 内存总体情况 watch -n 5 free -h # 2. mysqld 本身 ps -o rss -p $(pidof mysqld) # 每分钟记录一次看是否稳定/持续上涨 # 3. 连接数和临时表 mysql -e SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Created_tmp%;如果 RSS 稳定在一个水位线不再往上爬说明基线定住了。如果还是在缓缓上涨那一定还有没查到的部分回去继续看sys.memory_global_by_current_bytes。6.3 几个容易被忽略的坑改完参数忘记看有没有!includedir。my.cnf或者my.ini里经常有一行!includedir /etc/my.cnf.d/各发行版会把额外的配置拆到单独文件里。如果你只在主配置里改拆分布置文件里还有一份旧配置覆盖了你导致修改不生效排查半天还以为自己记错了。tuning-primer.sh 和 mysqltuner 给的建议要带着判断看。这两个脚本能帮你快速列出配置风险但它们的建议是基于“最大化性能”写的比如动辄建议把innodb_buffer_pool_size调到总内存的 70% 以上。如果你的机器还要跑监控 agent、日常备份任务、偶尔有人上去查数据全给 MySQL 会适得其反。不要在高峰期直接改全局动态参数。比如SET GLOBAL sort_buffer_size XXX这类操作影响的是之后新建的连接老连接还在用旧值。而且sort_buffer_size调小后正在执行的大排序可能瞬间出现大量磁盘 filesort性能抖动比内存告警还难受。改配置要挑业务低峰期改完观察一个周期再决定下一步。根据我个人处理这类告警的经验MySQL 内存问题的排查框架永远是“先算基线、再判类型、后动配置”。90% 的内存告警要么是 baseline 定错了要么被 performance_schema 和会话缓冲这种“看不见的内存”带偏。把本文这套命令在出问题的机器上完整跑一遍你大概率也能像我一样在不牺牲核心性能的前提下把内存压回合理区间。