ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL配置文件从入门到排错:my.cnf加载顺序、核心参数与性能调优实战

MySQL配置文件从入门到排错:my.cnf加载顺序、核心参数与性能调优实战

调MySQL,我给我手下人定的规矩是:接手一套生产环境,不急着跑SQL、不急着看表结构,先把配置文件从头到尾读一遍。MySQL的脾气有一大半藏在配置文件里,很多看似无缘无故的慢查询、偶发断连、内存异常,最后都能在my.cnf(Windows下叫my.ini)里找到答案。

这篇文章我想认真聊聊MySQL配置文件这个东西。它本身不复杂,说白了就是一坨键值对,但真正动手时你会发现:多份配置文件谁先谁后、改完为什么没生效、参数之间有因果关系、不同版本默认值又不一样。这些问题如果你没踩过坑,很难一次搞对。内容适合刚入门的DBA、需要自己维护数据库的后端开发,也适合被线上故障折磨过、但还没系统整理过配置经验的人。

1. 配置文件藏在哪里:加载顺序与优先级

1.1 一个系统里其实有好几份配置

很多新手有个误区,以为MySQL配置就一个文件。实际上Linux系统里常常存在多份候选配置,MySQL启动时会按固定顺序依次查找,找到哪份就用哪份,所有找到的文件还会按顺序合并,后出现的参数覆盖先出现的同名参数。常见的位置有这些:

  • /etc/my.cnf
  • /etc/mysql/my.cnf
  • SYSCONFDIR/my.cnf(通常也是/etc/my.cnf)
  • $MYSQL_HOME/my.cnf
  • 用户家目录下的~/.my.cnf

Windows上则是my.ini,通常放在安装目录和C:/ProgramData/MySQL/MySQL Server xxx/下。不同发行版、不同安装方式,默认配置路径也会有差异。用mysql --help | grep "Default options"能直接看到当前实例到底按什么顺序找文件。

我接手过一套系统,之前的人习惯把所有自定义参数写在/etc/my.cnf里,后来又装了新版实例,自带了一份/etc/mysql/my.cnf,他以为没冲突就没管。结果新实例总是不加载自定义日志参数,用SHOW VARIABLES一看,真实生效的其实是另一份文件里的值。这种“你以为改了,其实没改”的问题,是所有配置排错里面最隐蔽、也最气人的一种。

1.2 多个文件同时存在时谁说了算

必须理解的一点是:MySQL不是“找到第一个文件就用它”,而是会把所有候选文件里的配置合并起来,段落和参数后者覆盖前者。启动顺序基本是:/etc/my.cnf→/etc/mysql/my.cnf→~/.my.cnf。所以如果你在/etc/my.cnf里写max_connections=200,又在~/.my.cnf里写了max_connections=300,最终生效的很可能是300。

另外,启动命令行中的--参数优先级永远高于配置文件。这一点既是排查突破口,也是隐患来源。有些自动化运维平台会用启动脚本追加参数,看起来配置没问题,实际上命令行已经把配置文件里的值覆盖了。遇到配置不生效,第一件事不是怀疑文件权限,而是去看启动命令长什么样。

1.3 语法规则和防错技巧

配置文件的语法很简单:

  • 用[mysqld]、[client]、[mysql]区分段落,段落名决定参数生效范围;
  • 参数格式是参数名 = 值,等号两侧可以有空格;
  • #或;开头是注释;
  • 值可以带单位,比如2G、512M。

但有个容易犯的错:大小写。参数名和值在Linux上基本都区分大小写,max_connections写成Max_connections,MySQL可能不认或者当成别的参数,启动时只会报一个含混的warning。还有布尔值写法,1、ON、TRUE是开启,0、OFF、FALSE是关闭。虽然旧版本很多也能识别YES,但规范起见建议只用ON和OFF。

另一个防错技巧是改完配置文件先执行mysqld --validate-config(部分版本是mysqld --print-defaults)做语法和参数校验,再启动。别急着重启,把错误在重启前暴露出来,能省很多应急处理时间。

