如果你第一次认真研究 MySQL 配置文件,大概率会经历这个流程:先在脑海里回忆 my.cnf 到底在哪,然后打开 /etc/my.cnf 一看就几十行注释,改了几个参数重启实例,最后发现压根没生效,或者直接起不来了。这篇文章不打算罗列官方文档里那几百个参数,而是把我这些年调 MySQL 配置文件的实际经验捋一遍——从找到正确路径、理解核心参数,到怎么安全地改、怎么确认改完有效果,再到字符集和 sql_mode 这些最容易翻车的角落。
不管你是刚接触 MySQL 的新手,还是被线上问题追着跑的运维或后端同学,按这个思路走一遍,至少能避开我踩过的大部分坑。
1. 配置文件去哪找:my.cnf 的搜索顺序与优先级
1.1 常见路径与系统差异
Linux 系统上配置文件一般叫 my.cnf,常见位置是 /etc/my.cnf 或 /etc/mysql/my.cnf;Windows 平台上对应的文件叫 my.ini,通常在安装目录或数据目录下;macOS 上则常见于 /usr/local/etc/my.cnf。如果你是从源码编译安装的,那大概率在 /usr/local/mysql/etc/ 下,甚至可能根本没有现成配置文件,MySQL 会以编译时的默认参数直接运行。
这一点很关键:很多人以为新装的 MySQL 一定自带一个写好的 my.cnf,实际上用官方包安装时,多数情况给的是一个几乎全是注释的模板,很多参数不写就等于用默认值。你要先搞清楚自己机器上到底是什么状态,再谈调优。
不同 Linux 发行版的习惯也不太一样。有的发行版喜欢把配置拆到 /etc/mysql/conf.d/ 之类的目录里,主配置文件用 !includedir 把这些目录扫进来,方便软件包各自维护配置片段;有的发行版就是一个 /etc/my.cnf 全搞定。Windows 下还要注意,MySQL 服务在安装时会记录基于哪个 my.ini 启动,改完文件后如果路径记错,服务照样读的是旧文件,这种问题排查起来特别迷惑。
1.2 多个配置文件是叠加不是单选
MySQL 在启动时会按固定顺序读取一组文件,不是只读其中一个。官方文档给出的经典搜索顺序大致是:/etc/my.cnf、/etc/mysql/my.cnf、/usr/local/mysql/etc/my.cnf、~/.my.cnf,按顺序逐个读取。同一个参数如果在多个文件里都出现了,后面读到的会覆盖前面的值;如果参数只在其中一个文件出现,那就用那个文件的值。换句话说,配置是"叠加"的,而不是"二选一"。
这个机制经常导致一个诡异现象:你在 /etc/my.cnf 里把 max_connections 改成 500,重启后 SHOW VARIABLES 一看还是 151。原因多半是在后面的某个文件里,又有另一个 max_connections=151 把它覆盖回去了。所以我处理线上配置问题的第一步,永远是先确认当前实例实际加载了哪些文件、每个参数的最终值来自哪个文件,而不是凭记忆猜路径。
1.3 一条命令找到真实读取路径
想知道当前这台机器上 MySQL 到底按什么顺序找配置文件,不需要翻文档,直接执行:
mysqld --verbose --help 2>/dev/null | grep -A 1 'Default options'输出里会列出完整的配置文件搜索路径。如果 mysqld 不在 PATH 里,就写上 mysqld 的完整路径。另外也可以用 mysqladmin 连上实例后来看:
mysqladmin variables | grep -E 'basedir|datadir|character_set_server'不过我自己的习惯是两步都做:先看启动时的搜索顺序,再用 SHOW VARIABLES 确认当前实际值。因为配置文件里写的值不一定等于最终生效值——有些参数会被启动脚本追加的参数覆盖,也会被 8.0 的 SET PERSIST 生成的 mysqld-auto.cnf 覆盖,这些细节后面详细说。
2. 参数逐行拆解:把配置按用途分成五大类
2.1 连接与控制类
连接类参数直接决定你的业务能同时开多少条连接、空闲连接能挂多久。最核心的是 max_connections,默认值 151(5.7 和 8.0 都是 151),对小业务可能够用,但只要出现连接池配置不当、慢查询堆积,连接数很快会被占满,应用侧就报 "Too many connections"。这东西不能随手乱调,后面我会专门讲怎么估算和验证。
wait_timeout 和 interactive_timeout 决定空闲连接多少秒后被服务端断开,默认 28800 秒(8 小时)。对短连接为主的应用,可以适当调小到 60~300,但要注意:连接池如果没做探活,服务端主动断开后,连接池里的"死连接"会导致请求失败,所以调小之后必须配合连接池的探活机制。back_log 则是在连接还处于握手阶段时允许内核排队等待的数量,默认 80 在连接洪峰时往往不够,建议 80~128,但前提是操作系统的相应内核队列参数也要跟着调。
还有两个容易忽略的:skip_name_resolve 设为 ON 后,MySQL 不再对客户端 IP 做反向 DNS 解析,能减少握手延迟,但授权表里的 host 就不能再用域名了,必须写成 IP、localhost 或通配符,否则本来能连上的账号突然连不上。max_connect_errors 默认 100,某个 IP 如果反复连接失败超过这个数,会被临时锁住,报 "Host is blocked" 错误,维护脚本或者监控探针很容易触发这个,排查很久才发现是被自己人锁的。
2.2 内存与缓冲类
这一组是对性能影响最直接的。重中之重是 innodb_buffer_pool_size,也就是 InnoDB 的缓冲池大小,它负责缓存数据页和索引页。默认只有 128M,对任何有真实业务量的实例都不够。专用数据库服务器上,业界常见的做法是设为物理内存的 60%~75%,但如果你是单机部署、还跑着应用和其他中间件,就要保守一些,留足操作系统和其他进程的内存,我一般建议从 40%~50% 起步观察。
缓冲池大了以后还要关注两个配套参数。一是 innodb_buffer_pool_instances,缓冲池超过 1GB 后,InnoDB 会按策略拆成多个实例,目的是减少并发访问时的锁竞争。手工设置时要注意:实例数乘上 chunk size 必须能被缓冲池大小整除,否则启动时会被自动调整,MySQL 8.0 里改这个参数要特别小心,容易触发"不是预期大小"的报错。
连接级别的内存参数则要反着来。sort_buffer_size、join_buffer_size、read_buffer_size 这些是 per-connection 分配的,不是全局共享的。很多人一看排序慢就把它调到 256MB,忽略了一个基本算术:如果有 300 个并发连接,光 sort_buffer 一项就要吃掉 300×256MB=75GB,服务器直接 OOM。排序缓冲给个 2~8MB 足够,与其加大缓冲区,不如回去看 SQL 有没有走对索引。tmp_table_size 和 max_heap_table_size 控制内存临时表的上限,默认 16M,排序、去重、GROUP BY 产生的临时表一旦超过这个值就会落到磁盘临时表,性能断崖式下降,但改这两项也要连同 max_connections 一起算内存账。
2.3 InnoDB 落盘与日志类
innodb_flush_log_at_trx_commit 是我每次都要跟团队强调的参数。它有三个取值,直接影响崩溃时的数据丢失窗口:
| 取值 | 行为 | 数据安全级别 | 典型场景 |
|---|---|---|---|
| 1 | 每次事务提交都把 redo log 刷到磁盘 | 最安全,最多丢 0 个事务 | 订单、支付、账务等核心业务 |
| 2 | 每次提交只写到操作系统缓存,每秒刷盘 | 实例崩溃时丢最后一秒数据,进程崩溃不丢 | 可容忍秒级丢失的业务 |
| 0 | 每秒刷一次磁盘 | 崩溃时可能丢最后一秒内的事务 | 性能优先、可容忍丢失的场景 |
金融、订单这类业务必须用 1,能容忍小概率丢失的可以考虑 2,性能差别明显。很多人为了性能直接改成 0,出问题再回来找我,我的态度是:没有明确评估过丢失窗口之前,别动它。
redo log 的大小的设置在 5.7 和 8.0 里完全不同。5.7 用 innodb_log_file_size,默认 48M,建议调到 512M~2G;8.0.30 之后改成了 innodb_redo_log_capacity,默认 100M,可以直接配置成 2G。redo log 太小会导致频繁触发刷盘,写密集型业务能明显看到性能波动;太大则崩溃恢复时要重放更多日志,恢复时间变长。我一般从 1G~2G 起步,再结合写入量观察。
binlog 相关的配置也要单独说。8.0 默认就是开启的,5.7 需要手动开。log_bin 指定 binlog 文件前缀,server-id 必须显式配置,否则同步场景起不来;binlog_format 强烈建议 ROW,因为 statement 格式在函数、存储过程、非确定性语句上容易造成主从数据不一致。binlog 的保留策略在 5.7 用 expire_logs_days,8.0 里这个参数已被弃用,改用 binlog_expire_logs_seconds,不清理的话磁盘会被 binlog 吃满,这是最常踩的运维事故之一。
2.4 查询日志与慢日志
慢日志是排查性能问题的第一抓手。slow_query_log=1 开启,long_query_time=2 表示超过 2 秒的查询进日志。这个阈值别设成 0 或 0.1,否则生产环境会瞬间刷出海量日志,磁盘和 IO 都被拖累。log_queries_not_using_indexes 可以把没走索引的查询也记下来,刚上线时开着能发现问题 SQL,但要记得及时关掉,因为很多查询本身并不慢,一直开着日志会非常多。
还有一个容易漏掉的:log_output 可以设置成 FILE 或者 TABLE。默认 FILE,写到日志文件里好定位;如果设成 TABLE,慢日志会写进 mysql.slow_log 表,查询方便,但表模型会导致大量写入拖慢系统,而且表增长后清理也麻烦。我实际中一直用 FILE 模式,配合每天按大小切割归档就够了。普通的 general_log 记录所有 SQL,对线上实例基本别开,那是压垮 IO 的最后一根稻草,只有在本地开发环境排查问题时我才会临时打开几分钟。
2.5 字符集、时区与 SQL 行为类
字符集这块,5.7 时代默认是 latin1,8.0 默认是 utf8mb4,差别巨大。latin1 存不了中文之外的多字节字符,更存不了 emoji,而且一旦建表时用了 latin1,后面想改编码要做表重建,非常痛苦。所以新环境我建议一上来就显式写 character_set_server=utf8mb4 和 collation_server=utf8mb4_0900_ai_ci(8.0)或 utf8mb4_general_ci(5.7),不要依赖默认值,因为默认值在不同版本之间会变。
时区参数 default-time-zone 也值得写清楚。很多业务的"时间错 8 小时"问题,本质是 JDBC 连接串里的 serverTimezone 和数据库时区对不上,或者数据库用了操作系统默认时区而操作系统时间不对。我推荐在配置文件里显式写成 default-time-zone='+08:00',这样无论服务器系统时区怎么变,数据库自身行为都稳定。sql_mode 单独拿出来说,因为它是 5.7 之后最容易制造线上事故的配置,具体坑我在最后一章展开。
3. 改配置不是改完就完事:生效方式与验证手段
3.1 三种生效方式:重启、SET GLOBAL、SET PERSIST
MySQL 参数分两类:动态参数和静态参数。动态参数可以不用重启,直接执行 SET GLOBAL 修改全局值,但要注意:SET GLOBAL 只影响之后的连接,已建立的连接会继续用旧参数,如果想对当前会话立即生效,要给对应的 SET SESSION;SET GLOBAL 的修改在重启后丢失,不会写回配置文件。有些参数根本不是动态的,只能在启动时决定,比如 lower_case_table_names,初始化之后再改,MySQL 8.0 甚至可能直接拒绝启动或导致表名访问异常。
8.0 引入的 SET PERSIST 解决了"重启丢失"的问题,它会把参数写到数据目录下的 mysqld-auto.cnf 里,重启后自动加载,优先级高于普通配置文件。这个机制很方便,但也带来一个新坑:你手工改了 /etc/my.cnf 想覆盖某个参数,结果重启后值还是没变,一看 mysqld-auto.cnf 里有个 SET PERSIST 留下的旧值在作怪。我建议线上环境对 mysqld-auto.cnf 内容保持透明,定期确认里面都有哪些参数,别等到出了问题才去翻。
3.2 语法校验与启动前检查
改完配置文件直接重启,是最容易把实例搞挂的路径。MySQL 8.0 提供了 mysqld --validate-config 命令,可以在不启动服务的情况下校验配置文件语法和参数合法性,有问题会直接报错。5.7 没有这个专用参数,我常用的替代方案是:
mysqld --verbose --help 2>&1 | head -100如果配置里有非法参数,这个命令同样会报错,合法时则打印帮助信息。启动前还有两个习惯动作:一是备份原配置文件,改完先 diff 对比,回滚时能一键还原;二是把数据目录的属主属组确认好,MySQL 通常要求数据目录属主是专用运行用户,否则即使语法全对,初始化或启动都会报权限错误。新装的实例如果启动失败,第一步看错误日志,不要在配置文件里瞎猜。
3.3 运行期怎么看参数和状态
配置文件写得再漂亮,最终都要用运行期信息验证。查看参数生效值用:
SHOW VARIABLES LIKE 'max_connections'; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_size';注意 SHOW VARIABLES 返回的是会话级视图,部分参数会和使用者有关;要看全局真实值,尽量用 SHOW GLOBAL VARIABLES。验证改动是否真的缓解了问题,则要配合状态变量:看 Threads_connected 判断连接数峰值,看 Innodb_buffer_pool_read_requests 和 Innodb_buffer_pool_reads 计算缓冲池命中率。命中率公式是 (read_requests - reads) / read_requests,长期低于 99% 就该考虑加大缓冲池或优化热点数据访问。性能诊断相关的库和表也值得用起来,但日常巡检阶段,上面几个命令就够定位 80% 的问题。
4. 一次真实的配置演进:从默认参数到能扛住业务
4.1 先别动手:摸清硬件和基线
我拿一台典型的单机配置举例:16G 内存、4 核 CPU、SSD 磁盘,跑着一个中等访问量的业务系统——注意这里有个很关键的前提:这台机器是混合部署,MySQL 和应用服务在同一台机器上。面对这种场景,第一原则是别照抄所谓最佳实践,因为网上大多数 buffer_pool 给内存 70% 的建议都假设你是专用数据库服务器。我给这套环境先定了两条底线:操作系统和其他进程留 4G 左右,MySQL 相关内存总量控制在 10G 以内,其中 buffer_pool 给 6G,剩下的留给各种 per-connection 缓冲和临时内存。
动手之前先记录基线。查看当前实例的空闲连接、慢查询数量、磁盘使用率、QPS 波动,有条件就用压测工具跑一轮只读和只写测试。没有基线就调优,等于闭着眼开枪,后面无论改好改坏都说不清楚。基线的另一个作用是设置合理的期望值:如果业务瓶颈本来就在 SQL 本身,你调再大的缓冲池,该慢的查询还是慢。
4.2 逐个参数的调整过程与理由
这个场景下我的配置文件调整顺序是固定的:先安全后性能,先全局后单点。第一步把 max_connections 从默认 151 调到 300,依据不是拍脑袋,而是观察 Threads_connected 的日常峰值——如果峰值长期在 80~100,那 300 就是 3 倍余量,够撑住突发流量。内存账也顺手算了:按每线程 1~2MB 的连接内存算,300 个连接占用不到 600MB,安全。wait_timeout 从 28800 调到 300,配合应用连接池的探活机制,减少僵尸连接堆积。
第二步是 InnoDB 内存。buffer_pool_size 设 6G,同时 innodb_buffer_pool_instances 设为 8,innodb_flush_log_at_trx_commit 先保持 1——业务里有订单数据,我不能替业务做"丢最后一秒"的决定。redo log 在 8.0.30 之后的版本直接配 innodb_redo_log_capacity=2G,这样写入高峰时不容易频繁刷盘。
第三步是日志和可观测性。开启 slow_query_log,long_query_time 先设 2 秒,观察一周后如果日志量很少,再收紧到 1 秒。binlog 开启并设 binlog_expire_logs_seconds 为 7 天对应的秒数,保留一周,配合每天检查磁盘使用率。第四步是字符集和时区,显式指定 utf8mb4 和 '+08:00',避免默认值在不同环境漂移。每一步改完都只重启一次,用 SHOW VARIABLES 逐条确认,而不是攒了一堆改动最后一次性生效,出了问题根本不知道是哪条配置引起的。
4.3 调整之后的验证与效果观察
重启完成不是终点,我会再花一周观察三个指标。第一是缓冲池命中率,用前面提到的两个状态变量算,目标是长期稳定在 99% 以上。如果命中率低,说明内存还是给小了,或者访问的数据太分散,这时候继续加大 buffer_pool 的收益已经不大,要反过来看业务 SQL 的访问模式。第二是 Threads_connected 曲线,如果调整后连接数仍然经常顶到 300,说明业务并发真的增长上来了,而不是配置不够,要继续排查应用侧连接池设置。第三是慢查询日志数量和类型,调优后同样的业务量下,慢查询数量和平均耗时应该稳步下降,如果没变化,就得回去看执行计划了。
这一轮配置演进下来,典型的效果是:高峰期连接数不再报警,磁盘 IO 的写放大明显降低,慢查询从最初的一页报表减少到个位数,剩下的基本是确实复杂的统计报表。需要强调,这套数值是我的场景下的选择,换一台机器、换一个业务模型,合理值完全不同。配置文件调优的价值不在于某个参数填多少,而在于这套"先测、再改、后验"的流程是不是被严格执行了。
5. 配置文件里的那些坑:字符集、sql_mode、版本迁移
5.1 字符集相关的三个坑
第一个坑是只改服务端不改连接。很多人把 character_set_server 改成 utf8mb4,但应用连接串里没有设置客户端字符集,驱动可能用旧编码去解读数据,结果写入正常、读出来乱码。所以排查乱码问题要同时看四个变量:character_set_server、character_set_client、character_set_connection、character_set_results,四者不一致,必然乱码。
第二个坑是建表时没有显式指定字符集,依赖了数据库级别的默认值。数据库默认值是 utf8mb4,不代表所有表都是 utf8mb4,老库迁移过来的表可能还是 latin1。我处理过不止一次"新库没问题,老库写入 emoji 报错"的情况,最后定位就是某张表建表时用的旧默认值。正确做法是迁移前统一查 information_schema 里各表的字符集,该转换的提前转换。第三个坑是排序规则的选择,不同 collation 在字符比较和排序上有差异,跨版本数据迁移后,如果排序规则不一致,索引匹配结果可能变化,导致同样的 SQL 走不同的执行计划。
5.2 sql_mode 导致的线上事故
MySQL 5.7 开始默认 sql_mode 里带了 ONLY_FULL_GROUP_BY,这是很多老业务升级后的第一灾难。老代码写 SELECT a, COUNT(*) FROM t GROUP BY b,在宽松模式下能跑,5.7 默认模式直接报错。我见过一次典型的升级事故:某系统从 5.6 升到 5.7,业务方没关注 sql_mode,上线当天大量 GROUP BY 查询报错,迫不得已临时把 sql_mode 改成不含 ONLY_FULL_GROUP_BY 的值才恢复。临时救火没问题,但长期依赖宽松模式,等于允许业务写不规范 SQL,后面再来一次升级还得炸。我建议是升级前先用测试环境跑一遍全量 SQL 回归,把所有违反 ONLY_FULL_GROUP_BY 的 SQL 改掉,再升级生产。
另一个相关坑是 STRICT_TRANS_TABLES。它让 INSERT 或 UPDATE 时数据类型不合法直接报错,而不是写入截断后的值(非严格模式下会截断并告警)。业务如果用了大量不规范写入,开启严格模式后会突然冒出一堆报错,但关闭它只是掩盖问题,坏数据进库后引发的逻辑错误更难查。我的态度:数据完整性和"能写进去"之间,必须选完整性,写不进总比写错强。
5.3 高版本迁移时的未知参数
配置迁移是大家最容易偷懒又最容易翻车的地方。把 5.7 的 my.cnf 原样搬到 8.0,启动时直接报 unknown variable,因为不少老参数在 8.0 被删了。最常见的几个:
| 老配置(5.7) | 8.0 状态 | 替代方案 |
|---|---|---|
| query_cache_type / query_cache_size | 已移除 | 删掉查询缓存相关配置 |
| innodb_large_prefix | 已移除(默认开启) | 删掉该行 |
| expire_logs_days | 已弃用 | 改用 binlog_expire_logs_seconds |
| innodb_log_file_size | 8.0.30 起弃用 | 改用 innodb_redo_log_capacity |
反过来,把 8.0 的配置用到 5.7 也会遇到问题,比如 innodb_redo_log_capacity 在 5.7 里完全不认识。处理跨版本配置迁移,我推荐的做法是:在新版本实例上用 mysqld --verbose --help 启动一遍,把输出里出现 unknown variable 的配置行全部清掉,再根据新版本的默认值和兼容性说明逐项核对。配置项不在多,而在准确,迁移后的实例跑得稳,比"看起来配置豪华"重要得多。
5.4 连接数与内存的算术题
最后再算一遍最容易让服务器 OOM 的账。连接内存 = max_connections × (sort_buffer_size + join_buffer_size + read_buffer_size + read_rnd_buffer_size + 基础线程内存)。如果 sort_buffer_size 设 8M、join_buffer_size 设 8M、read_buffer_size 设 1M、read_rnd_buffer_size 设 1M,每连接光这些就 18M,300 个连接就是 5.4G,再加上 buffer_pool 6G 和 redo、binlog 的缓冲,一个 16G 的机器很容易被吃满。所以我会先定 buffer_pool,再反推 per-connection 内存上限,最后才定 max_connections,顺序反了迟早出事。
运维上有句老话叫"配置是背着包袱走路的",每多设一个参数就多一个需要维护的假设。我在实际中看到太多配置文件被塞了几百行注释、几十个参数,但真正影响业务的不过十来个。建议每隔一段时间就做一次配置瘦身:删掉被注释的僵尸配置,删掉不再生效的旧参数,清掉从网上抄来却从没验证过的"优化项"。配置文件越干净,出问题时的定位速度越快。我自己的习惯是每次调完一个参数,就在配置文件里写一行注释说明当时的场景和理由,三个月后再回头看,这些注释比任何文档都有用。