MySQL内存占用过高怎么排查?分层定位与OOM根因

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

有一次线上,应用一直报 OOM,监控上数据库的内存曲线只涨不落。我第一反应是 Buffer Pool 出了问题,就去把它调小了。结果内存没降下来,查询反而更慢。折腾半天才搞明白,涨上去的那块,根本不在 Buffer Pool。

那次之后我记牢一件事。内存打满,先别急着调池子,得先看清是谁在占。数据库的内存不是一个数,是好几层叠起来的。你把它当一个总量去调,多半调错地方。

内存被谁占了

要看清是谁在占,先得知道内存分几层。MySQL 的内存,粗略分五层。Buffer Pool 是最大的一块,但常常不是涨的那块。

层 谁在占 会不会只涨不落
Buffer Pool 数据页、索引页、undo、change buffer、自适应哈希 会,但它是缓存,高是好事
每连接缓冲 read / sort / join buffer、net buffer 会话活着就一直占着
内存临时表 GROUP BY、子查询物化 并发一高就暴涨
元数据/其他 table_open_cache、log buffer、key_buffer 一般不主导
性能库 performance_schema 自身 固定开销,可观

先记住一句话。Buffer Pool 高占用,是缓存在命中,是数据库在好好干活。它不该是你排查的第一嫌疑,除非它被设得超过物理内存。

除了 Buffer Pool,还有一层容易被忽略,就是每连接缓冲。它按会话分配,随连接数线性涨,而且上限能算出来。

sql 复制代码
SHOW VARIABLES WHERE Variable_name IN (
  'read_buffer_size','read_rnd_buffer_size','sort_buffer_size',
  'join_buffer_size','binlog_cache_size','max_allowed_packet','thread_stack');

把这几项加一加,再乘上连接数,就是这一层的潜在上限。注意是「潜在」。这些缓冲按需分配,平时用不满。但要留个心眼,sort_buffer_size 和 join_buffer_size 是每次排序、每次连接操作各算一份。一个会话连跑几条大排序,累起来很可观。

逐层定位:先看组件,再看进程

分完层,接下来的问题是:每一层怎么查。以前我只会看 top,看到 mysqld 的 RES 高就慌。后来才知道,那里面混着 page cache 和 malloc 碎片,水分很大。现在我先看数据库自己报的账。

MySQL 5.7 起,performance_schema 会把内存按组件记下来。谁占得多,一眼可查。

sql 复制代码
-- 按组件看谁在占内存(换算成 MB)
SELECT EVENT_NAME,
       CURRENT_NUMBER_OF_BYTES_USED/1024/1024 AS mb
FROM performance_schema.memory_summary_global_by_event_name
ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC
LIMIT 15;

8.0 还有更顺手的 sys 库视图,省得自己换算。

sql 复制代码
-- sys 库:全局内存大户
SELECT * FROM sys.memory_global_by_current_bytes LIMIT 10;
-- 按线程看:哪类会话在占
SELECT * FROM sys.memory_by_thread_by_current_bytes
ORDER BY current_allocated DESC LIMIT 10;

Buffer Pool 单独确认一下。别光看它大不大,要看命中率。

sql 复制代码
SELECT @@innodb_buffer_pool_size/1024/1024 AS bp_mb,
       @@innodb_buffer_pool_instances      AS instances;
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_%';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';

命中率大概这么算:1 减去 Innodb_buffer_pool_reads 除以 Innodb_buffer_pool_read_requests。命中率高,说明池子够用,别再动它。命中率低还硬调小,只会更慢。

临时表这层最容易漏,也最容易爆。

sql 复制代码
SHOW GLOBAL STATUS LIKE 'Created_tmp%';   -- 看落盘比例
SHOW VARIABLES LIKE 'tmp_table_size';
SHOW VARIABLES LIKE 'max_heap_table_size';

内存临时表的实际上限,是 tmp_table_size 和 max_heap_table_size 里小的那个。它一旦被撑爆,临时表就落到磁盘,Created_tmp_disk_tables 开始涨。这既是一次内存事件,也是一次 IO 事件。

注意版本。上面这套是 5.7 及以前 MEMORY 引擎的逻辑。8.0 换成了 TempTable 引擎,旋钮也跟着变。

sql 复制代码
SHOW VARIABLES LIKE 'internal_tmp_mem_storage_engine';  -- TempTable 还是 MEMORY
SHOW VARIABLES LIKE 'temptable_max_ram';                -- 内存临时表上限
SHOW VARIABLES LIKE 'temptable_max_mmap';               -- 超过后 mmap 的上限

8.0 下,内存临时表涨过 temptable_max_ram,不会直接落盘,而是先尝试 mmap 文件,涨到 temptable_max_mmap 才真正走磁盘。所以 8.0 还照 5.7 的 tmp_table_size 去调,是调不准的。

数据库自己报完账,还得回到机器上对一遍。两边对得上,才算确认。

bash 复制代码
top -p $(pgrep -x mysqld)      # 只看 RES,别被 VIRT 吓到
cat /proc/$(pgrep -x mysqld)/status | grep VmRSS
pmap -x $(pgrep -x mysqld) | tail -1

两类根因,别混为一谈

定位到最后你会发现,涨上去的内存无非两类:真不释放,和高水位不回收。