一个关键习惯:修改生产配置前先备份原文件,备份带上日期:cp /etc/my.cnf /etc/my.cnf.bak.20250101。我见过太多人改完忘了原来改成啥,最后只能靠脑补。

2. 基础但容易翻车的全局配置

2.1 max_connections:连接数不是越大越好

max_connections控制MySQL允许的最大客户端连接数,5.7和8.0的默认值都是151。看起来不多,但一接上监控系统、多个应用服务和一堆后台任务,瞬间就能打满。把连接数调高是对的,但很多人直接改成2000甚至5000,这就埋了雷。

每个连接都会占用线程栈和内存。如果平均每个连接消耗几十KB到几MB(取决于排序缓冲等会话参数),5000个并发连接可能直接吃满内存,然后触发OOM,数据库被操作系统杀掉,这比“Too many connections”更可怕。比较合理的思路是:先用SHOW PROCESSLIST观察高峰期到底有多少连接,预留1.5到2倍余量;连接数调高的同时,还需要检查thread_cache_size,否则频繁创建销毁线程也是损耗。

实际中常见的一套组合是这样的:

  • wait_timeout、interactive_timeout不能太长,否则死连接占着会话不放;
  • max_connections和max_user_connections要配套控制,防止单个应用把连接池冲垮;
  • 应用层的数据库连接池大小也得收敛,不能拿MySQL当无状态服务使劲开连接。

我调过一套线上系统,max_connections从默认151改成300,同时把wait_timeout从28800调成60,配合应用层连接池空闲回收,连接数高峰从此稳定在80左右,再也没有出现过Too many connections。这是最典型的配置联动:不是只调一个数字,而是把连接的生命周期整体管起来。

2.2 字符集与排序规则的统一

字符集是配置文件里不起眼但后果最严重的参数。如果数据库、表、连接三级字符集不统一,轻则中文乱码,重则索引失效、排序结果诡异。8.0里默认utf8mb4已经好了很多,但老项目从5.6、5.7迁移上来时,经常带着latin1或utf8。

配置文件里通常会看到这几个:

  • character_set_server:服务器默认字符集;
  • collation_server:默认排序规则,比如utf8mb4_0900_ai_ci;
  • character_set_client:客户端连接字符集,也可以动态设置。

建议统一写死为utf8mb4和utf8mb4_general_ci。如果你用的8.0,也可以跟官方默认的utf8mb4_0900_ai_ci。我的想法是:既然用了8.0,就跟着官方默认走,再用ALTER DATABASE、ALTER TABLE把存量库表慢慢迁移,最后写个巡检脚本检查有没有“漏网”的latin1表。

另外要记住,连接字符集受character_set_client影响最大。就算服务器和表都是utf8mb4,客户端指定了latin1,写入时照样可能乱码。程序连接串里的useUnicode=true&characterEncoding=utf8不是可有可无的,这条经常被后台开发忽略,然后甩锅给DBA。

2.3 默认存储引擎与表结构习惯

默认存储引擎在多版本里演进了好几次,MyISAM是古董,InnoDB是主流,8.0里InnoDB就是官方默认。配置文件里把default_storage_engine=InnoDB写死很重要,因为存量MyISAM表在高并发写场景下会全表锁,读多写少看着没事,一旦高峰写入进来就锁等待堆积。

我碰到过一个案例:某系统的日志表是MyISAM,业务侧某天写量大增,SHOW PROCESSLIST里全是Waiting for table level lock。排查后发现建表语句没写ENGINE=InnoDB,沿用了历史默认引擎。处理办法是把所有非InnoDB表改成InnoDB,同时在配置文件里把默认引擎固定成InnoDB,防止后面新表再入坑。

如果你负责的库里还保留着MyISAM或MEMORY表,不要急着全量改,得先评估查询有没有依赖MyISAM的特性,比如FULLTEXT全文索引、压缩表、某些空间函数。多数情况下改成InnoDB都没问题,但大表转换时会重建表,耗时较长,必须避开业务高峰在低峰期操作,同时提前评估磁盘空间的双倍占用。

