内存只涨不落|拆到组件才发现占的是会话和临时表

简介: 从应用报 OOM、调小 Buffer Pool 没用切入,把 MySQL 内存分五层,用 performance_schema 逐层定位,讲清真不释放与高水位不回收两类根因,给出止血与根治。

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

有一次线上,应用一直报 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 是缓存,占得多是好事,不该背锅。真正该抓的,是那些借了不还、只涨不落的部分。会话、临时表、碎片,这三样最像沉默的成本,平时不出声,结账时才知道贵。内存排查的功夫也不在调参,在看清。看清是谁在占,比急着释放更要紧。

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

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

相关文章
|
26天前
|
存储 Oracle 关系型数据库
双轨并行架构设计:全量迁移的并行策略、增量同步的延迟基准与双向Master实现
传统数据库迁移面临停机窗口长、数据一致性难保证、故障缺乏回退手段三大结构性挑战。双轨并行方案通过“全量迁移+增量同步+双向回切”的技术组合,将迁移从“一次性大爆炸”拆解为“可验证、可回退的渐进过程”。本文从全量迁移的并行策略、增量同步的日志解析与延迟控制、双向切换的决策点设计、一致性校验的触发机制四个层次展开。
|
23天前
|
存储 SQL Linux
时序数据库InfluxDB内存30分钟从40%飙到98%——高基数问题的四步诊断与四种解法
时序数据库InfluxDB的内存使用率突然从40%飙升至98%,OOM Killer强制终止进程——这是高基数问题的典型表现。TSM引擎为每个序列维护独立的内存索引项,当标签组合爆炸时,序列数急剧增长,内存索引膨胀导致OOM。本文拆解从基数爆炸到OOM的完整链路,给出四步诊断法和四种解决方案,并分析如何从内核层面规避高基数陷阱。
|
25天前
|
人工智能 缓存 安全
企业上 Agent 之前,先把 skill 沉淀这件事想清楚
AI圈“不聊skill”并非其衰落,而是已成默认基础设施——如“上云”般无需争论。真正焦点转向三件事:模型商业变现、Agent失控风险、开放生态竞争。skill的退场,恰是AI从技术炫技迈向产业落地的分水岭。
184 0
|
6天前
|
SQL 监控 关系型数据库
一条DELETE删千万行|从库延迟飙到八分钟
从一条 DELETE 删千万行、跑半小时没完、主从延迟飙到八分钟切入,拆清一条大 DELETE 其实是一个大事务:undo 只涨不缩、行锁持有到提交、ROW binlog 撑爆带宽、回滚比执行更久。给出按主键分批的脚本、批次监控与场景判断表,并说明能归档就别硬删。
|
22天前
|
存储 监控 关系型数据库
PostgreSQL表没变大索引却膨胀了——HOT更新的两个前提条件与三个失效场景
PostgreSQL表没怎么变大,索引却膨胀了几倍——这是频繁更新的表最常见的性能退化模式。HOT(Heap-Only Tuple)更新是PostgreSQL针对这个问题的核心优化:当UPDATE不修改任何索引列,且新行能放在同一页时,可以跳过所有索引更新。但HOT有两个前提条件,任何一个被破坏,索引膨胀就会加速。本文从HOT的机制出发,拆解三个最常见的HOT失效场景,给出fillfactor调优策略和n_tup_hot_upd监控方法。
|
15天前
|
存储 关系型数据库 BI
对账差了一天|查到最后是时间字段的时区
从跨区域对账差一天切入,拆解 TIMESTAMP 与 DATETIME 的存储差异、时区三层与时区表缺失、查询索引陷阱、驱动重复换算、夏令时与主从迁移,附选型判断和避坑清单。
|
15天前
|
监控 网络协议 关系型数据库
backlog调大了却没生效——somaxconn限制与TCP连接队列的完整链路
Threads_connected远未到max_connections,应用却报“连接超时”。问题在TCP连接队列:三次握手完成但MySQL还没accept的连接,不计入Threads_connected,却受backlog队列长度限制。backlog的实际生效值受Linux内核somaxconn约束,本文拆解连接队列的完整链路、overflow的排查方法,以及连接池与内核参数的配合调优。
|
16天前
|
SQL 关系型数据库 MySQL
一个 LEFT JOIN 的空行全没了|搞懂 SQL 执行顺序才明白
LEFT JOIN 过滤条件写进 WHERE 导致左表行丢失,讲清执行顺序,复现 LEFT JOIN 变 INNER JOIN 与别名失效,区分逻辑顺序与执行计划,给出 ON、WHERE 判定规则。
|
21天前
|
存储 SQL 监控
备份任务绿勾还在|数据已经回不到对的样子
从一次恢复演练里数据是错值切入,把数据可靠性拆成可用性与正确性两层,讲清静默损坏的三类来源、页校验和与双写的检测机制及其边界,说明为什么物理备份会连同坏页一起被带走,给出页校验扫描、字段级指纹比对与恢复演练验收的做法。
|
20天前
|
SQL 固态存储 关系型数据库
SMO无法并发+加锁范围大——InnoDB B-tree索引锁的两个结构性问题
写密集、高并发场景下,MySQL吞吐量上不去,加CPU、换SSD都没用——瓶颈可能不在硬件,在InnoDB的B-tree索引锁上。B-tree的结构修改操作(SMO)一次只能跑一个,索引级锁把所有写操作串行化。本文拆解B-tree索引锁的瓶颈根源、并发SMO的技术原理、Latch Coupling的加锁优化机制,以及热点行事务合并的实践思路。

热门文章

最新文章