MySQL 优化这个话题,聊的人多,懂的人少。每次一说到性能问题,总有同事甩给我一份网上抄来的“万能 my.cnf”,几百行带注释的配置,恨不得把每个参数都改一遍。结果呢,数据库没快多少,反而把本该自己吃饭的内存让给了各种无意义的缓存,重启之后一脸懵。做了这么多年数据库相关的工作,我越来越确信一件事:MySQL 真正重要的变量,一只手数得过来,剩下的绝大多数,保持默认值就是最好的选择。这篇不聊那些花哨的“优化一百条”,就聚焦在真正影响性能、真正值得你花时间去理解和调整的几个核心变量上,掰开揉碎讲清楚它们为什么重要、怎么判断该调多大、以及我实践中踩过哪些坑。
如果你是刚接手 MySQL 运维的开发者,或者正被线上数据库性能问题折磨,这篇文章应该能帮你理清头绪。我不会给你一份抄作业的配置清单,而是告诉你如何根据自己机器的实际情况,把资源用在刀刃上。
1. 先想明白:为什么多数参数不用调
接触过太多项目,一上来就甩配置文档,这其实搞错了顺序。在动任何参数之前,要先搞清楚 MySQL 慢在哪里。
1.1 调参是最后一步,不是第一步
理解性能问题,本质上是理解一条查询从客户端发出到结果返回,中间经历了什么。数据在磁盘上,内存里有 Buffer Pool,数据变更要写 Redo Log,查询要解析、优化、执行,引擎层和 Server 层各有各的资源消耗。调参的目的,就是减少不必要的重复劳动,让资源花在最值得的地方。
但很多人的做法是反向的。业务慢了,第一反应就是“改参数”。实际上,慢查询最常见的原因往往不在参数,而是 SQL 本身写得太烂、索引没建对、数据量涨了没做归档。参数只是压死骆驼的最后一根稻草,前面几根全都是查询设计问题。所以我的经验是:先开慢查询日志,抓到慢 SQL,用 EXPLAIN 分析执行计划,确认是内存、IO、锁还是 SQL 层面的问题,再决定要不要动参数。
1.2 很多“经典优化项”已经名存实亡
MySQL 发展到现在,很多早期的参数早已内化或者被更好的机制取代。比如 Query Cache,在 8.0 里已经彻底移除,之前版本开启它还会因为全局锁争用导致性能雪崩。再比如各种 buffer 的默认值,在 MySQL 5.7 之后已经对普通场景做了充分适配,盲目调大反而可能弄巧成拙。
所以真正值得去动手的,一定是那些对性能影响倍数级放大、并且能通过现有监控指标反推合理值的参数。它们有这么几个特征:数量少、影响大、且“不够”的症状非常明显。我调了这么多年,值得长期跟踪的变量,其实就是内存缓冲相关、日志刷盘相关、连接管理相关这三类。
2. 内存与缓冲:性能的基石
数据库性能的第一大支柱就是内存。磁盘 IO 和内存延迟差着好几个数量级,如何让热点数据尽可能留在内存里,是所有优化的核心。
2.1 innodb_buffer_pool_size:第一优先级,没有之一
这个参数几乎决定了 InnoDB 的读性能上限。Buffer Pool 不仅缓存数据页,还缓存索引页、插入缓冲、自适应哈希索引等。它的大小直接决定了查询命中的概率,也直接决定了磁盘 IO 的压力。
我见过不少机器,内存有 128G,Buffer Pool 只配了 1G,数据库天天慢如蜗牛。也见过机器内存 16G,Buffer Pool 配到 12G,直接把操作系统和 MySQL 的内部管理结构挤爆,触发了内存交换,反而更慢。Buffer Pool 该多大,有一个比较公认的经验:给操作系统和 MySQL 其他结构留够余量后,尽量大,但不建议超过机器物理内存的 70% 左右。剩下的内存要给操作系统做文件缓存、给 MySQL 的各个会话 buffer、给排序和临时表留空间。
怎么确认自己该调多大了?不要猜,看指标。MySQL 提供了命中率的数据来源。在 5.7+ 上可以:
- 用
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'看累计的物理读次数和逻辑读次数。 - 命中率大致可以算成
1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)。
如果命中率低于 95%,同时磁盘读的 IOPS 已经接近饱和,那就是 Buffer Pool 不够的信号。此时再看看机器的空闲内存,确认充足,就可以加大。注意还有一点:如果 Buffer Pool 很大(比如超过几十 G),建议开启innodb_buffer_pool_instances,拆成多个实例,减少高并发下的缓冲池锁竞争。这个参数我个人倾向于在调整 Buffer Pool 时一起配上,一般 8 个实例是比较稳妥的起点。
提示:调整 Buffer Pool 前观察当前
Innodb_buffer_pool_wait_free状态值。如果持续非零,说明有线程在等待刷脏页,这往往是 Buffer Pool 过小或刷盘能力不足引起的,光加大缓冲池还不够,还得看刷盘相关配置。
另外,Buffer Pool 不是永久保存的内容。数据库重启后需要重新预热,这在冷启动阶段会有一段时间的“性能陡降”。5.7 之后有个innodb_buffer_pool_dump_at_shutdown和innodb_buffer_pool_load_at_startup,开启后可以把 Buffer Pool 中的页信息(注意是页的引用,不是数据本身)存下来,重启后按图加载,能明显缩短预热期。这个参数反正我是一定会开的,代价很小,收益却很明显。
2.2 innodb_log_file_size:写多读少场景的隐藏瓶颈
Buffer Pool 解决的是读的问题,而写入场景的关键在 Redo Log。InnoDB 为了保证持久性,在真正把脏页刷到磁盘之前,会先把修改记录写入 Redo Log,这个日志是以追加方式写的,顺序写性能远高于随机写。Redo Log 的大小决定了你在崩溃恢复时需要回放多少日志,更重要的是,它决定了“写满之前,脏页能积攒多少”,这直接影响刷盘行为。
如果innodb_log_file_size设置得太小,日志很快写满,后台线程就要频繁地将脏页强制刷盘来腾出日志空间。这种“日志切换导致的强迫刷盘”会让 IO 出现毛刺,写性能像心电图一样忽高忽低。我遇到过一个案例,某业务写请求量很大,磁盘是 SSD,IO 延迟却隔几十秒就飙升一下。排查发现日志文件只有 256M,每次切换都引发一次刷盘风暴。
在 8.0 里,Redo Log 的配置方式变了,直接指定日志文件大小和数量。根据官方文档,默认总共约 32M,这对生产环境偏小。我个人的经验,写密集型业务可以先把 Redo Log 总大小配到 1G 到 4G 之间。为什么是这个范围?太小了频繁切换,太大了崩溃恢复时间变长,要平衡。InnoDB 崩溃恢复时需要回放日志里尚未刷盘的操作,日志文件越大,恢复时间越久,但对绝大多数业务来说,总大小 4G 以内恢复时间都可接受。实在不放心可以逐步加大,观察SHOW ENGINE INNODB STATUS里Log sequence number和Log flushed up to的差距,如果差距经常很大,说明日志写入压力高,可以适度加大。注意调整日志大小需要干净地关闭实例,最好在低峰期操作,提前确认没有长事务。
2.3 定位性能视图:用数据说话
动手改之前,花几分钟看一下以下关键状态值,可以让调整目标明确很多:
Innodb_buffer_pool_read_requests和Innodb_buffer_pool_reads:判断逻辑读和物理读的比值。Innodb_buffer_pool_wait_free:如果持续增长,说明 Buffer Pool 刷脏跟不上。Innodb_os_log_written和Innodb_log_write_requests:判断日志写入频率。Innodb_data_reads/Innodb_data_writes:总体 IO 情况。
这些指标可以通过SHOW GLOBAL STATUS直接查看,也可以用系统表把数据拉出来画成趋势图,看它增长是稳定的还是脉冲式的。没有监控图就不好判断,所以做 MySQL 运维的人,把这几个状态值存进时序库是基本功。
3. 连接与会话级:别把资源耗在不该耗的地方
很多人只盯着 Buffer Pool 和日志,结果把连接数、临时表、排序缓冲全忽略了。这些参数单个看起来不大,但在并发场景下,累积效应非常惊人。
3.1 max_connections 与 thread_cache_size:连接风暴的第一道防线
max_connections这个参数几乎是新手最爱调的,一发生Too many connections就立刻调大。这是典型治标不治本的思路。连接本身不重,重的是每个连接背后分配的线程栈、排序缓冲、临时表空间等资源。调大连接数,实际是给系统增加了大量内存占用和上下文切换开销。
一个更稳妥的思路是:先查当前峰值连接数,然后对比max_connections是否真的紧张。如果连接常年不满,说明问题不在连接数,而在连接被长时间占用。比如连接池配置过小、慢查询占用连接过久,或者有没关闭的事务。盲目调大只会让系统支撑更多“病恹恹”的连接,整体吞吐反而下降。
thread_cache_size也是一个值得关注的连接相关变量。它控制 MySQL 缓存多少空闲线程,供新连接复用。这个值如果太小,短连接频繁创建销毁线程的成本就会显现;但设太大也意义不大,因为缓存的是线程,不是连接本身。通常设为 16 到 64 之间足够。对这个参数我的态度是:别过度关注,但不意味着可以完全忽视,尤其是业务大量使用短连接(比如 PHP 进程每请求新建连接)的场景,线程缓存能带来肉眼可见的改善。
3.2 会话级 buffer 的误区:sort_buffer 和 join_buffer 并不是越大越好
这两个是典型的“每个连接单独分配”的缓冲区。sort_buffer 用于排序,join_buffer 用于无法走索引的 join 操作。问题在于:它们是按会话分配的,不是全局共享的。你把 sort_buffer 从 2M 调到 64M,但如果有 200 个并发连接同时排序,那最多可能占用 200 × 64M = 12.8G 内存,瞬间就能挤爆机器。
我见过有人犯这个错,机器总共 32G 内存,把几个会话级 buffer 都调大,结果一遇到高峰查询,内存直接被吃光,OOM 进程被杀。这个教训非常深刻。对这类参数,正确的做法是:尽量保持小,让它们成为“溢出缓冲区”。更进一步,应该去优化 SQL,避免真正触发大排序和大 join,而不是把它们调大。对于确需保留的排序操作,可以单独观察Sort_merge_passes状态值。这个值如果很高,说明排序溢出到磁盘了,才需要考虑适度增大 sort_buffer。切记是“适度”,比如从 2M 调到 8M,而不是一下子翻几十倍。
3.3 临时表的隐形开销:tmp_table_size 和 max_heap_table_size
一个容易忽略的点是临时表。排序、去重、GROUP BY、子查询都可能在内存中创建临时表。如果临时表太大,MySQL 会把内存临时表转成磁盘临时表,这个过程相比内存操作慢得多。控制临时表大小的两个参数是tmp_table_size和max_heap_table_size,实际生效值取两者中的较小者。
怎么判断这个值够不够?看到状态变量Created_tmp_disk_tables和Created_tmp_tables的比值。如果磁盘临时表占比超过 25%,说明临时表的配置偏小,或者 SQL 写得有问题导致产生了超大中间结果集。调整之前先想想:这个临时表真的需要这么大吗?对某些业务,一个简单的 GROUP BY 在数据量上来后可能产生几 G 的中间表,光把 tmp_table_size 从 16M 调到 512M 并不能真正解决问题,反而是 SQL 需要拆解。但如果确实有大量小临时表频繁落盘,适度提高这个值是有帮助的。
注意:
tmp_table_size是每个会话的限制,不是全局的上限。假设 100 个并发,每个临时表 64M,那最多也可能用到 6.4G 内存,所以要结合并发来看,别以为调大就万事大吉。
4. 刷盘与一致性:你看不到的延迟开关
还有一个经常被忽视的维度,就是数据到底什么时候落盘。MySQL 在保证数据安全和追求高性能之间,给了你几个“开关”,这几个开关的取舍,直接决定了你在极端情况下的数据丢失风险和写入延迟。
4.1 innodb_flush_log_at_trx_commit:写性能与数据安全的天平
这个参数有三个值:0、1、2。默认是 1,表示每次事务提交,Redo Log 都要刷到磁盘上。这是最安全的选择,但代价也是最大的,每次提交都等待磁盘落盘,在高并发写入场景下,SSD 还能扛,机械硬盘会非常吃力。
如果你把值设为 2,事务提交时只要日志写入操作系统的缓存即可,由操作系统决定什么时候真正刷到磁盘。这种模式下,如果只是 MySQL 崩溃,操作系统没断电,数据不会丢太多;但如果整个机器断电,那么最近一秒左右的未刷盘日志可能会丢失。设为 0 则是完全交给 MySQL 后台处理,最快但危险程度也更高,任何 MySQL 进程异常退出都可能导致最近一批数据丢失。
我对这个参数的实践建议是:默认的 1 在绝大多数情况下都不要动,除非你的业务能明确接受“最多丢失最近 1 秒的已提交数据”。我调过一些低频写入的报表库,用值 2 明显提升了批量导入速度,但那种场景业务对丢失 1 秒数据的容忍度很高。如果你想保证安全又想提速,首先想到的不该是改这个参数,而是把磁盘从机械盘换成 SSD,或者检查是不是有别的 IO 瓶颈。
4.2 双写与同步的联动:sync_binlog
除了 InnoDB 自己的日志,二进制日志(binlog)的刷盘策略也影响性能。sync_binlog参数控制 binlog 何时同步到磁盘。默认配置下,binlog 的写入可能是由操作系统缓存控制的,如果设为 1,则每次事务提交都要同步刷盘。当innodb_flush_log_at_trx_commit=1且sync_binlog=1时,数据安全级别最高,但写入性能会受两重刷盘拖累。
我个人的倾向是:如果数据库不是主从架构,binlog 的同步策略可以不必那么激进;但在主从复制场景,为了保证 binlog 不丢,sync_binlog=1是更稳妥的选择。若同时想兼顾写入性能,可以考虑把 binlog 放到独立的磁盘或目录,让日志写入和数据文件写入不互相争用同一块磁盘的 IO。这个物理层面的拆分,比纠结参数值往往更有效。
5. 实操:一次标准调优的执行记录
参数了解得再多,不如实际动手操作一遍。这里分享一次我在模拟项目 X 中的标准调优流程,供参考。这套流程适合大多数中低复杂度的业务系统,核心原则是“一次只动一个变量,并且每次改动都有监控数据支撑”。
5.1 先收集基线数据
调优之前,先花 15 分钟收集系统的基线数据:
- 机器层面:CPU 使用率、内存占用、磁盘 IOPS、磁盘读写延迟。
- MySQL 层面:
SHOW GLOBAL STATUS中的关键状态值,重点看 Buffer Pool 命中率、临时表落盘情况、排序合并次数、慢查询数量。 - 业务层面:当前 QPS/TPS、平均查询耗时、长事务数量。
记录这些数据后,再开始调整。没有基线数据,你根本不知道改动是变好还是变坏。
5.2 核心参数的调整对照表
下表是我在一个 32G 内存、8 核 CPU、SSD 磁盘的 MySQL 实例上,针对一个读多写少、单表数据量在 1 亿左右的业务场景所做的调整,以及调整的依据:
| 参数 | 默认值(示例) | 调整后值 | 调整依据 |
|---|---|---|---|
| innodb_buffer_pool_size | 128M | 20G | 内存 32G,留出系统与线程空间;命中率测试低于 80% |
| innodb_buffer_pool_instances | 1 | 8 | 大缓冲池并发竞争拆分解锁 |
| innodb_log_file_size | 48M(累计) | 1G | 写日志频繁切换,IO 毛刺 |
| tmp_table_size | 16M | 64M | Created_tmp_disk_tables 占比高 |
| max_heap_table_size | 16M | 64M | 与 tmp_table_size 联动生效 |
| thread_cache_size | 自动 | 32 | 短连接场景,线程重复创建成本高 |
| sync_binlog | 0(或 1) | 1 | 主从场景,保证 binlog 不丢 |
注意:这张表只是示例。不同版本的 MySQL,参数默认值可能不同。8.0 和 5.7 在 Redo Log 的处理方式上就有很大差异。引用第三方参数时一定要结合自己实例的SHOW VARIABLES查看现状。
5.3 修改配置后的验证方法
改完配置,重启 MySQL 或者用SET GLOBAL动态设置后,不要立刻下结论。给系统至少二十分钟到半个小时的运行时间,让缓存预热、统计信息稳定,再对比之前的基线数据。此时重点看:
- 慢查询数量有没有下降。
- Buffer Pool 命中率有没有提升。
Innodb_log_file_size调整后,Log sequence number和Log flushed up to的差距是否缩小。- 磁盘 IOPS 有没有从频繁尖峰变成相对平缓。
有条件的,最好把监控图拉出来做前后对比。如果改动后核心指标没有明显变化,那大概率说明你改的方向有问题,继续调下去也意义不大,应该退回原值,重新分析定位瓶颈。
6. 常见问题与排查技巧实录
平时群里被问得最多的,就是各种“调完之后怎么更慢了”的问题。这里把典型的几个坑集中整理一下。
6.1 改完参数没生效,问题出在哪
很多人改的是my.cnf,但不同配置项所属的段落不一样,比如innodb_buffer_pool_size必须在[mysqld]段下。如果你把它写在[client]或[mysql]段,MySQL 启动时根本不读。另外,部分参数虽然动态可调,但重启后如果配置文件里没写对,也会恢复原值。所以改完配置后,建议用SHOW VARIABLES LIKE 'xxx'确认一下当前实际生效的值,别想当然。
还有一个经典问题:SET GLOBAL并不影响已经存在的连接,只影响之后新建的连接。如果你用SET GLOBAL调了某个参数,再用同一个已存在的会话去查看,看到的值可能没变,甚至部分会话级参数只对当前会话生效。这种时候要区分“全局值”和“会话值”。
6.2 缓存命中率 100% 但还是很慢
Buffer Pool 命中率高不等于整体性能好。有一次排查,命中率 99% 以上,查询还是慢。后来发现是慢 SQL 在索引选择上出了问题,大量回表导致 CPU 居高不下。Buffer Pool 只解决“从哪读”的问题,不解决“读什么查得慢”的问题。遇到这种情况,该看的是执行计划,是不是有一个隐式类型转换导致索引失效,是不是有一个SELECT *导致大量无用数据传输。参数优化在 SQL 优化面前,永远是退而求其次的选择。
6.3 参数变大,性能反而变差
这个我在前面也提过,最典型的就是会话级 buffer 调大后内存吃紧。另外一个不容易察觉的坑是innodb_buffer_pool_instances配得不合理。实例数超过 CPU 核心数时,锁竞争没有继续降低,反而因为每个实例需要额外的内存管理结构,造成轻微损耗。所以这个值不是越大越好,通常不超过 8 或者不超过 CPU 核心数是比较稳妥的。
这里整理一个速查表,方便排查时对照:
| 现象 | 优先排查方向 | 相关参数或工具 |
|---|---|---|
| 大量磁盘读,命中率低 | Buffer Pool 偏小 | innodb_buffer_pool_size |
| 写入毛刺,日志频繁切换 | Redo Log 太小 | innodb_log_file_size |
| Too many connections | 连接数或慢查询堆积 | max_connections、slow log |
| 临时表落盘频繁 | 中间结果集过大或 tmp 太小 | tmp_table_size、SQL 改写 |
| 排序性能差 | Sort_merge_passes 高 | sort_buffer_size、索引优化 |
| 数据安全顾虑高 | 刷盘策略过弱 | innodb_flush_log_at_trx_commit、sync_binlog |
| 重启后性能骤降 | Buffer Pool 未预热 | innodb_buffer_pool_dump_at_shutdown |
6.4 一次调优只能解决一个主要矛盾
最后说一个经验之谈:你永远不要在同一个时段同时调五六个参数。每个参数的调整都会带来连锁反应,同时改多个变量,出了问题你根本分不清是哪个引起的。我更习惯的做法是:一次只改一个最可疑的变量,观察一段时间,记录效果,再决定下一步。如果连续改了两三个变量,核心指标都没有任何改善,那大概率是你对瓶颈的判断从一开始就错了,这时候最有效的动作其实是回滚所有改动,重新回到慢日志和监控面板上。
在实际工作中我是吃过几次亏才慢慢建立起这套流程的。每次项目一紧张,总想着调大这个调大那个来兜底,结果往往是把问题从查询层引到了内存层。MySQL 的配置不是免死金牌,它更像是一套精密的资源分配系统,你只有理解了每个变量背后的资源申请逻辑,才知道哪些地方该抠、哪些地方该松。如果你打算在自己的机器上做实验,我建议从innodb_buffer_pool_size和innodb_log_file_size这两个入手,先把内存和日志这两个地基打牢,再往上层看连接和临时表。对照着慢日志和状态值,先观察再动手,你的数据库离稳定运行就不远了。