news 2026/9/17 5:30:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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_sizeinnodb_log_buffer_sizekey_buffer_size(MyISAM 用,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 pool:3G~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 -20

RSS 是常驻物理内存,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是 800,sort_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 GiB
memory/performance_schema/mutex_alloc412 MiB
memory/performance_schema/events_statements_history_long390 MiB
memory/performance_schema/events_statements_summary_by_digest231 MiB
memory/mysys/IO_CACHE86 MiB

performance_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_schema=OFF,最省心。但代价是以后排查慢 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_size=2Mjoin_buffer_size=2Mread_buffer_size=1Mread_rnd_buffer_size=2M,都不算夸张,但乘上并发数就很可观。

高峰期我用下面的 SQL 查过 Threads:

SHOW 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 = 200M
  • join_buffer 部分:100 × 2M = 200M
  • read_buffer + read_rnd_buffer:100 × 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_buffermemory/sql/join_buffer都很高,那答案基本就坐实了。

4.4 怎么压内存,又不伤正常查询

这里有个取舍问题。有人一看到内存高就把sort_buffer_size从 2M 降到 256K,这其实有风险。如果业务里确实有需要排序的大查询,buffer 太小会导致 MySQL 频繁把中间结果刷到磁盘临时文件,排序变慢,IO 飙升。最后的结局是内存省下来了,SQL 慢到用户投诉。

我的建议是分三步走:

  1. 先用连接池把应用侧并发压住。大多数“连接数暴涨”都不是业务真的需要这么多并发,而是连接池配置太奔放,或者存在连接泄漏。把max_connections从 800 调到 300,同时让应用侧连接池最大连接数和它对齐,效果立竿见影。
  2. 缩小单连接上限,而不是直接砍半。比如sort_buffer_size2M 降到 1M,影响通常可控。
  3. 给不产生明显收益的缓冲设置更小值read_buffer_size用于全表扫描,如果业务 SQL 基本走索引,它大多数时候用不上,从 1M 降到 512K 问题不大。

注意:MySQL 8.0 的这些 per-thread buffer 是按需分配、用完即释放的。你看到连接数高不用担心,但要盯住“同时执行排序/join 的线程数”。这比单纯看连接数更有价值。

5. 大排序 / 大 JOIN / 临时表:SQL 层面的内存尖峰容易误判

排查完连接和 performance_schema,我还遇到过一个更难定位的场景:内存占用总体不高,但会周期性出现非常尖锐的暴涨,持续时间几秒到几十秒不等。最后查下来,既不是配置问题,也不是连接风暴,而是一条 SQL 的临时表把内存顶上去的。

5.1 内存临时表是怎么产生的

MySQL 在执行GROUP BYDISTINCTORDER 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_tables145231
Created_tmp_disk_tables2034

Created_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 几个容易被忽略的坑

改完参数忘记看有没有!includedirmy.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 和会话缓冲这种“看不见的内存”带偏。把本文这套命令在出问题的机器上完整跑一遍,你大概率也能像我一样,在不牺牲核心性能的前提下把内存压回合理区间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 5:27:54

高校后勤报修系统开发:Python+Django全栈实践

1. 项目背景与需求分析高校后勤报修系统是校园信息化建设中的重要组成部分。传统报修方式存在诸多痛点:电话报修容易占线、纸质登记易丢失、维修进度不透明、数据统计困难等。我们团队开发的这套系统正是为了解决这些实际问题。从技术角度看,这个系统需要…

作者头像 李华
网站建设 2026/9/17 5:27:40

无代码AI智能体落地指南:从选型到全托管PaaS实践

直接说结论:现在的企业做 AI 智能体,早就不是技术竞赛,而是选型竞赛。你团队里有没有专职算法工程师?没有的话,无代码方案就是你的主力路线;你有没有时间和精力去维护 GPU、向量库、推理服务、限流、监控一…

作者头像 李华
网站建设 2026/9/17 5:27:37

CANN挑战赛赛题解析:算子开发与模型迁移实操指南

9月10日下午4点那场CANN挑战赛的赛题解析直播,我蹲完了全程,边看边记了不少东西。说实话,赛题刚放出来的时候,很多人第一反应是"这题看着不难啊",但真动手做起来,才发现坑比想象中多。我自己前后…

作者头像 李华
网站建设 2026/9/17 5:27:33

Agent落地四块基石:Skill、后训练、世界模型与MCP/A2A

1. 这不是“更聪明的聊天机器人”,而是工作流重构的临界点最近在几个技术闭门会上,我反复听到一句话:“Agent 能聊得天花乱坠,但一到真干活就卡壳。”——这话听着刺耳,但实测下来,几乎每家落地 Agent 的团…

作者头像 李华
网站建设 2026/9/17 5:27:12

OpenCV三维重建实战:从相机标定到点云生成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 5:26:41

车载CAN-LIN网关刷写升级与OTA实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华