3. InnoDB参数:性能瓶颈的核心地带

3.1 innodb_buffer_pool_size 的估算方法

InnoDB的缓冲池是内存中缓存数据页和索引页的地方,它的大小直接影响读写性能。8.0的官方默认只有128M,对于动辄几十G的数据量来说,这个值基本等于没调。

怎么估算合理值?前提是纯InnoDB库。经验公式是:

  • 物理内存的50%到70%可以预留给buffer pool,前提是数据库服务器是专用实例;
  • 再留出连接线程、临时表、排序缓冲和系统本身的余量;
  • 如果数据总量小于物理内存,上限可以接近数据总量,因为全部热数据都在内存里就是最理想状态。

我调过一台内存64G、数据量大概200G的实例,配置大致是这样的:

[mysqld] innodb_buffer_pool_size = 32G innodb_buffer_pool_instances = 8

innodb_buffer_pool_instances在8.0里通常和buffer pool大小关联比较好,官方建议每实例不低于1G,所以设8个实例比较稳。改后缓存命中率从92%提升到接近99%,在同样业务负载下,整体响应时间下降非常明显。

还有一个细节:8.0里buffer pool可以手动设置,也支持运行期动态调整。官方提供了ALTER INSTANCE ENABLE INNODB BUFFER POOL RESIZE这类能力,但我个人习惯在生产环境里直接设定一个大值,不开自动收缩,避免运行期内存抖动。动态调整更适合云上和容器化场景。

3.2 redo log 与 doublewrite

redo log是崩溃恢复的关键。事务提交后,数据页不一定要立刻刷盘,但redo log必须按策略写。经典参数是innodb_log_file_size和innodb_log_files_in_group,两者乘积就是redo log总大小。8.0.30之前默认每个48M、两个一组;8.0.30之后可以用innodb_redo_log_capacity更直观地控制总量。

生产环境一般可以按下面的思路参考设置:

  • 写入频繁、数据量大的库,redo log总容量建议配到1G到4G;
  • 如果状态信息里Log_writes和Fsyncs比例高,且经常出现checkpoint相关告警,基本是redo log偏小;
  • redo log太小,检查点刷得太频繁,写放大严重;redo log太大,崩溃恢复时间会变长,但一般影响可控。

doublewrite是另一个容易被忽略的机制。它保证页写入过程中发生崩溃时,能有一个一致性副本。SSD时代确实有人想关掉它换性能,但官方强烈不建议关,因为断电或系统崩溃时可能产生损坏的数据页。如果用的是自带原子写能力的存储,可以考虑关闭,否则请保持默认开启。

我自己的权衡是:宁可少一点极端性能,也要保证掉电安全。数据库崩了可以重来,页损坏往往是静默的,排查成本极高。所以innodb_doublewrite=ON在多数生产环境里都应该保留。

3.3 flush_log_at_trx_commit 的取舍

事务提交时redo log怎么刷盘,有三个经典取值:

  • 0:每秒刷一次,性能最好,但最多可能丢1秒的事务;
  • 1:每次提交都刷盘,最安全,也是满足ACID要求的默认值;
  • 2:每次提交写入操作系统缓存,每秒钟再刷盘,比1快、比0稳,但主机断电可能丢已提交事务。

很多优化文章说改成0或2能提升性能,但代价是数据安全。线上核心交易系统不建议改,DBA的职责首先是保证数据不丢。只有非核心查询库、报表库、临时分析库,或者能容忍少量丢失的场景,才考虑用0或2。

我见过一个例子:某报表组自己把实例改成0,跑批速度快了40%,后来一次数据库异常重启,最新几分钟的报表数据全没了。幸好字段还能从上游重新抽取,否则就是事故。所以,性能调优永远要和安全性要求放在一起评估,不是单看一个数字好看。

