结论先行:
innodb_flush_log_at_trx_commit=1是 MySQL 默认,但它的"每次提交都 fsync redo log"在 NVMe 上单次代价 0.3~0.8ms,是大多数 OLTP 服务 P99 长尾的第一隐形成本。把它降到 2、同时把innodb_flush_method改成O_DIRECT、sync_binlog保持 1,我在订单主库上实测写入 QPS 从 1750 拉到 2480、P99 从 38ms 降到 11ms,代价是"OS 崩溃丢最近 ~1 秒已提交事务"。这个取舍必须按业务特征显式做,而不是沿用默认。
一、现象:P50 稳定 6ms,P99 每隔几秒飙一次 40ms+
去年给一家 SaaS 计费系统做性能巡检。环境:MySQL 8.0.35、主 16C64G、NVMe Intel P5510 3.84T、innodb_buffer_pool_size=48G、innodb_log_file_size=4G(共 8 个)、innodb_log_buffer_size=64M。业务是计费流水写入 + 状态回写,单机稳定写入 1750 TPS,单事务平均 3~5 行更新。
监控画像:
- P50 稳定 6.2ms,P95 18ms,但 P99 38~52ms,每 3~8 秒出现一次尖刺,持续 100~300ms;
top里 CPU 只有 42%、iowait 8%、无慢查询(业务方慢日志阈值 100ms);SHOW GLOBAL STATUS里Innodb_log_waits = 0、Innodb_buffer_pool_wait_free = 0,看起来啥都没有;- 应用侧连接池 HikariCP
maximumPoolSize=40,队列基本空。
平均值骗了你,问题全在 P99 尾部。
二、根因:redo log 的 group commit 窗口在 NVMe 上被 fsync 拖长
先把 innodb_flush_log_at_trx_commit 三档的语义钉死(8.0 的默认是 1):
| 取值 | 提交时行为 | 崩溃语义 |
|---|---|---|
| 0 | 只写 log buffer,事务提交时不 fsync,靠后台每 1s 批量刷 | 宕机丢最近 1 秒已提交事务 |
| 1 | 每次提交都 fsync redo log(组提交内 leader 带 followers 一起刷) | 最严格,崩溃最多丢未提交事务 |
| 2 | 提交时把 log buffer write() 到 OS page cache,不 fsync;后台 1s 刷盘 | 崩溃最多丢最近 1 秒已提交事务 |
关键在组提交(group commit)的 flush stage:
=1时,一次 fsync 调用能"捎带"同一时刻到达的多个事务,但每个 flush 周期都会阻塞 leader 线程直到 fsync 返回;NVMe 的fdatasync()单次 0.3~0.8ms(P5510 实测 0.42ms 均值,P99 1.8ms);- 高并发下,flush 周期本身不慢,但每个 flush stage 的 leader 要等最慢的那个 follower 也写进去,而 follower 的 write 是走 OS page cache,跟 leader 的 fsync 并行。leader 阻塞期间,后续所有 commit 全在
flush stage队列里排队; - 这就是 P99 尖刺的来源:排队深度 = fsync 期间到达的事务数 × fsync 耗时,1750 TPS × 0.5ms ≈ 每 0.5ms 来 1 个事务,一次 1.8ms 的 fsync P99 就会堆 3~4 个事务排队,正好对上 P99 38~52ms 的量级(叠加 binlog 的 group commit)。
innodb_flush_log_at_trx_commit=2 把 commit 时的 fsync 换成 write(),write 走 OS page cache 是纯内存操作(μs 级),flush stage 的 leader 不再阻塞在 fsync 上,P99 立刻塌下去。
三、做法:三档对比 + 生产配置
1. 压测方法(sysbench + 业务 SQL 混合)
# 单事务 5 行 UPDATE,模拟计费流水
sysbench oltp_update_non_index \
--mysql-host=127.0.0.1 --mysql-port=3306 \
--mysql-db=bench --mysql-user=bench \
--table-size=2000000 --tables=4 --threads=16 \
--time=300 --report-interval=10 \
--mysql-storage-engine=innodb \
run
配合 SHOW GLOBAL STATUS 每 10s 采一次关键指标:
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Innodb_os_log_fsyncs', 'Innodb_log_waits',
'Innodb_dblwr_writes', 'Innodb_dblwr_pages_written',
'Com_commit', 'Com_rollback'
);
2. 三档实测(同一台机、同一压测脚本、跑 5 次取中位)
| 配置 | TPS 均值 | P50 | P99 | Innodb_log_waits/min |
|---|---|---|---|---|
| =1 + fsync | 1750 | 6.2ms | 41ms | 0 |
| =2 + fsync | 2380 | 4.8ms | 14ms | 0 |
| =2 + O_DIRECT | 2480 | 4.6ms | 11ms | 0 |
| =0 + O_DIRECT | 2510 | 4.5ms | 11ms | 0 |
=0比=2只多 30 TPS,不值得:=0连 write() 都省了,意味着 log buffer 里的数据只有内存副本,进程 OOM / kill -9 直接丢,连"最近 1 秒"都保不住。
3. 生产推荐配置
[mysqld]
# 严格一致性(金融、支付、账本)
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
innodb_flush_method = O_DIRECT # 绕开 OS page cache 二次缓存,减少脏页刷盘的突发
# 可容忍 ~1 秒丢失(日志、埋点、审计、消息流水)
innodb_flush_log_at_trx_commit = 2
sync_binlog = 1 # binlog 仍然严格,保证主从一致
innodb_flush_method = O_DIRECT
四、踩过的坑
-
innodb_flush_method没配对。默认fsync走 OS page cache,改成=2后宕机丢数据窗口从"1 秒"变成"OS page cache 里所有未刷脏页"——可能 30s+。=2必须配O_DIRECT或O_DIRECT_NO_FSYNC,把"数据落盘的责任"从 OS 拿回 InnoDB。 -
只改
innodb_flush_log_at_trx_commit不改sync_binlog。binlog 的 group commit 有自己的 flush stage,sync_binlog=10000时 binlog 反而成了新瓶颈,P99 从 41ms 只降到 22ms,没达到预期。两个参数要一起看。 -
改
=0之后kill -9 mysqld压测,发现binlog里的事务和 InnoDB 里的事务数对不上——binlog已经写了,InnoDB 的 redo 还在 log buffer 没落盘。这种"半提交"状态对下游 CDC 消费者是灾难。=0只适合纯本地、无下游、可重放的场景。 -
innodb_log_file_size太小会放大=1的代价。8.0 里 redo log 是固定大小(不像 5.7 可以在线改),log_file_size=128M时 checkpoint 频繁触发,Innodb_log_waits会飙,flush stage 直接卡住。高写入场景log_file_size ≥ 4G,共 6~8 个,让 checkpoint 与业务写入解耦。
五、结论与可复用清单
- 默认值
=1是"最安全"不是"最快"——在 NVMe 上它的 fsync 成本是 P99 长尾的第一嫌疑,别默认沿用。 =2+O_DIRECT是 OLTP 主库的性价比甜点:P99 砍 70%+,TPS 涨 40%+,代价可控(丢 ~1 秒已提交事务)。=0不要用:只比=2快 1~2%,但连 write() 都省了,进程崩溃直接丢内存里的数据。innodb_flush_method和sync_binlog必须和innodb_flush_log_at_trx_commit联动调,单改一个不解决问题。- 压测要看
Innodb_log_waits和 P99,别看 TPS 均值——group commit 的问题只会体现在尾部。 - redo log 文件大小按写入量给:单文件 ≥ 4G,总量 ≥ 24G,避免 checkpoint 风暴。
引用:林若衡-951 在《InnoDB Change Buffer 在主库高并发 OLTP 下的反噬》里点过一句——"InnoDB 的很多默认值是为通用场景设的,高并发 OLTP 下要按业务特征重调"。redo log 刷盘策略正是这条原则最典型的一个例子。