replication - log - replay
05. 主从复制:复制的都是日志
0. 本章先解决什么问题
MySQL 主从复制、Redis 主从复制、Kafka 副本机制,我最初是当三个独立专题学的,每个都有一堆专有名词:binlog、relay log、PSYNC、repl_backlog、ISR、HW。本章先给出统一它们的那句话,再把三家逐个拆到零件级:
两台机器,初始状态相同,按相同顺序执行相同的操作序列,
终态一定相同。
所以复制一个数据库,只需要复制它的操作日志并重放。
这叫状态机复制。上一章说日志是复制的载体,本章就是那个预告的展开。
这张图怎么读
上半是通用骨架:主节点追加日志,日志通过网络流向从节点,从节点重放。下半是三个系统往骨架上的对号入座:MySQL 送 binlog,Redis 送命令流,Kafka 的 follower 直接拉日志本体。
1. 为什么要复制:三个目的先分清
- 高可用: 主挂了,从顶上(下一章故障转移的前提)
- 读扩展: 读请求分流到从库,主库只扛写
- 数据安全: 多一份物理拷贝,单机磁盘损坏不等于数据消失
注意复制不解决容量问题:每个从节点都存全量数据。数据大到一台装不下,要的是分片(第 7 章),复制和分片正交。
状态机复制成立有两个前提,后面每家的设计都在保它们:操作序列有全序(谁先谁后全网一致),以及每个操作是确定性的(同样输入必得同样输出)。上一章 binlog 的 row 格式取代 statement,就是在保第二条。
2. MySQL:binlog 从主到从的四线程流水线
2.1 完整链路
- 主库:事务提交时写 binlog
- 主库 dump 线程:每个从库连上来,主库派一个 dump 线程
盯着 binlog 有新事件就推给它 - 从库 IO 线程:收下来先原样写进本地的 relay log(中继日志)
- 从库 SQL 线程:读 relay log,逐条重放
为什么中间要过一道 relay log:接收和重放解耦。网络快慢和重放快慢互不拖累,从库崩了重启接着重放本地已收的,不用重新拉。这个”先落地再处理”和消息队列先入队再消费同构。
排查时的对应关系:SHOW REPLICA STATUS 里 IO 线程状态对应”拉得动吗”(网络、主库压力),SQL 线程状态对应”放得完吗”(重放速度),Seconds_Behind_Master 是 SQL 线程手里这条事件距它在主库发生时刻的差。
2.2 定位方式:从 file + position 到 GTID
- 老式:从库记”我读到 mysql-bin.000042 的 1337 字节处”
主从切换时,新主的文件名偏移和老主对不上,人肉换算,极易出错 - GTID:每个事务标一个全局唯一编号(源库 uuid:递增序号)
从库只声明”我执行过的 GTID 集合”,
连上任何一个新主,双方对比集合自动续传(auto position)
主从切换从”人肉找位点”变成”自动对表”
2.3 同步程度:三档加两个半同步细分
复制可靠性可以逐档理解:
- 异步复制(默认):主库本地提交后立即答复客户端,不等待 binlog 发往从库。主库若在发送前宕机,从库没有这笔事务,故障切换后数据就会丢失。
- 半同步复制:至少一个从库确认“收到 binlog”后,主库才答复客户端。这里确认的是写入
relay log,不是已经重放完成。AFTER_COMMIT(旧模式):主库先完成本地提交,再等待从库确认。等待期间其他会话已经能看到这笔事务;如果此时主库崩溃而从库尚未收到,就会出现“别人看见过的数据后来丢了”。AFTER_SYNC(MySQL 5.7 默认,无损半同步):先等待从库确认,再完成主库本地提交。从库尚未确认的事务在主库上也还不可见,因此主库此时崩溃不构成已提交数据丢失。
- 组复制 MGR:把写集合广播给成员并经过多数派认证,可靠性模型由主从确认继续推进到共识。
2.4 主从延迟:单写多读架构的固有病
延迟的来源要按流水线的三段分开找:
拉不动: 主库 binlog 产得太快、网络带宽不足(大事务一次产几 GB)
放不完: SQL 线程重放慢,经典原因三个:
- 老版本单线程重放对阵主库多线程并发写,天然追不上
- 大事务: 主库跑 10 分钟的 UPDATE,从库也要跑 10 分钟,
期间延迟直线上升 - 从库机器或参数比主库弱(常见于”从库反正只读”的省钱心态)
并行复制的演化就是在治”放不完”:
- 5.6:按库并行。不同 database 的事务可以并发重放,单库场景无效
- 5.7:按组提交批次并行(LOGICAL_CLOCK)。
上一章组提交时说过”同批 fsync 的事务标同号”,
能同批提交说明在主库上互不冲突,从库就敢并发重放它们 - 8.0:WRITESET。按行级写集合判断冲突,
不同批次的事务只要没碰同一行也能并行,并行度再上台阶
业务侧的配套纪律:拆大事务(一次删百万行改成分批),避免无主键表(row 格式重放定位行要全表扫)。
2.5 读写分离的一致性:你会读到过去
复制是异步的,从库永远活在主库的几毫秒到几秒前。读写分离一上,业务就要面对”刚写的数据查不到”:
典型事故: 下单成功 -> 跳转订单页(路由到从库)-> 订单不存在
处理手段按成本排序:
- 强一致的读强制走主库(按业务标记,最常用)
- 会话粘性: 同一用户写后一段时间内的读都走主
- GTID 等位: 读从库前先等它追上”我刚才那次写”的 GTID
- 接受不一致,前端乐观展示(很多场景其实可以)
没有免费午餐,只有”哪些读必须新、哪些读可以旧”的业务分类。完整展开在 主从复制 · 运维篇 和 读写分离。
3. Redis:全量快照 + 增量命令流
3.1 首次同步:又见”快照 + 增量”配方
从节点执行 replicaof 之后的握手,教科书级复现上一章的配方:
从: PSYNC ? -1 (我什么都没有,求全量)
主: FULLRESYNC
fork 子进程生成 RDB
生成期间的新写命令暂存进该从库的复制缓冲
从: 清空自己 -> 载入 RDB -> 重放暂存的增量 -> 进入在线复制
在线复制: 主每执行一条写命令,异步转发给所有从
两个工程细节:
无盘复制 repl-diskless-sync: 主库磁盘慢时,RDB 不落盘,
fork 出的子进程直接往 socket 写,省一进一出两趟磁盘
fork 的代价: 和实例内存量成正比(页表拷贝 + 后续写时复制),
大实例每次全量同步都是一次抖动源,所以要极力避免无谓的全量
3.2 断线重连:backlog 环形缓冲决定补量还是重来
主库把最近的写命令流同时抄一份进 repl_backlog(环形缓冲,默认只有 1 MB),断线重连时:
断线从库会发送 PSYNC <replid> <offset>,告诉主库自己上次所属的复制系谱和已经处理到的进度。主库随后判断:
replid匹配且offset仍在 backlog 环内:返回+CONTINUE,只补缺失的那一段,完成部分重同步。replid不匹配,或offset已被 backlog 覆盖:重新执行fork + RDB的全量同步;对大实例而言,这正是代价最高的路径。
backlog 的容量算术直接决定线上稳定性:
需要的 backlog >= 主库写入速率 x 最长容忍断线时长
例: 写入 5 MB/s,想扛住 60 秒网络抖动 -> 至少 300 MB
默认 1 MB 在生产写入量下只够撑零点几秒,
这就是”网络一抖就触发全量同步风暴”的标准病因
4.0 的 psync2 补了另一个洞:从库记两代身份证(replid 和 replid2)。故障转移后新主继承老主的 replid 作为 replid2,老部下连上新主时依然能对上身份、走部分重同步,不用因为换了主就全体全量。
3.3 在线复制的语义
传播是异步的: 主执行完就答复客户端,命令在路上丢了就丢了
从库默认只读(replica-read-only)
WAIT N t: 罕用的补丁,阻塞到至少 N 个从库确认了当前偏移,
给个别关键写一点半同步的意思,但没有回滚语义,不是真同步
级联复制: 从库还可以再挂从库,分摊主库的复制扇出压力
细节在 yori - 持久化 & 集群 &哨兵。
4. Kafka:日志本身就是数据,复制反而最纯粹
4.1 拉模型和 ISR
每个 partition 一个 leader、若干 follower,follower 像普通消费者一样向 leader 发 fetch 请求拉日志追加到本地。两个进度指针是理解一切语义的钥匙:
LEO(log end offset): 每个副本自己日志的末尾
HW(high watermark): ISR 全体都已复制到的位置
leader 收集所有 ISR 成员的 LEO,取最小值定为 HW
消费者只能读到 HW 之前的消息
(HW 之后的数据还没被足够多副本持有,leader 崩了可能回退,
先藏着不给消费者看,避免”读到过又消失”)
ISR: 跟得上 leader 的副本集合。
落后超过 replica.lag.time.max.ms(默认 30 秒)就被踢出,
追上了再回来。ISR 收缩是集群健康的头号观察指标
4.2 acks:把同步程度的滑杆交给每次写入
acks=0 发出去就算成功。丢了无感知,只配点日志埋点用
acks=1 leader 写入本地就确认。leader 崩且数据没复制走则丢
acks=all ISR 全体确认才成功,配 min.insync.replicas=2 使用:
ISR 缩到 1 个(只剩 leader)时直接拒写,
宁可不可用也不写出”只有一份”的数据
unclean.leader.election.enable=false(默认):
ISR 全灭时不许 ISR 外的落后副本竞选 leader,
宁可分区不可用也不接受数据回退
对照前两节:acks=1 近似 MySQL 异步,acks=all 加 min.insync.replicas 已经踩进多数派地界。Kafka 把别家藏在服务端配置里的滑杆,直接暴露给每个生产者按 topic 自选。
4.3 leader epoch:修”按 HW 截断”的老 bug
老版本 follower 重启后按自己记的 HW 截断日志再追,但 HW 的传播比 LEO 慢一拍,特定时序下会把已提交的数据截掉,或者新老 leader 数据分叉。
修法是给每任 leader 编号(leader epoch),副本重启后先问现任 leader”你这任从哪个 offset 开始”,按任期边界截断而不是按 HW。
这正是下一章”纪元编号封印旧时代”思想在数据面的应用,两章在这里提前握手。
5. 一张对照表收拢
| MySQL | Redis | Kafka | |
|---|---|---|---|
| 复制的东西 | binlog(逻辑) | 写命令流(逻辑) | 日志本体 |
| 全量引导 | 备份导入 | fork RDB | 从头拉日志 |
| 增量断点 | file+pos / GTID | offset + backlog 环 | fetch offset |
| 传输方向 | dump 线程推流 | 主动推给从 | follower 拉 |
| 同步程度 | 异步 / 半同步 / MGR | 异步(WAIT 补丁) | acks 每写可选 |
| 可见性控制 | 无(从库读到啥算啥) | 无 | HW 之前才可见 |
| 延迟观察 | Seconds_Behind_Master | offset 差 | ISR 收缩 |
记法:列是三个产品,行是同一骨架的七个部位。新遇到任何带副本的系统(ES、MongoDB、etcd),拿这七行去问,答案都填得进去。
6. 联系实际:复制视角的排查清单
-
现象:从库延迟持续增长
-
方向:先分”拉不动还是放不完”(IO 线程和 SQL 线程哪个落后),
放不完就查大事务、并行复制配置、从库配置 -
现象:Redis 从节点频繁全量同步,主库周期性抖动
-
方向:网络抖动 + backlog 太小,offset 总掉出环外;
按”写入速率 x 断线时长”重算 repl-backlog-size -
现象:Kafka 生产端报 NotEnoughReplicas
-
方向:ISR 收缩到小于 min.insync.replicas;查慢副本的磁盘和 GC
-
现象:主从切换后少了一截数据
-
方向:异步复制的丢失窗口。评估半同步 AFTER_SYNC / acks=all /
多数派方案,以及业务是否值得为此付延迟 -
现象:读写分离上线后偶发”刚写的查不到”
-
方向:原理性现象。按业务分类哪些读必须走主,哪些可容忍旧
7. 学完本章你能解决什么问题
- 状态机复制的两个前提是什么,row 格式和 GTID 各在保哪条?
- 四线程流水线里 relay log 为什么存在,延迟怎么按段定位?
- AFTER_SYNC 和 AFTER_COMMIT 差的那半步,差出了什么后果?
- 并行复制三代(按库、LOGICAL_CLOCK、WRITESET)各靠什么判断”能并行”?
- Redis 部分重同步的判断条件是什么,backlog 该怎么算容量?
- psync2 的两代 replid 解决了什么场景的全量风暴?
- HW 为什么要挡住消费者,leader epoch 修了什么 bug?
- acks=all 加 min.insync.replicas 为什么已经算半只脚进了多数派?
核心一句话:复制就是把主节点的日志送到从节点重放;产品差异只在日志形态、增量断点怎么记、“等几个副本确认”这根滑杆拧到哪,以及未完全复制的数据给不给人看。