把三个MySQL节点压到10万TPS,Raft日志复制到底在干嘛?

上周做了一次压测。三个MySQL节点跑Group Replication。目标是10万TPS写入。

压到7万的时候,延迟开始抖动。压到10万,P99直接飙到20毫秒。

当时第一反应是网络带宽不够。换了万兆网卡还是抖。

后来才搞明白,真正的瓶颈在Raft日志复制这一层。日志追不上写入速度,节点间commitIndex差距一拉大,整个集群就开始抖。

今天把这次压测和背后的原理一起讲清楚。各位看完应该能搞懂:Raft到底在复制什么,为什么会影响性能。


一、先说结论

对比维度 MySQL主从复制 Raft日志复制
一致性模型 最终一致 强一致
主库确认 异步不等ACK 等多数派ACK
自动选主 不支持(靠MHA等) 协议内置
脑裂防护 需外部仲裁 Quorum机制
写入延迟 略高
节点数扩展 无限从库 奇数节点,通常3或5

说白了:主从复制的代价是数据可能丢;Raft的代价是写入慢一点。各有取舍。


二、主从复制为什么扛不住

先回顾一下主从复制的流程。

主库执行完事务,把变更写入binlog。从库的IO线程把binlog拉过去,SQL线程回放。整个过程是异步的——主库不需要等从库确认就能返回客户端。

问题很直接:主库宕机时,已提交但没同步到从库的事务就丢了。在10万TPS的写入压力下,每秒有上万条事务可能处于"主库已提交、从库未同步"的状态。一出故障就是大面积数据丢失。

我当年做后端开发的时候,觉得主从复制够用了。直到某天凌晨主库磁盘故障,切到从库发现丢了上百条订单。那之后才开始认真研究一致性协议。

讲真,主从复制解决的是数据备份问题,不是一致性问题。


三、Raft的核心机制

Raft是2014年斯坦福提出的分布式一致性算法。一句话总结:多数节点确认过的日志,就不可变了。

几个关键概念:

Entry(日志条目):每个写请求对应一个Entry。包含操作内容、任期号Term、索引号index。

Commit(提交):Leader收到多数派确认后,将该Entry标记为已提交。

Apply(应用):已提交的Entry应用到状态机。在MySQL里就是回放binlog。

概念 含义 MySQL对应
Entry 日志条目 binlog event
Commit 多数派确认 事务提交确认
Apply 应用到状态机 binlog回放
Term 任期号 无直接对应
CommitIndex 最新已提交位置 GTID集合

四、日志复制的完整流程

以三节点集群为例,拆解一次写请求的完整生命周期。

1. Leader接收写请求

客户端向Leader发送写请求。Leader为该请求生成一个新Entry,包含当前Term和递增的index,追加到本地Raft日志。

此时Entry状态:已追加但未提交。

2. AppendEntries RPC

Leader通过AppendEntries请求把Entry发给两个Follower。请求中携带:

  • 前一条Entry的index和Term(用于日志匹配校验)
  • 新Entry内容
  • Leader的commitIndex

3. Follower校验

Follower收到AppendEntries后做两件事:

第一,校验前一条Entry的index和Term是否匹配本地日志。不匹配就拒绝,Leader需要回退重发。这是Raft保证日志一致性的关键机制——Log Matching Property

第二,匹配就追加Entry到本地日志,返回成功。

4. Leader推进CommitIndex

当Leader收到超过半数节点(三节点就是两个,含自己)的确认后,将该Entry的index推进commitIndex。

从此刻起,这条Entry在逻辑上已提交且不可变。

5. Apply到状态机

所有节点(包括Leader和Follower)将已提交的Entry应用到状态机。

在MySQL Group Replication的语境下,就是把对应的binlog event回放到InnoDB存储引擎。

Leader:  追加日志 → 复制给Follower → 收到多数派ACK → 推进commitIndex → Apply
Follower: 收到AppendEntries → 校验日志匹配 → 追加本地日志 → 返回ACK → Apply

五、压测数据:3节点集群在10万TPS下发生了什么

测试环境:

配置项 规格
节点数 3
CPU 16核
内存 64GB
磁盘 NVMe SSD
网络 10GbE
MySQL版本 8.0.32
事务模式 单行UPDATE