真不释放,常见两个来源。一是长驻会话。连接池把连接一直留着,每条会话的 sort、read 缓冲用过高水位,就一直占着不放。max_connections 越大,这块上限越高。二是内存临时表并发暴涨。参数设得太大,几十个并发一起建大临时表,内存瞬间见底。

高水位不回收,更多是机制,不是 bug。Buffer Pool 一旦涨上去,不会主动缩;OS 的 page cache 也会把一份页缓在内存里,等于和 Buffer Pool 存了两份。这不是泄漏,是设计。

那份重复的 page cache,其实能省。把 innodb_flush_method 设成 O_DIRECT,InnoDB 读写就绕过 OS 的页缓存,只在 Buffer Pool 里存一份。省下的内存,反哺给 Buffer Pool 正合适。

还有一层不在数据库里,在进程自己身上。Linux 的 glibc 给每个线程一个内存 arena,最多开到 8 倍核数。线程用完的内存,退给 arena,却不退给操作系统。这就是为什么数据库里查不到它,top 的 RES 却下不来。压一压用 MALLOC_ARENA_MAX,或者干脆换 jemalloc。

分清这两类,处置完全不同。前者要抓出来释放,后者根本不用管。

先止血,再根治

分清了是哪一类,处置就有方向了。线上正在 OOM 时,先止痛,再治病。第一步,把危险的会话摘掉。

sql 复制代码
-- 找出跑太久、耗资源的会话
SELECT id, user, time, state, LEFT(info, 60) AS sql_head
FROM information_schema.processlist
WHERE command <> 'Sleep' AND time > 60
ORDER BY time DESC;
-- 确认后用 KILL 摘掉

第二步,临时表该落盘就落盘。把 tmp_table_size 调小,让大临时表走磁盘,用 IO 换内存。这是权宜,不是答案。第三步,限制并发和入参,max_allowed_packet 这类别设得过大。

止血之后,再看根治。参数上,Buffer Pool 按物理内存给,专用库一般占五到七成,别超过内存总量。每连接的缓冲参数,除非有实据,尽量用默认。把 sort_buffer_size 设成几个 G 是大忌,它是每个会话、每次排序都各算一份。

治理上,连接池卡上限、空闲连接及时回收。监控上,把进程 RSS、Buffer Pool 用量、连接数、临时表落盘率这几条线立起来,OOM 之前先报警。止损靠临时手段,根治靠参数、连接和监控一起收口。

判断力

内存这事,最忌讳只看一个总量就下手。很多人一看到使用率到九成就慌,其实该先问一句:涨上去的那块,创造价值了没有。

Buffer Pool 高,是缓存在干活,那是好内存,不该动。真正要盯的,是只涨不落、又不产出缓存收益的那部分。长驻会话攥着的连接缓冲,堆在高并发里的临时表,还有借了不还的内存碎片,它们才是真正被吃掉的。

所以顺序永远是先分层、再定位,最后才谈收缩还是释放。先看清是谁在占,再决定动谁。顺序反了,你动的往往是最不该动的那层。

避坑清单

连接缓冲是按连接数线性涨的,别只盯 Buffer Pool。max_connections 调大一时爽,内存上限跟着水涨船高。

临时表超限会落盘,它既是内存问题,也是 IO 问题。tmp_table_size 设太大,等于把雷埋进高并发里,平时看不见,一忙就响。

最后一条是我自己踩的。我一开始拿 top 的 RES 当数据库的真实占用,被 page cache 和 malloc 的碎片骗过。RES 高,不等于 InnoDB 真占了那么多。现在我都先用 performance_schema 按组件拆一遍,再去 OS 层对齐。两个数对不上,才知道该往下查哪。

写在最后

数据库内存涨,从来不是一个「调小参数」就能解的问题。它更像在查一笔账:先分层,再定位到具体那层,最后才决定收缩还是释放。

Buffer Pool 是缓存,占得多是好事,不该背锅。真正该抓的,是那些借了不还、只涨不落的部分。会话、临时表、碎片,这三样最像沉默的成本,平时不出声,结账时才知道贵。内存排查的功夫也不在调参,在看清。看清是谁在占,比急着释放更要紧。

你们线上数据库的内存,是涨到某条线就稳,还是一路不回头?用的又是哪套工具定位的?评论区聊聊。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关推荐
Leo.yuan1 小时前
2026穿透式监管系统选型避坑:数据接不进、风险穿不透、系统难联动,哪些平台能解决?
大数据·数据库
刘胡子大叔1 小时前
旧 SQL 在新 PG 上能不能跑
数据库·sql
毕设单片机1 小时前
springboot志愿服务平台的设计与实现 36731
java·spring boot·mysql·志愿服务平台
Elastic 中国社区官方博客1 小时前
教程:监控 Java 应用
java·开发语言·数据库·elasticsearch·搜索引擎·全文检索
xixingzhe22 小时前
秒杀库存三种缓存实现方案
数据库·redis·缓存
总线通信小百科2 小时前
车载以太网转换器品牌对比:速率、接口、API与故障测试能力
经验分享·adas
软件交付观察室2 小时前
数据大屏总数与明细为什么对不上?2026项目开发排查指南
软件测试·数据库·sql·sqlite·数据可视化
倔强的石头_2 小时前
数据集成软件怎么选 研究了一圈,我盯上了金仓KFS
数据库