大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。
有一次线上,应用一直报 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,还有一层容易被忽略,就是每连接缓冲。它按会话分配,随连接数线性涨,而且上限能算出来。
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 会把内存按组件记下来。谁占得多,一眼可查。
-- 按组件看谁在占内存(换算成 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 库视图,省得自己换算。
-- 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 单独确认一下。别光看它大不大,要看命中率。
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。命中率高,说明池子够用,别再动它。命中率低还硬调小,只会更慢。
临时表这层最容易漏,也最容易爆。
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 引擎,旋钮也跟着变。
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 去调,是调不准的。
数据库自己报完账,还得回到机器上对一遍。两边对得上,才算确认。
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 时,先止痛,再治病。第一步,把危险的会话摘掉。
-- 找出跑太久、耗资源的会话
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 是缓存,占得多是好事,不该背锅。真正该抓的,是那些借了不还、只涨不落的部分。会话、临时表、碎片,这三样最像沉默的成本,平时不出声,结账时才知道贵。内存排查的功夫也不在调参,在看清。看清是谁在占,比急着释放更要紧。
你们线上数据库的内存,是涨到某条线就稳,还是一路不回头?用的又是哪套工具定位的?评论区聊聊。
我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