结果:

压力(QPS) 平均延迟 P99延迟 CPU使用率 网络带宽
1万 2.3ms 5ms 25% 200Mbps
3万 3.8ms 8ms 45% 600Mbps
5万 5.2ms 10ms 65% 1Gbps
10万 8.5ms 18ms 85% 2Gbps

数据摆在这,说几个值得注意的地方。

延迟拐点在5万QPS附近。5万以下还算线性,5万以上增速明显加快。瓶颈出在Leader节点,所有写请求和AppendEntries RPC都得它来扛。

到10万QPS的时候,网络带宽才是真正被低估的那个。每秒10万个Entry,每个大约500字节,单向就是50MB/s。加上ACK返回和心跳,实际带宽占用在2Gbps左右。

CPU这边倒不意外。Leader比Follower高了40%,高并发下这个差距还会拉大。Raft就这特性,Leader是单点瓶颈,没法绕。


六、节点数扩展:3节点 vs 5节点 vs 7节点

保持相同硬件和10万TPS目标:

节点数 Majority 平均延迟 P99延迟 吞吐变化
3 2 8.5ms 18ms 基准
5 3 11.2ms 22ms -15%
7 4 14.8ms 28ms -28%

节点数增加,每次Commit要等更多ACK,延迟自然上升。

这就是Raft的工程权衡,节点越多容错越强,写入性能越差。生产环境大多数选三节点,够用。


七、故障恢复:Raft和主从的本质区别

故障场景 主从复制 Raft
单节点宕机 需要手动或MHA切主 自动选主,秒级恢复
网络分区 可能脑裂 Quorum保证一致性
选主期间 不确定,可能丢数据 拒绝写入,保证不丢
恢复后同步 可能需要全量重搭 自动日志追赶

我那次凌晨丢订单的经历,本质上就是主从复制在故障切换时没有协议层面的数据保障。如果当时用的是Raft,已提交的Entry必然存在于多数派节点上,切主后数据不会丢。


八、对后端开发者的启示

我写了十年Java,习惯从应用层想问题。但数据库集群的一致性协议是另一层抽象,应用层的ORM再怎么封装也帮不了你。

选型就一个判断:你的业务能不能容忍数据丢?

金融、支付、订单系统,答案是不能。那就上Raft或者同等一致性协议。写入多个几毫秒延迟,换数据不丢。

日志、缓存、统计类数据,能容忍丢。那主从异步复制的性能优势很香,没必要多花成本。


九、踩过的坑

先说最容易被忽略的一条:不要只盯着吞吐量数字。10万TPS听着好听,P99抖到50ms业务方照样找你。压测报告要把延迟分布拉出来看。

Leader瓶颈问题前面提过了,这里补一个实操细节。写入压力大的时候Leader CPU飙高很正常,但Follower可能还很闲。监控的时候Leader和Follower的指标要分开看,commitIndex和AppliedIndex也得分开盯,一个是共识层,一个是存储层,两层都可能出问题。

节点数选奇数这个老生常谈了。三节点和四节点容错能力一样,四节点多花钱没收益。

网络带宽要提前算好。Entry大小乘QPS乘节点数,峰值再留点余量。日志匹配校验本身也有开销,Follower校验前一条Entry的index和Term,高并发下这步可能成为热点。

选主期间写入会抖动,客户端要做好重试和超时。Follower日志落后太多还会触发全量同步,复制延迟得盯着,别让它落过阈值。

还有个坑:别在Raft集群上跑OLAP。大查询阻塞状态机Apply,写入跟着遭殃。

最后一条,国产数据库的Raft实现各有差异,选型的时候不能光看"支持Raft"四个字,得看具体协议实现细节。


结语

回到开头的问题:压到10万TPS的时候,Raft日志复制到底在干嘛?

说白了就是在复制日志、达成共识。Leader把Entry发给Follower,多数节点确认了就算提交。代价是Leader变成了瓶颈,写入延迟多了几毫秒。

但已提交的数据不会丢。对大多数业务来说,几毫秒换这个保障,值。

我是底层玩家老张,咱们下篇见。

posted @ 2026-06-16 16:52  底层玩家老张  阅读(16)  评论(0)    收藏  举报