把三个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变成了瓶颈,写入延迟多了几毫秒。
但已提交的数据不会丢。对大多数业务来说,几毫秒换这个保障,值。
我是底层玩家老张,咱们下篇见。
浙公网安备 33010602011771号