1. 故障现象:MySQL 内存占用到底该怎么看
先说个真实场景。去年我接手一台线上服务器,配置是 16G 内存,跑着 MySQL 8.0 和几个 Java 应用。某天监控报警,说内存使用率飙到 95% 以上。我登录服务器一看,free -h显示 used 接近 15G,但细细一算,Java 应用加起来也就占了 6G 左右,剩下的内存几乎全被 mysqld 吃掉了。
很多朋友一遇到 MySQL 内存占用高,第一反应是“是不是内存泄漏了”,然后急着重启数据库。其实这个结论下得太早。MySQL 内存占用高,有相当一部分是正常现象,特别是 InnoDB 引擎的缓冲池和操作系统的 Page Cache 机制,会导致内存看着很高,但实际并不影响性能。真正需要警惕的是那部分“只涨不降、涨到系统 OOM”的内存。
排查之前,先把口径统一了。Linux 下看内存,别只看free -m里的 used 列,要看这几项:
| 指标 | 含义 | 说明 |
|---|---|---|
| total | 物理内存总量 | 服务器实际内存 |
| used | 已使用内存 | 包含 cache/buffer |
| free | 完全空闲的内存 | 不含任何缓存 |
| available | 可分配给新进程的内存 | 包含可回收的 cache |
| buff/cache | 文件缓存 | 可回收,不用太紧张 |
重点记住一句话:free命令里的 used 包含了 buff/cache,这部分内存是 Linux 的文件页缓存,MySQL 读了磁盘数据后,操作系统会把这些数据留在内存里,下次读取直接命中缓存,速度飞快。如果 MySQL 内存高主要高在 buff/cache 上,这其实是好事,根本不叫问题。真正需要排查的是RES(常驻内存)这一列,也就是 mysqld 进程真正占用的物理内存。
所以第一步不是急着优化,而是先精确定位:到底是 mysqld 的哪个内存区域吃掉了内存,是 InnoDB Buffer Pool,还是连接线程,还是排序缓冲,还是其他临时结构。定位错了方向,后面所有的优化都白做。
2. 先分清:正常占用 vs 异常占用
2.1 InnoDB Buffer Pool 是内存大户,但这不算故障
InnoDB Buffer Pool(以下简称 BP)是 MySQL 内存占比最大的部分,默认配置下可能占物理内存的 60% 到 80%。它缓存的是表数据页和索引页,相当于把磁盘上的热点数据预加载到内存里,减少磁盘 IO。
举个生活化的例子,BP 就像你家楼下的便利店,老板会把卖得最好的几款饮料放在冰柜最显眼的位置。你每次来买,不用让店员去仓库翻,直接拿起就走。如果冰柜太小,每次都要去仓库取,那顾客体验就很差。MySQL 的 BP 就是这个冰柜,大小决定了一半以上的性能。
既然 BP 是“缓存”,它占内存就是合理的。但问题在于:如果innodb_buffer_pool_size配得太大,超过了物理内存减去系统和其他应用所需的总和,它就会跟别的进程抢内存,严重时触发 OOM Killer,直接把 mysqld 杀掉。我在云服务器上见过不少事故:用户把 4G 服务器上的 MySQL 配置直接套用了 32G 服务器的配置文件,innodb_buffer_pool_size=24G,结果系统一启动就卡死,日志里全是 out of memory。
2.2 连接线程和排序缓冲:临时分配却常被忽略
如果说 BP 是稳定占用,那连接线程和各类临时缓冲就是“喝水”型占用:来一个连接就分配一部分内存,连接一多,内存水涨船高。
MySQL 每个连接在服务端至少要分配两个缓冲区:sort_buffer_size和join_buffer_size。注意,这两个参数是“每连接、每操作”都会分配的,不是全局共享。假设你的sort_buffer_size=4M,同时有 200 个连接在跑 ORDER BY 排序,那光排序缓冲就可能吃掉 800M。同理,join_buffer_size在BKA、BNL等连接算法中也按连接分配。再加上read_buffer_size、read_rnd_buffer_size、net_buffer_length,一个活跃连接的内存开销随随便便能到十几兆甚至几十兆。
更隐蔽的是performance_schema。这个库默认是开启的,它会在内存里维护大量统计表,比如每个线程的事件记录、每个语句的摘要、锁等待信息等。我排查过一个小型实例,业务量并不大,但performance_schema居然吃了 2G 多内存。这个参数不像 BP 那样显眼,但它真的会在后台悄悄积累。
还有临时表。MySQL 8.0 默认临时表存储引擎是 TempTable,内存中的临时表达到一定阈值后才转磁盘。如果业务里有大批量 GROUP BY、DISTINCT、子查询,临时表内存占用会瞬间暴涨。
2.3 关键认知:MySQL 不释放内存是常态
这里必须强调一个很多新手想不通的问题:为什么我删掉了大量数据,内存却不降?
原因有两个层面。
第一,InnoDB Buffer Pool 本身是“缓存”,它里面存的数据页即使被删除和修改,只要内存足够的空间,MySQL 不会主动淘汰这些页。只有当需要加载新数据页且内存池已满时,才会按照 LRU 算法换出一些旧页。所以删数据不会导致 BP 内存下降,这是设计使然。
第二,glibc 的malloc和free机制决定了,进程释放的内存未必交还给操作系统,而是留在进程的堆内存空闲链表中,供后续的分配复用。这是个优化策略:假设 MySQL 某个瞬间并发很高,分配了 2G 内存做排序,排序完成后,这 2G 内存并不会立刻归还给系统。如果下一次查询又需要排序,直接复用这 2G 空闲内存,比重新向内核申请要快得多。这个机制叫arena缓存。所以在top里看着 mysqld 的 RES 很高,不代表它一直在用这些内存,只能说“它曾经用过这些内存”。
理解了这一点,你就明白为什么网上很多教程教你“重启 MySQL 释放内存”是治标不治本。重启后内存确实降下来了,但重新跑热数据后内存又会涨回来,而且重启期间业务中断,代价远大于那点内存收益。除非内存真的已经成为瓶颈且无法通过参数调整解决,否则不要用重启来“释放内存”。
3. 系统层面排查:定位内存到底被谁吃掉
3.1 用 top 和 pidstat 确认 mysqld 的实际内存
排查第一步,用top看进程排行:
top -o %MEM按内存降序排列,找到 mysqld 那一行,记下 PID,然后用如下命令观察 mysqld 的实时内存变化:
pidstat -r -p PID 1 10pidstat会输出minflt/s(每秒缺页数)、majflt/s(每秒主缺页数)、VSZ和RSS。如果你发现majflt/s频繁出现非零值,说明 MySQL 在不断访问磁盘上的数据页,内存可能真的不够用了;如果RSS一直稳定高位且majflt/s为零,说明数据都在内存里,性能是好的,只是数字看着吓人。
再看一下整机的内存压力:
cat /proc/meminfo重点关注CommitLimit和Committed_AS。Committed_AS是内核承诺给所有进程的内存总和,如果它超过CommitLimit,系统启用 overcommit 后可能触发 OOM。
3.2 查看 MySQL 内部内存统计视图
MySQL 5.7 及以上版本提供了内存统计表,可以直接查询各内存组件的使用量:
SELECT * FROM sys.memory_by_thread_by_current_bytes;这个视图按线程展示当前内存占用,能看到哪些连接最“吃”内存。如果某个连接的内存占用异常高,定位到PROCESSLIST_ID后,可以结合SHOW PROCESSLIST看它在执行什么 SQL。
再查全局的内存消耗状况:
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;这个查询会列出所有内存事件(memory event)的当前使用量。我一般按字节数排序,重点看前二十项。里面常见的几类如下所示。
| EVENT_NAME 关键字 | 对应内存区域 |
|---|---|
| memory/innodb/buf_buf_pool | InnoDB 缓冲池 |
| memory/sql/Query_cache | 查询缓存(8.0 已移除) |
| memory/sql/thd::main_mem_root | 连接线程基础内存 |
| memory/sql/THD::mem_root | 语句执行内存 |
| memory/sql/derived_table | 派生表 |
| memory/performance_schema | 性能字典自身内存 |
如果performance_schema相关的 EVENT_NAME 占到了总内存很大比例,可以考虑关闭它或缩小它的采集项配置。生产环境中很多性能监控工具(如 PMM、Prometheus mysqld_exporter)依赖它提供指标,不能全关,但可以调整performance_schema_*_size相关参数来控制采集表的大小。
4. 参数优化:抓住核心配置逐项调整
4.1 InnoDB Buffer Pool:设置合理的上限
innodb_buffer_pool_size是 MySQL 内存设置里最重要也最敏感的一个参数。设置公式没有绝对标准,但有一条安全底线:总内存的 50% 到 70%,同时必须留出给操作系统和 MySQL 其他结构(连接线程、排序缓存、临时表、binlog cache 等)的余量。
如果服务器上只有 MySQL 一个数据库服务,没有部署其他重量级应用,可以参考:
总内存 8G: innodb_buffer_pool_size = 4G ~ 5G 总内存 16G: innodb_buffer_pool_size = 8G ~ 10G 总内存 32G: innodb_buffer_pool_size = 16G ~ 20G 总内存 64G: innodb_buffer_pool_size = 32G ~ 40G注意,MySQL 8.0.30 及以后可以设置innodb_buffer_pool_size为动态值,不需要重启。但是调整后要观察一段时间,不能一次性从 4G 调到 20G,容易在初始化缓冲池时造成性能抖动。
还有一个细节:innodb_buffer_pool_instances。如果 BP 设置得很大(比如超过 16G),建议把实例数调大,比如设成 8 或 16。每个 Buffer Pool Instance 有独立的 free list 和 mutex,能减少并发访问时的锁竞争。这个参数在 MySQL 8.0.28 之后可以在线调整,但注意它只能整倍数变化。
4.2 连接数与各类 buffer:防止“蚂蚁搬家”式占用
max_connections这个参数是内存占用的乘法器。每增加一个连接,就要预留一组线程栈、net buffer 和可能用到的排序/连接缓冲。如果max_connections设成 1000,但实际业务只有 200 并发,那剩下的 800 个连接虽然没建立,但 MySQL 需要按最大值预留线程缓存,内存开销照样存在。
建议按实际业务并发设置,一般是 200 到 500,不要盲目设成 32767。配合max_used_connections指标观察,线上运行一段时间后,看SHOW GLOBAL STATUS LIKE 'Max_used_connections'的值,max_connections设置为这个峰值的 1.5 到 2 倍即可。
排序和连接缓冲的调整策略更简单:能小则小,需要用时再临时调大单个会话的值。
sort_buffer_size = 2M join_buffer_size = 2M read_buffer_size = 1M read_rnd_buffer_size = 1M很多教程把sort_buffer_size设成 64M,这是误人子弟。排序缓冲按连接分配,200 个连接每个 64M,瞬间 12.8G 没了。通常 2M 到 4M 完全够小查询用,大排序可以通过增大磁盘临时表的容量或者优化 SQL(比如加索引避免 filesort)来解决,而不是靠堆内存。
如果你有特定的大排序需求,可以在会话级别临时设大:
SET SESSION sort_buffer_size = 64 * 1024 * 1024;这样只影响当前连接,不会波及全局。
4.3 临时表内存阈值:避免 Group By 拖垮内存
如果业务查询中有大量GROUP BY、DISTINCT或派生表,临时表会频繁从内存表转成磁盘表。这里有一个关键参数tmp_table_size和max_heap_table_size,两者取较小值作为内存临时表的大小上限。
默认配置下这两个参数可能是 16M 到 32M,对于复杂报表查询确实不够。但是同样要注意“每连接分配”的特性。我通常把这两个参数设置在 64M 到 128M 之间,然后监控Created_tmp_disk_tables和Created_tmp_tables状态值。如果磁盘临时表占比过高,说明内存临时表太小,SQL 需要中转磁盘;如果内存临时表占用本身异常高,说明 SQL 写得有问题,需要优化而不是加内存。
判断方法:
SHOW GLOBAL STATUS LIKE 'Created_tmp%';当Created_tmp_disk_tables / Created_tmp_tables的比例大于 25% 时,才值得考虑调大内存临时表参数;否则保持默认即可。
4.4 performance_schema:低调的内存吞噬者
MySQL 5.7 之后performance_schema默认开启,意在采集各类性能事件。但它内部每个采集项都有内存开销。举个例子,events_statements_summary_by_digest会缓存所有 SQL 语句的指纹摘要,默认情况下最多缓存 1000 种语句模式,如果业务里动态 SQL 非常多,这个表会持续增长。
处理方式有两个方向:如果线上不需要细粒度性能诊断,可以关闭:
performance_schema = OFF但副作用是很多监控工具(比如 mysqld_exporter 的部分指标)拿不到数据。所以我更推荐第二个方式:按需裁剪采集项。
performance_schema_max_table_instances = 1000 performance_schema_max_thread_instances = 500 performance_schema_max_statement_classes = 200具体裁剪哪些项,要结合查询结果来看:
SELECT EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_global_by_event_name WHERE EVENT_NAME LIKE '%performance_schema%' ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC;把占用最大的几个采集项单独设小。这需要一点耐心,但效果明显。
4.5 其他容易被忽略的内存选项
下面这几个参数虽然不显眼,但在特定业务场景下能影响几百兆甚至上G的内存。
table_open_cache:打开表缓存,每个缓存条目都有元数据和分区信息。如果表非常多(几千张),这个值设成 2000 的默认值可能不够用,但设成 10000 又会增加内存。结合Open_Tables和Opened_tables状态值来判断。innodb_log_buffer_size:默认 16M 通常没问题。如果日志写入非常频繁(大量更新、删除),可以调到 64M,减少日志刷盘次数。这个参数对内存影响不大,但容易被忽略。innodb_adaptive_hash_index:自适应哈希索引,默认开启。它在高并发点查场景能提升性能,但也会消耗内存。如果表上的等值查询不多,或者内存非常紧张,可以考虑关闭。注意,这个参数在 8.0 中仍然是动态的,修改后观察响应性能再确定是否长期生效。max_heap_table_size和tmp_table_size:前面提过,放一起说,取小值生效。
再补充一个binlog_cache_size,它只在事务提交时分配,默认 32K。如果业务中有大批事务,这个值过小会导致磁盘临时写入频繁,过大则每个连接都要预留空间。一般不要超过 1M,除非你确认有大事务场景。
5. 实操记录:一次 16G 内存服务器的完整排查过程
5.1 现场信息收集
为了让你少走弯路,我用案例方式完整走一遍流程。假设这是一台 16G 内存的云服务器,运行 MySQL 8.0.34,部署了 4 个 Java 微服务(合计占用约 4G 内存)。报警记录显示内存使用率 93%,但服务并未宕机,只是频繁触发 swap。
登录服务器后的操作顺序如下。
第一步,确认整机内存分配:
free -h输出结果为:
total used free shared buff/cache available Mem: 15Gi 8.1Gi 1.2Gi 181Mi 5.7Gi 7.0Giused 8.1G,available 7G,表面上看还有余量,但能看到已经出现 swap 使用了。再看 swap:
swapon --show发现 swap 分区已经用掉 512M,说明内存压力确实存在,不止是“看着高”这么简单。
第二步,定位 mysqld 的实际内存占用:
top -p $(pgrep mysqld) -b -n 1mysqld 的 RES 显示 6.8G。然后查看 MySQL 内部的详细内存统计:
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;结果中最显眼的几项:
- memory/innodb/buf_buf_pool:5.0G
- memory/sql/THD::mem_root:380M
- memory/sql/derived_table:226M
- memory/performance_schema:800M
- memory/innodb/adaptive_hash_index:365M
从这个结果能看到两个异常点:连接线程相关内存 380M 偏高,说明当前活跃连接数量不少,且涉及复杂查询;derived_table 占 226M,说明有大量派生表在内存中处理;performance_schema 占 800M,对于一个 16G 服务器来说偏大。
第三步,查看当前连接数和状态:
SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections';Threads_connected当前 280,Max_used_connections340。再查看SHOW PROCESSLIST,发现其中有大量来自同一个应用服务的连接,正在执行带ORDER BY的列表查询。
第四步,确认这些连接的内存分配参数:
SHOW VARIABLES LIKE 'sort_buffer_size'; SHOW VARIABLES LIKE 'join_buffer_size'; SHOW VARIABLES LIKE 'max_connections';排序缓冲 4M、连接缓冲 4M、最大连接数 1000。按最大连接数 1000 算,光排序和连接缓冲就可能分配 8G,加上基础线程内存,符合现状。
5.2 优化动作与效果
基于现场数据,我做了以下调整:
innodb_buffer_pool_size从 6G 调到 5G。为什么调小?不是因为它本身不正常,而是因为这台服务器还要跑 Java 应用,又开启了 swap。留出更多内存给系统级缓存和 Java 堆,避免 OOM。max_connections从 1000 调到 500。根据Max_used_connections只有 340,500 完全够用,但在内存层面砍掉了近一半的预留线程空间。sort_buffer_size和join_buffer_size都从 4M 降到 2M。新版 MySQL 是动态分配机制,小查询用不了那么多,大查询在会话级临时调大。performance_schema保持开启,但把一些非必要的采集表关掉:
UPDATE performance_schema.setup_instruments SET ENABLED = 'NO' WHERE NAME LIKE 'wait/%';注意这个操作要谨慎,会影响后续诊断数据的收集。我更推荐在配置文件里加入:
performance_schema_max_table_instances = 500 performance_schema_max_thread_instances = 300 performance_schema_max_statement_classes = 150tmp_table_size从 32M 调到 64M,同时把max_heap_table_size也调到 64M。
调整完后重启 MySQL(参数里有需要重启生效的项),观察两周。
第一天的内存情况:
mysqld RES: 5.1G free available: 9.4G 使用率: 42%七天后的稳定状态:
mysqld RES: 5.4G free available: 8.2G 使用率: 48% swap 使用 0K整体内存压力大幅缓解,且从监控看查询响应时间波动不大。我特别注意到,由于 sort_buffer 调小,部分复杂排序 SQL 的执行时间从 80ms 涨到 120ms,可接受,但如果业务更敏感,我会用创建合适索引来消除 filesort,而不是开大 buffer。
5.3 经过较长时间运行,操作系统关闭了大页内存
这里有个后验细节。服务器在调整后跑了大概一个月,某天突然发现free -h里 buff/cache 涨到了 7G 多,而 mysqld 的 RES 只有 3.8G。一开始有点疑惑,以为是 MySQL 数据访问变少了。后来查了 IO 状态,发现是业务在做一个大范围的表扫描操作,操作系统把扫描到的数据页都缓存下来了,属于正常现象,不需要处理。数据扫描结束,buff/cache 会逐步回落。
所以说,内存优化不是一次性工作,需要持续监控并理解各类内存的真实含义。看到内存高先别慌,区分“缓存型占用”和“消耗型占用”,前者是朋友,后者才是敌人。
6. 常见问题与排查技巧实录
6.1 问题一:为什么执行大查询后内存涨了不降
这是最高频的问题。前面讲过 glibc 的 arena 缓存机制,这里再展开一下。
当 MySQL 执行一个需要 500M 排序缓冲的查询时,malloc会向内核申请 500M 内存。查询执行完,调用了free,这 500M 内存被释放,但 glibc 并没有马上用munmap把内存段归还内核,而是放在进程的空闲链表中。这样下次排序时,MySQL 可以直接从空闲链表取内存,省去了系统调用开销。
从外部看,mysqld 的 RES 一直保持在高位,即使空闲链接都释放了也不会降。这不能通过“调参数”来解决,因为这是运行库层面为了性能而做的取舍。应对办法有两个方向:
第一种,如果内存非常紧张,想强制让进程归还内存,可以用malloc_trim相关工具,但 MySQL 官方并未内置,需要挂载到进程里操作,风险很高。生产环境我不建议这样做。
第二种,从源头限制单条大查询的内存使用。在 MySQL 中没有像max_memory_size这样直接限制所有内存去向的单一变量,但可以限制单条 SQL 的执行时间(max_execution_time),或者对特定用户设置资源组限制。MySQL 8.0 的资源组功能可以给线程组设定 CPU 和内存限制,但内存限制只有 virtual memory 维度,实际效果有限。
所以我的结论是:大查询导致的内存残留,能不处理就不处理。只要还有 available 内存,硬件层面撑得住,就别折腾。真到了 OOM 边缘,优化 SQL 才是治本。
6.2 问题二:mysqld 被 OOM Killer 杀死,常见模式
一种常见模式是:MySQL 的innodb_buffer_pool_size设成了物理内存的 80%,再加上连接线程、临时表、排序缓冲的消耗,在业务高峰一下越过内存红线,触发内核 OOM Killer。
因为 OOM Killer 的分值(oom_score)是根据进程内存占用大小来定的,mysqld 作为最大的内存占用者,经常被优先选中。
查看内核日志确认:
dmesg | grep -i oom journalctl -k | grep -i oom如果找到类似 “Out of memory: Kill process 12345 (mysqld), score 750” 的记录,说明确实是 mysqld 被杀了。
对策:修改/proc/PID/oom_score_adj保护 mysqld 进程。写进 systemd service 文件的ExecStartPre或/etc/sysctl.conf都行,但最保险的方式是在 MySQL 的 systemd unit 里加:
OOMScoreAdjust=-800把 mysqld 的 OOM 分数调低,让它不容易被选中。但这不是万能药:如果系统整体内存真的耗尽,内核还是会根据各种因素挑一个进程杀掉,所以 OOMScoreAdjust 只起到降低概率的作用。根子还是要把内存占用控制住。
另外,如果使用非 systemd 方式管理 MySQL,可以在启动脚本里加上:
echo -800 > /proc/$(pgrep mysqld)/oom_score_adj这种“保护”仅限手动启动场景,且每次启动后都要执行。
6.3 问题三:MySQL 内存缓慢增长,怀疑是内存泄漏
MySQL 本身的稳定性已经很高,真正因为代码缺陷导致的内存泄漏相对少见。更多情况是以下原因:
第一,performance_schema的某些表如果开启了sum字段更新,会持续为语句、等待事件分配内存,导致内存曲线缓慢上升。针对这个场景,可以定期清理:
TRUNCATE TABLE performance_schema.events_statements_history_long;第二,Prepared Statement 没有正确释放。客户端每执行一次PREPARE就会在服务端保存一份解析后的 SQL 和参数类型,如果连接不断开,这些资源不会释放,只会在超过max_prepared_stmt_count或连接关闭时才清理。检查应用层是否有PreparedStatement未关闭的逻辑,是 MySQL 内存慢涨排查中的高性价比动作。
第三,临时内存表碎片化。内存表(MEMORY 引擎或者 TempTable 引擎)在频繁增删后会产生碎片,导致内存总量只涨不降。如果是 MEMORY 引擎的业务表,考虑改造成 InnoDB 并优化索引。
如果确认内存确实持续增长而业务没有对应增长,可以用性能字典的memory_summary_global_by_event_name多次采样对比,找出哪类 EVENT_NAME 在持续增长,然后按图索骥去查对应的连接和 SQL。
6.4 排查技巧速查表
| 症状 | 首选命令/查询 | 紧急处置 |
|---|---|---|
| 内存占用高但可用内存充足 | free -h看 available | 不需要处置 |
| mysqld RES 持续上涨 | 多次查询 memory_summary_global_by_event_name 对比 | 按 EVENT_NAME 定位组件 |
| 频繁 swap | vmstat 1 10看 si/so | 调小 BP,降 max_connections |
| 出现 OOM | `dmesg | grep oom` |
| 连接数多但并发不高 | SHOW STATUS LIKE 'Threads_connected' | 调低 max_connections,检查连接池配置 |
| 排序后内存不释放 | topRES 高位但 stable | 无需处理,观察 available |
| performance_schema 内存大 | 查询 memory_summary_by_event_name | 裁剪采集项或关闭 |
7. 调优之外的长期策略
参数调优能在短期内缓解内存压力,但真正想要根治,需要从架构和习惯两个层面下手。
第一层,监控体系建设。给 MySQL 配上 mysqld_exporter + Prometheus + Grafana 是基础方案,重点盯住四个指标:process_resident_memory_bytes、process_virtual_memory_bytes、mysql_global_status_threads_connected、mysql_global_status_innodb_buffer_pool_pages_free。有了趋势数据,你能在内存告警之前看到“连接数在爬升”“空闲页在减少”,而不是等问题爆发了才去现场救火。
第二层,SQL 治理。大部分内存问题最终都能追溯到某几条 SQL。可以定期查看慢查询日志,重点关注ORDER BY、GROUP BY、多表JOIN且没有走索引的语句。一条 10 万行数据的全表排序可能就吃掉 100M 内存,如果这类语句一分钟跑一次,内存压力自然大。SQL 优化能从根本上减少临时表和排序缓冲的申请次数。
第三层,容量评估与基线管理。每台服务器上的 MySQL 内存占用并不是固定值,它和业务流量密切相关。建议每周或者每次大版本发布后,记录一组基线数据(连接数、QPS、内存占用率),建立正相关性分析。比如“当 TPS 达到 2000 时,内存占用约 5.2G”,有了这种经验数据,下一次容量规划就有据可依。
第四层,定期清理连接泄漏。查代码里的数据库连接管理,看看连接是否在 finally 块中关闭,连接池的空闲连接超时时间是否合理。曾经遇到过一个应用把连接池最大连接数设到 500 而实际只需要 50,导致 MySQL 端始终保持大量空闲连接,每个连接 3M 左右,白白浪费 1G 多内存。
8. 一些心里话
排查 MySQL 内存问题,最考验人的不是会不会敲命令,而是遇到“看着很吓人的高内存”时还能不能冷静分析。我刚入行的时候,看到free显示 used 90% 就想重启 MySQL,后来被前辈拦下“你看看 buff/cache”。现在回头看,那些年踩过的坑都成了判断经验。
如果你现在正被这个问题困扰,我的建议是:先花十分钟做诊断,再花五分钟想清楚业务是否真的受影响。如果没有性能下降,没有 swap,没有 OOM,大可不必纠结内存数字。如果有,从 Buffer Pool 这个最大头开始调,然后是连接数,再是各类 buffer,最后考虑 SQL 层面。这个顺序记下来,你基本上能解决 70% 的 MySQL 内存焦虑。剩下的 30%,就要靠对业务特征的理解和长期监控数据的沉淀了。