接手一台MySQL服务器的第一件事,永远不是急着调参数。说句实话,大家在群里问“MySQL内存占用过大怎么排查”,十有八九是先被top或者云监控的告警吓到了,看到RES那栏飘到十几个G甚至几十个G,第一反应就是“这玩意儿是不是漏了”。但实际上,MySQL吃内存这件事,跟Java进程“想占就占”的脾气还不太一样——它有章可循,有参数能查,有一整套内存账本可以算得明明白白。这篇文章就拿我最近一次处理线上MySQL内存问题的完整过程当例子,把排查的思路、工具、参数调优和那些容易踩的坑一次说清楚。
先给结论:MySQL内存占用过高,90%以上的情况不是“内存泄漏”,而是“配置没算明白”。真正需要慌的是极少数——比如performance_schema无限膨胀、连接数爆炸、或者某个奇葩SQL把排序缓冲撑爆。大部分时候,你只需要知道内存到底花在哪了、哪些参数能缩、哪些参数打死不能动,就能把内存降到合理水位,而且不影响性能。
把这套东西摸透,适合谁看呢?如果你是运维、DBA、后端开发,或者自己折腾服务器部署过MySQL的小白,这篇文章能帮你少走弯路。下面的内容我会先带你建立MySQL内存架构的整体概念,再给出具体的排查命令和调优参数,最后附上我实际踩过的问题清单。全程不会有“背参数”式的枯燥,我会告诉你这个参数为什么这么调、算出来的依据是什么。
1. 先看现象:一台飙内存的MySQL长什么样
1.1 现场还原:free看到的是“被吃光”
那天下午收到告警,说某台8G内存的云主机可用内存只剩不到1个G。我free -h一看,used差不多6.7G,buff/cache倒是没占多少,就说明这机器内存确实是被实打实“吃”掉了,而不是被文件缓存占了。再用top切到内存排序看一眼,MySQL的RES已经到6.3G了,%MEM直接78%。
这种场景很多朋友都见过。但这里有个容易误判的点:RES本身并不完全等于MySQL真实占用的“逻辑内存”。RES里还包含共享内存段、堆内存的物理驻留部分等等。所以它适合做“是否异常”的初步判断,但不能用来精确算账。真正要算细账,得进MySQL内部看。
我做的第一件事不是连MySQL,而是先在系统层面确认一件事——是不是只有MySQL吃内存?ps aux --sort=-%mem | head扫了一遍,发现其他进程加一起也就占了几百M。那就锁定问题范围了:MySQL自身的内存管理出了问题,或者配置本身就不合理。
1.2 排查前先排除“假性内存占用”
这里的“假性”不是骗人的意思,而是指系统层面的缓存计数误差。Linux的内存管理里,free显示的used是“真正被进程使用的物理内存”,buff/cache则被算作“可用”的一部分。MySQL的InnoDB缓冲池是直接用mmap和read/write来管理内存的,它的内存页既会算在RES里,部分页也会出现在buff/cache里。如果只看top不加区分,很容易觉得“MySQL占了所有内存”。
还有一个常见情况是:刚重启过MySQL,第一次跑大查询时内存会迅速爬升,这是“预加载”机制在起作用,不算泄漏。所以排查的第一步,一定是先记录当前数值作为基线,而不是马上动手改配置。后续所有判断都要跟这个基线做对比,不然就是盲人摸象。
2. 搞清楚内存到底被谁吃了:MySQL内存架构拆解
2.1 两类内存:全局的桶 vs 每连接的水杯
MySQL的内存模型,我用一个生活化的比喻来解释:全局缓冲是仓库,会话缓冲是每个工人手里的水杯。
仓库是所有人共享的,比如innodb_buffer_pool_size就是InnoDB用来缓存数据页和索引页的“主仓库”,数据读写都从这里经过。除了它,还有key_buffer(MyISAM索引缓冲)、query cache(如果还开着的话)、performance_schema内存池、以及各类内部结构(table cache、thread cache等)。这些是“不管多少连接都要占的固定大头”。
会话缓冲则是每个连接各自占一份的临时空间。比如sort_buffer_size(排序缓冲)、join_buffer_size(嵌套循环连接缓冲)、read_buffer_size(顺序读缓冲)、read_rnd_buffer_size(随机读缓冲)、bulk_insert_buffer_size(批量插入缓冲)。问题就出在这:这些参数虽然是按连接来分配的,但很多时候“分配”不等于“立刻使用”,而是“按需增长”。可一旦你真的跑了一个要排序的大SQL,这几个缓冲就会瞬间拉满。
2.2 最容易爆雷的:performance_schema
可能很多朋友都忽略了这块。performance_schema是MySQL 5.7和8.0里默认开启的性能监控模块,它会在内存里维护大量的监控数据表。如果你把performance_schema的采集项开得很全,比如events_waits_history_long、events_statements_history_long,它默认会保留每个线程的若干条历史记录,而且每条记录占几百字节到几KB不等。连接数一多、SQL一频繁,这个模块可以轻松吃掉几百MB甚至上GB的内存。
我自己踩过一个特别典型的坑:某次为了排查慢查询,把performance_schema=ON打开,随手把performance_schema_max_table_instances设成了很大。结果MySQL吃掉的内存比平时多了1.5G,而且都是不可回收的。后来一查SHOW ENGINE PERFORMANCE_SCHEMA STATUS,看到memory那一行的字节数,才发现是它干的。
2.3 线程和连接数:乘法效应的恐怖
MySQL的每一个连接都会对应一个线程,每个线程默认会预分配一份thread_stack(通常256KB),加上net_buffer_length(默认16KB)等,即便这个连接什么都不干,也要占几百KB。如果连接数涨到500,光这层就是几百MB。更关键的是,每个连接都在做sort或join的时候,如果sort_buffer_size设为2M,那你开500个连接又恰好同时触发排序,那就是1G的瞬时内存。这就是为什么我一直强调“小参数 × 大连接数 = 大内存”,这是排查时最容易忽略的乘法效应。
3. 动手调优第一步:三个全局参数先调明白
3.1 innodb_buffer_pool_size:核心中的核心
只要跑的是InnoDB引擎,innodb_buffer_pool_size就是内存占用的绝对大头。它缓存数据页、索引页、插入缓冲、锁信息等,是所有InnoDB读写的必经之路。它设多大合适,业界有一个经验公式:
专机专用(服务器上只有MySQL):设为物理内存的 50%~70%,留出操作系统、文件缓存和临时性内存增长的空间。
我这里说的8G机器,如果跑的是纯MySQL,理论上可以设5G。但实际情况是这台机器还跑着监控agent和一些脚本,我最终设成了4G。怎么判断合不合适?看SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests'(逻辑读请求次数)和Innodb_buffer_pool_reads(从磁盘读的次数),命中率 = 逻辑读请求次数 /(逻辑读请求次数 + 磁盘读次数)。如果命中率长期低于95%,那就说明缓冲池太小了;如果长期在99%以上且还有富余内存,可以考虑加。但注意:在内存本就紧张的场景下,提高缓冲池不是唯一解,也有可能是SQL没走索引导致扫了大量数据页。
这里还有一个坑:innodb_buffer_pool_size不是越小越好,也不是越大越好。把它调到1G这种“安全值”,确实能把内存降下来,但代价是会产生严重的磁盘IO和性能劣化。线上业务如果对延迟敏感,这种“自杀式调优”宁可别做。
3.2 innodb_buffer_pool_instances:大缓冲池的并行优化
MySQL 5.7及以上版本,如果你把innodb_buffer_pool_size设到了1G以上,强烈建议把innodb_buffer_pool_instances也调起来。这个参数把缓冲池切分成多个独立实例,减少并发访问的锁竞争。理论上每个实例建议不低于1G,比如4G缓冲池可以设4个实例,每实例1G。这个参数本身不会直接“减少内存占用”,但它能改善大缓冲池的并发性能,间接减少因锁等待产生的临时内存和线程堆积。
注意了,重点来了:修改innodb_buffer_pool_size和innodb_buffer_pool_instances都需要重启MySQL才能生效。所以在生产环境改这个之前,一定先确认业务低峰期,并做好回滚方案。
3.3 performance_schema:关掉还是调瘦?
前面说过performance_schema是隐藏的内存大户。但这模块本身很有用,我不建议粗暴关闭。正确做法是“调瘦”,只保留你需要的采集项。比如把performance_schema_events_statements_history_long_size从默认的10000调小到1000,把performance_schema_events_waits_history_long_size同理调小,或者直接禁止某些不需要的consumer。改完再配合SHOW ENGINE PERFORMANCE_SCHEMA STATUS来看内存有没有降下来。
如果业务量很小、遇到内存极度紧张,也可以直接在my.cnf里performance_schema=OFF。但如果你要排查慢查询、等待事件、锁问题,这个模块又不可或缺。我个人的习惯是:先用它排查问题,排查完调瘦配置,而不是直接关掉。
4. 动手调优第二步:线程级内存参数,别让小缓冲吃大内存
4.1 sort_buffer_size 和 join_buffer_size:典型的“按需分配”被误解
很多老教程会告诉你“sort_buffer_size建议设2M、4M,甚至8M”,然后你就信了。但实际上,在MySQL 5.7和8.0里,这两个缓冲是“连接创建时不立刻分配,需要排序或join时才按需增长到最大值”的。但架不住并发高啊,你想想,sort_buffer_size=4M、500个并发排序,瞬时内存就是2G。所以这个参数不能拍脑袋,我建议的方法是:先把线上实际的SHOW STATUS LIKE 'Sort_merge_passes'拿出来看,如果这个值很高,说明排序频繁落盘(说明sort_buffer_size太小);如果这个值很低,但内存压力又大,那说明sort_buffer_size设大了,该缩。默认值256KB其实对绝大多数OLTP场景足够了。
join_buffer_size同理,它既不是连接缓冲也不是索引缓冲,而是“当join无法使用索引时,为每次连接操作分配的缓冲块”。你把它设大了,对于用小表驱动大表、且驱动表能被完全放入缓冲的场景,确实能减少扫描次数,但代价同样是乘连接数。我一般建议在2M以内,并且要留意SHOW STATUS LIKE 'Select_full_join'的值,如果频繁出现全表join,调大缓冲远不如优化SQL或加索引来得有效。
4.2 read_buffer_size 和 read_rnd_buffer_size:顺序读和随机读的缓冲区
这两个参数也经常被误调。read_buffer_size是MyISAM和InnoDB做顺序全表扫描时用的缓存,read_rnd_buffer_size则是读取排序后的结果集时用到的随机读缓存。它们都是会话级的、按需分配的。read_buffer_size不能用太大,不然全表扫描一次就吃掉几十MB的例子我也见过。一般保持默认值或小幅调高到1M到2M就够了。
这里有个经验点:排查内存问题时,先别动这两个,因为它们对内存的影响远小于排序和连接相关的参数。只有当确认了SQL层没有全表扫描和排序问题时,才轮到它们。
4.3 tmp_table_size 和 max_heap_table_size:临时表内存的上限
MySQL内部临时表有两种,一种是内存临时表(MEMORY引擎),一种是磁盘临时表。tmp_table_size决定了单个内存临时表的最大大小,超过后就自动转为磁盘临时表。max_heap_table_size则限制MEMORY引擎表的大小。两个参数取较小的那个值作为实际上限。
很多朋友可能不知道,group by和order by一起出现、union、子查询等情况,都可能在内存里建临时表。如果你把这两个值设得很大,内存临时表塞得下还好,塞不下就会触发磁盘临时表(Created_tmp_disk_tables状态值猛涨,性能陡降)。如果设得太小,SQL频繁创建临时表又会撑爆内存。我的经验值是:如果业务里复杂统计查询多,可以设到64M上下,但要配合监控Created_tmp_disk_tables,确保落盘次数不夸张;如果业务是简单CRUD,设16M都嫌多。注意,这两个参数不是全局唯一的,是会话级的,还是有乘法效应,别贪。
4.4 max_connections:内存账本里的乘法基数
max_connections虽然不直接叫“内存参数”,但它本质上是所有会话缓冲的内存乘法基数。刚才说的一堆xxx_buffer_size,全部都要乘以这个数才是不最坏情况下的内存预算。假设你把sort_buffer_size、join_buffer_size、read_buffer_size、read_rnd_buffer_size都调到2M,那每个连接在最坏情况下就会吞掉8M以上,再算上thread_stack等固定开销,假设max_connections是1000,光是理论上限就是8GB以上,物理内存再多也被榨干。
所以我算内存账的时候,永远会先问自己一个问题:这台机器要支撑的最大并发连接数是多少?用SHOW STATUS LIKE 'Threads_connected'看实际峰值,然后跟max_connections对比。如果实际峰值只有50,max_connections却设成1000,那是不合理配置,至少说明风险敞口太大。调低max_connections比调低任何缓冲参数都更有效。但注意,调低之前,一定要确认你的应用连接池(比如HikariCP、Druid)的最大连接数小于这个值,否则连接池建连失败,报Too many connections的错,那就得不偿失了。
5. 容易被忽略的隐藏内存杀手:连接数、碎片和监控系统
5.1 连接数异常:从几十直接冲到几百
线上环境最常见的“突然飙内存”,往往不是参数配错,而是连接数瞬间长起来了。可能是有慢SQL堵住了连接池,应用不断新建连接;也可能是某个后台任务发起了全表扫描,把连接全占住了。此时你进MySQL执行SHOW PROCESSLIST,会看到一大堆Sleep状态的连接,它们占着连接不释放,每个连接又留着各自的会话缓冲分配,内存自然就上去了。
遇到这种情况,第一件事是查SHOW STATUS LIKE 'Threads_connected'和SHOW STATUS LIKE 'Threads_running',前者是当前打开的连接数,后者是当前正在执行的线程数。如果Threads_running很高,说明确实有大量SQL在跑,这时候调内存参数没用,得先杀慢查询或优化SQL。如果Threads_running很低但Threads_connected很高,说明是连接泄漏、没有正常归还,要去查应用层的连接池配置。
这里插一句,MySQL 8.0.16以后有个好用的功能是SHOW STATISTICS?不是,是SHOW ENGINE INNODB STATUS里的连接信息有限,我一般直接用performance_schema里的events_statements_summary_by_digest来查哪类SQL消耗最大。这才是从根上解决问题的思路。
5.2 内存碎片:进程龟速膨胀的原因之一
有一回我遇到的情况很诡异:SHOW GLOBAL STATUS里所有缓冲参数都合理,连接数也不高,但MySQL的RES就是缓慢上涨,重启之后过几天又涨回来。查了一圈才想到是内存碎片。InnoDB缓冲池本身是有LRU淘汰机制的,按理说不会无限膨胀。但如果你用了大量的临时表、频繁的排序、频繁的连接/断开,C++层的malloc/free会产生内存碎片,物理内存的驻留量(RSS)就会慢慢升高。这时候SHOW GLOBAL STATUS LIKE 'Memory_used'可能显示得并不高,但操作系统层面看到的RSS却很高。
这种情况最实用的解法是定期低峰期重启MySQL(比如每两周一次),或者把performance_schema相关的内存池重建一下(ALTER INSTANCE DISABLE INNODB REDO_LOG这类操作不行,但可以用CALL sys.ps_rebuild_table这种整理碎片的方式)。实际上,最干净的办法是调整连接池,减少频繁建连/断连,让MySQL复用线程和内存,碎片问题会显著缓解。
5.3 监控系统吃掉的内存:你自己可能就是问题的一部分
排查的时候别光盯着MySQL。我遇到过一个有趣的问题:某台服务器上装了node_exporter和mysqld_exporter,采集频率设成了1秒,结果Prometheus每抓一次就建连一次,连接数忽高忽低,内存也跟着抖动。后来把采集时间间隔改成15秒,内存立刻平稳很多。所以排查内存问题时,把监控软件也纳入排查范围。MySQL的连接数,对监控系统来说是“看不见的手”。
6. 排查方法论:从现象到根因的一套完整流程
6.1 第一步:用free和top建立基线
不要上来就改配置。先记录:
free -h top -o %MEM | head -30 ps aux --sort=-%mem | head -10这三条命令能在30秒内告诉你:内存总量、MySQL的RES、有没有其他进程抢内存。把这个时间点的数据记下来,作为“优化前”的基线。后续改完参数对比,才知道调优有没有效果。
6.2 第二步:进MySQL看全局状态和关键指标
登录MySQL后,重点查询以下几个:
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'; SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Sort_merge_passes'; SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables'; SHOW VARIABLES LIKE 'innodb_buffer_pool_size%'; SHOW VARIABLES LIKE 'max_connections%'; SHOW VARIABLES LIKE 'performance_schema%';Innodb_buffer_pool_read_requests和Innodb_buffer_pool_reads的比值,能看出缓冲池命中率。Sort_merge_passes高,说明排序频繁落盘,sort_buffer_size可能偏小;Created_tmp_disk_tables高,说明临时表频繁落地,tmp_table_size可能偏小。这两个指标偏高不一定会“占大内存”,但会“占大CPU和磁盘”,间接拖慢业务,所以也要留意。
6.3 第三步:使用状态变量计算“最坏情况内存”
这里给出一个可以直接套用的MySQL内存预估公式:
总内存预算 ≈ innodb_buffer_pool_size + key_buffer_size + max_connections ×(sort_buffer_size + join_buffer_size + read_buffer_size + read_rnd_buffer_size + thread_stack + net_buffer_length) + performance_schema内存池 + 固定开销
我自己算过一次:4G缓冲池,key_buffer=8M,max_connections=200,每个连接的会话缓冲加起来大概6MB(sort=2M、join=2M、read=1M、read_rnd=1M、thread_stack=256K、net_buffer=16K等),这边就是1.2G,performance_schema大约0.5G,固定开销0.5G,总共大约6.2G。这台机器物理内存8G,理论上能扛住。但如果max_connections是500呢?会话缓冲部分直接变成3G,总预算飙到8G以上,系统就随时可能OOM。公式算出来你会发现,真正占大头的往往不是缓冲池,而是会话缓冲乘连接数。
6.4 第四步:针对性调参并验证
根据上面的计算,如果确定是max_connections偏大,就调小它;如果确定是sort_buffer_size偏大,就调小。每次只改一个参数,改完观察一段时间,用第6.2步的指标来验证效果。我反对“一次改十个参数”的搞法,那样出了问题根本不知道是哪一步引入的。
一个小技巧:MySQL 5.7以上支持动态调整部分参数,不用重启。比如SET GLOBAL sort_buffer_size = 1048576;这种,可以临时生效,先观察效果,再决定是否写进my.cnf。但innodb_buffer_pool_size这种就不能动态“缩”到过小,它内部有加载数据页的过程,缩减反而会引发性能抖动,所以这类参数必须在低峰期做。
6.5 常用的问题排查速查表
| 现象 | 首选排查指标 | 常见根因 | 推荐动作 |
|---|---|---|---|
| 内存缓慢上涨,重启后回落 | RSS、Memory_used、碎片状态 | 内存碎片、连接频繁建连/断开 | 调连接池、定期低峰期重启 |
| 内存突增,伴随CPU飙高 | Threads_running、慢查询日志 | 大量排序、全表扫描、笛卡尔积join | 抓慢SQL、优化索引、限制sort/join缓冲 |
| 内存高,但Threads_running很低 | Threads_connected、应用连接池配置 | 连接泄漏、连接池maxSize过大 | 调连接池、排查应用代码归还连接 |
| 刚上线新功能后内存翻倍 | performance_schema内存状态 | 采集项开太多、监控导致连接数暴涨 | 调小采集历史、延长监控采集间隔 |
| 配置看起来合理但内存仍高 | 全局状态、表数量、缓存表数量 | table_open_cache过大、表数量太多 | 调小table_open_cache、优化表数量 |
7. 实操现场:一次8G内存机器的完整调优记录
7.1 现场数据采集
接着文章开头那个案例继续说。我那台8G内存的机器,free -h显示used 6.7G,MySQL的RES是6.3G。进MySQL一看,innodb_buffer_pool_size是5G,max_connections是300,sort_buffer_size是2M,join_buffer_size是2M,read_buffer_size是1M,read_rnd_buffer_size是1M,performance_schema=ON且没做过任何瘦身。
7.2 算账和定位
按公式估算:5G(Buffer Pool) + 300×(2M + 2M + 1M + 1M + 256K + 16K) ≈ 5G + 300×6.3M ≈ 5G + 1.85G ≈ 6.85G。再加上操作系统和其他进程,8G的机器确实被榨干了。
但再仔细看一眼Threads_connected,实际只有80。也就是说,这台机器日常根本用不到300个连接。问题不在Buffer Pool,而在于max_connections的配置和会话缓冲乘出来的“最坏情况内存”。另外,performance_schema没做瘦身,估算也吃了0.6G左右。
7.3 调整动作和效果
我的调整方案是:
- 把
innodb_buffer_pool_size从5G降到4G(留出操作系统和突发内存的余量) - 把
max_connections从300降到150(跟应用侧确认过连接池上限是100) sort_buffer_size和join_buffer_size先动态降到1M,观察一星期看Sort_merge_passes和Select_full_join是否明显增长performance_schema把events_statements_history_long_size从10000调到2000
降完以后,最坏情况内存预算 ≈ 4G + 150×(1M + 1M + 1M + 1M + 256K + 16K) ≈ 4G + 0.65G ≈ 4.65G。实际运行一周,MySQL的RES从6.3G降到了4.2G,业务查询延迟没有明显变化,Sort_merge_passes没有增加,连接数峰值稳定在90左右。
7.4 临时性内存暴涨的应急处理
如果遇到的是“立刻要降内存”的突发状况,来不及仔细调参,可以这样应急:
-- 杀空闲连接 SHOW PROCESSLIST; -- 找到大量Sleep的连接之后,可以使用如下语句批量kill(这只是示例,执行前一定确认连接ID) KILL <connection_id>;但注意,KILL不能乱用,必须先确认那些连接确实是可以断开的。更稳妥的办法是直接在应用连接池层面做流控,或者把max_connections动态改小,让新连接进不来:
SET GLOBAL max_connections = 100;这种操作能瞬间卡住新增连接,给排查争取时间。但要记住这只是一个临时止血动作,根本解法还是要把慢SQL和连接池问题处理掉。
8. 避坑清单:那些年我们一起踩过的内存坑
8.1 别被“默认配置就是合理的”骗了
my.cnf如果是从网上某个教程直接复制来的,很可能别人那台机器是128G内存,而你的只有4G。安装完MySQL的第一件事,永远是手动检查一遍内存相关参数,而不是直接用默认的。尤其是innodb_buffer_pool_size在8.0里默认值变成了128M,对于小内存机器但大数据的场景,这配置反而会逼着MySQL频繁刷盘,性能惨不忍睹。默认值只保证“能跑”,不保证“跑得好”。
8.2 调小InnoDB Buffer Pool之前,先想清楚代价
Buffer Pool是InnoDB的性能根基,它不是“省内存万能药”。如果遇到内存不足,优先调会话缓冲、连接数、performance_schema这些“水分”比较大的部分,最后才考虑动Buffer Pool。我曾经见过一哥们儿为了把内存从2G降到512M,把Buffer Pool调成128M,结果业务QPS直接掉了一半。内存是降下来了,性能也废了,这属于本末倒置。
8.3 不要盲信“RES高 = 内存泄漏”
MySQL的RSS高不一定就是泄漏。它可能只是把Buffer Pool占满后,又因为某些原因没有及时把冷数据页刷出。MySQL的LRU机制会根据innodb_old_blocks_time等参数延迟淘汰,所以刚跑完大查询后,内存短暂居高不下是正常现象。要判断是否泄漏,应该持续观察好几个小时或几天,看RSS是否在业务平稳的前提下还持续上涨,而不是看完一次top就下结论。
8.4 修改参数后,别忘了重启和验证
有些参数支持动态修改,但写进配置文件后需要重启才生效。我有一次在my.cnf里改了max_connections,SET GLOBAL也执行了,但忘了改配置文件,下次MySQL重启后配置又回去了,等于白忙活。另外一个细节是:SET GLOBAL对已存在的连接无效,新连接才用新值。所以如果是想立刻限制现有连接数,得手动Kill掉一部分多余的连接。
8.5 不同的存储引擎,内存参数完全不同
这篇文章主要讨论InnoDB。但如果你还在用MyISAM表,那key_buffer_size就要单独算。MyISAM的索引缓存全靠它,默认8M很容易成为瓶颈,但设大了也是内存开销。用SHOW GLOBAL STATUS LIKE 'Key_blocks_unused'来看利用率,一般情况下key_buffer_size设到256M以内足够大多数场景用了。如果你的库全是InnoDB,那key_buffer_size保持默认就行,别浪费内存。
9. 一点额外的复盘:从内存问题看到更本质的东西
排查MySQL内存问题,看起来是一个“调参”的活儿,但实际上是对整个数据库运行状态做一次综合体检。内存只是表象,背后往往是SQL质量、索引设计、连接池管理、监控体系这些更深层的东西。比如我们线上那次内存告警,真正的根因其实是一个业务方跑了半小时的报表查询,触发了大排序和临时表,把会话缓冲全部拉满。如果只调整内存参数而不管SQL,下次还会以另一种形式爆发。
所以我最后想分享的一个实际心得是:每次处理完内存问题,不要急着关工单,花一点时间把所有慢查询日志拉出来看看,把events_statements_summary_by_digest按总延迟排序,揪出那几个消耗最大的SQL模板。你会发现,优化其中一条SQL的索引,省下的内存可能比你调十个参数还多。我这次排查的最后,就是帮业务方加了一个联合索引,那条报表查询的排序临时表直接消失了,MySQL内存又降了500M。这才是治本。
另外还有一个小技巧,可能很多人不知道:mysql客户端连接时,可以执行RESET CONNECTION或者用连接池的心跳检测定期清理会话状态,把每个连接上临时表、排序缓冲、用户变量都释放掉。如果你用的连接池是HikariCP,可以打开connectionTestQuery,确保拿到的连接是健康的。这些小细节平时没人提,但真到内存紧张的时候,它们是压死骆驼的最后一根稻草和救命稻草的区别。
本文的调参经验和数值,都是基于我实际维护过的环境总结出来的。你的服务器配置和业务场景跟我不同,数值不能盲目照搬,但排查思路和计算公式是完全通用的。建议你收藏这份方法论,下次再看到MySQL内存告警,至少能从容地先算一笔账,而不是重启了事。