另外一个关联参数是innodb_flush_method。Linux下通常设置成O_DIRECT,跳过操作系统页缓存,让InnoDB自己管理缓冲。但要注意如果文件系统不支持,启动时会报错。用fdatasync更安全但多一层拷贝。云厂商的实例一般已经有合理默认值,不是必须要动它。

3.4 后台线程与并发瓶颈

InnoDB的并发控制相关参数主要有几个:

  • innodb_thread_concurrency:进入内核并发的线程数限制,默认0表示不限制。并发太高时系统会抖动,但人为限流可能反而降低吞吐;
  • innodb_read_io_threads和innodb_write_io_threads:控制读写IO线程数,高并发SSD环境可以适当调高,但超过8一般收益就有限;
  • innodb_purge_threads:清理历史版本数据的线程,8.0里默认4,一般不用改。

后台线程和连接数是两个维度。有人把连接数拉到很高,却发现CPU没跑满、磁盘没吃满,大概率是InnoDB层面没有更多线程并行处理。不过也别急着堆参数,先在压测下观察性能等待事件,再决定要不要加线程。盲目加线程反而会让上下文切换变严重,造成整体吞吐下滑。

4. 日志配置:出问题时它就是你唯一的线索

4.1 二进制日志的正确打开方式

binlog是主从复制和数据恢复的基础。配置文件里典型设置如下:

[mysqld] server-id = 1 log-bin = mysql-bin binlog_format = ROW expire_logs_days = 7 # 8.0及以上推荐 binlog_expire_logs_seconds = 604800 max_binlog_size = 1024M sync_binlog = 1

server-id不设置,8.0里开binlog会直接启动失败。binlog_format强烈建议用ROW,因为STATEMENT格式在函数、存储过程、非确定性语句上会产生主从数据不一致。max_binlog_size控制单个binlog文件大小,超过后滚动,默认1G也比较合理。

重点提醒:sync_binlog=1让每次事务提交都同步binlog到磁盘,和innodb_flush_log_at_trx_commit=1搭配时,能保证最多只丢一个事务,但性能开销高。如果你的库能容忍极少数据丢失,sync_binlog=0或=N会快不少。核心库我建议保持1,尤其是订单、支付这类写入强一致的系统。

binlog保留天数也要想清楚:留太短,出故障想回滚时没有日志;留太长,占用磁盘空间且影响备份恢复时间。我的做法是保留7天,再叠加每日全量备份,异常时能用“全量+binlog”恢复到任意时间点。不要只盯着“省空间”,要始终问自己一句话:真出事的时候,这些日志够不够我救回全量数据。

4.2 慢查询日志与阈值设置

慢查询日志是性能优化里最直接的抓手。常用配置项是这些:

slow_query_log = ON slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = ON min_examined_row_limit = 100

long_query_time单位是秒,等于1就是记录超过1秒的SQL。建议从1开始,如果日志量太大再调成2,因为线上正常业务里,核心查询一般都不该超过1秒。log_queries_not_using_indexes会把全表扫描的SQL也记录下来,对排查缺索引非常有用,但会让日志暴涨,生产环境建议配合min_examined_row_limit过滤掉扫描行数过少的查询。

慢日志分析我习惯先用内置的mysqldumpslow工具,按执行次数、耗时排序,再看具体的SQL模板。有个容易被忽略的点:long_query_time统计的是执行时间,如果SQL在等待锁、等待IO,也算在执行时间里。所以慢日志里有些语句其实不是“慢在SQL本身”,而是“慢在资源竞争”。看到这类日志,不要急着加索引,先看锁等待和IO状态。

4.3 错误日志与日志轮转

log_error默认写到数据目录下的hostname.err,记录启动、关闭、表损坏、复制异常等关键信息。很多故障排查的第一现场都在这里。建议单独指定文件,别混在默认位置里:

log_error = /var/log/mysql/error.log

同时要注意系统里logrotate对MySQL日志的轮转配置。如果没有轮转,几年下来错误日志和慢查询日志会变成巨型文件,既难查又占空间。更麻烦的是MySQL自己持有文件句柄,直接删文件后不重建,新日志就不知道写哪去了。因此轮转时要执行FLUSH LOGS,或者使用配置了copytruncate的方式。

我之前真遇到过磁盘爆满的故障,排查后发现是binlog和慢日志文件没有正确轮转,占了上百G。后来加了cron任务和logrotate规则,并把expire_logs_days、slow_query_log文件路径都梳理清楚,磁盘使用率再也没有因为日志失控报警过。

5. 从硬件出发的性能调优思路

5.1 内存多大,buffer pool就多大?

很多人以为内存越大,buffer pool跟着调大准没错。但实际要考虑:

  • 服务器不一定只跑MySQL一个进程,操作系统本身也要占用内存;
  • 连接线程、临时表、排序缓冲、复制线程都有额外内存开销;
  • 如果开启performance_schema,内存占用也会明显增加。

一个比较稳妥的比例:MySQL专用主机,物理内存的60%到75%给buffer pool;如果机器上还有缓存中间件、应用服务的JVM,那MySQL顶多拿到40%到50%。我遇到过一台128G内存的机器,JVM占了8G、业务缓存占了32G,MySQL的buffer pool原来设了80G,结果频繁swap,性能反而差。把buffer pool降到48G之后,系统稳定了,慢查询数量也下来了。

所以配置内存参数前,先在操作系统层面看清楚是谁在占用内存。free -h、top这些命令不是摆给DBA看的,配置是跟着硬件和整体资源走的,脱离物理环境谈参数就是纸上谈兵。

5.2 磁盘类型不同,IO策略也不同

机械盘、SSD、NVMe的随机IO能力差距巨大。InnoDB的很多默认策略是针对老机械盘的保守值。如果数据库放在NVMe上,可以考虑调整:

  • innodb_flush_method=O_DIRECT
  • innodb_use_fdatasync=OFF
  • innodb_flush_neighbors=0,SSD上不需要把相邻页合并刷盘,关闭可以降低写放大;
  • innodb_io_capacity = 2000~4000,机械盘给200就合理,高性能SSD可以给到2000甚至更高;
  • innodb_io_capacity_max比io_capacity再高一倍。

innodb_io_capacity是InnoDB判断“刷多少脏页”的重要参数。设置太低,脏页积压到阈值后会造成写入抖动;设置太高,后台刷盘太激进,可能影响前台查询。所以它不是一个拍脑袋的数字,应该结合磁盘的实测IOPS来定。

我做过一组对比,同一套业务压测,机械盘下io_capacity=200,SSD下调到2000,写吞吐和延迟都更稳定。代价是SSD寿命消耗略增,但企业级SSD基本扛得住这个消耗,这个取舍是合理的。

5.3 会话级临时表与排序缓冲区的坑

有些参数是按“每次会话”分配的,比如sort_buffer_size、join_buffer_size、tmp_table_size。表面上单值不大,但连接数一多,内存总和会被瞬间放大。比如sort_buffer_size=8M,500个连接同时排序,理论峰值就是4G,虽然实际操作中未必真有那么多并发,但压测时就能看到内存飞涨。

这类参数在配置文件里适合设置一个“够用但不过分”的值:

sort_buffer_size = 2M join_buffer_size = 2M read_buffer_size = 1M read_rnd_buffer_size = 1M tmp_table_size = 64M max_heap_table_size = 64M

tmp_table_size和max_heap_table_size两者取小值决定临时内存表上限,超过后转磁盘临时表,速度会暴跌。大批量GROUP BY、ORDER BY的报表查询慢,很多时候就是临时表落到磁盘导致的。

另外特别提醒:很多参数支持在会话级别调整,SET SESSION sort_buffer_size = 16M,不需要全局改。开发或排查时临时调大没问题,但配置文件里如果写一个很大的全局值,坑的是所有连接。这种“全局改小、会话按需临时调大”的习惯,是DBA基本素养。

6. 配置文件排错实录

6.1 为什么改了没生效

故障现场最常见的一句话是:我改了配置文件,重启了,但是SHOW VARIABLES还是老值。常见原因有这几种:

  1. 改错文件了:启动实际读取的路径不是你改的那个;
  2. 改错段落:参数写进了[client]而不是[mysqld],导致服务器端没加载;
  3. 大小写或拼写错误:MySQL不报错,只是忽略或当成未知参数;
  4. 命令行参数覆盖:启动脚本里的--param优先级最高;
  5. 动态参数被运行期修改:比如SET GLOBAL改了内存值,但没同步到配置文件,重启后又是老值;
  6. 参数不是热加载参数:有些参数含义是“下次启动生效”,并不是动态改完立刻变。

排查顺序建议是:先执行mysql --help | grep -A 1 "Default options"确认读取路径;再grep对应参数在哪些文件里出现;然后看启动命令;最后用SHOW VARIABLES LIKE比对当前值。按这个顺序走,基本能定位到90%的“没生效”问题。

6.2 启动失败与日志辨析

配置文件错误最常见的表现是MySQL启动失败。但新手容易一头扎进配置文件里反复改,而忽略错误日志。优先级应该是:先看log_error里的提示,再回头检查配置。

比如报错提示里出现unknown variable 'max_connections=200',说明拼写错误或者参数不属于当前版本。还有[ERROR] Unrecognized data type for column这类启动错误,往往和配置没有直接关系,而是表定义有问题。这时候你改一万遍配置也没用,要看的是数据字典和表结构。

一个排查技巧:先执行mysqld --validate-config校验配置,返回OK再启动,能省去很多折腾。另外,修改配置后重启失败,如果看到“Starting MySQL”卡住,多半是参数取值不合规,比如innodb_buffer_pool_size低于某阈值,或者datadir权限不对。把错误日志里第一行[ERROR]开头的内容拿出来搜一下,通常答案就在那里。

6.3 用动态参数和performance_schema验证配置

很多参数支持SET GLOBAL动态修改,适合在排查阶段先验证效果。比如怀疑max_connections不够,可以先执行SET GLOBAL max_connections = 500;,观察一段时间的进程数和错误日志,确认有效后再写回配置文件。这样能避免一改配置就重启、重启失败导致长时间不可用。

但动态参数有一个坑:重启后丢失。所以我在所有操作规范里都明确一条:动态调整只是临时手段,最终一定要落回配置文件。这个“落回”不是简单写一次就完,还要确认值和当前运行值一致,否则下次重启又回去了。

performance_schema在MySQL 8.0里默认开启,可以用来查看等待事件、内存占用、连接情况。比如排查最耗时的SQL时,可以执行:

SELECT * FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 20;

你就能直接看到最耗时的SQL模式,结合慢日志和EXPLAIN,通常就能定位需要加索引或者改写SQL的问题。内存相关可以直接查:

SELECT * FROM sys.memory_by_host_by_current_bytes;

用这类手段去验证参数调整后的实际效果,比“拍脑袋调大一个数”可靠得多。配置优化的核心是“测量→调整→验证→固化”的循环,而不是抄一堆模板参数到配置里就算完事。

关于MySQL配置文件,我最后想说的是:它永远不只是一堆参数的罗列。每个参数背后都是对硬件、业务、安全三者的权衡。我见过把配置改得像论文一样复杂的系统,最后慢查询也没少多少;也见过只有十几行配置的小库,跑得异常稳定。核心经验就一句话:先搞清楚业务到底是什么模式,再动手改,每次只改一个变量,验证完再动下一个。

这些年踩过最深的坑,一个是改完配置忘记确认加载路径,另一个是临时动态调参后没回写配置文件。现在我的流程很简单:改前备份、改后校验、小步验证、动态参数回写。记住这套,MySQL的配置文件就没那么多玄学了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进