第 16 章

主从复制:PSYNC2 协议与复制积压缓冲区

第16章 主从复制:PSYNC2 协议与复制积压缓冲区

Redis 的主从复制是构建高可用系统的基石。本章深入剖析 PSYNC2 协议的握手流程、replid 机制、全量与部分重同步的判定逻辑,以及复制积压缓冲区的设计原理与生产调优。


16.1 主从握手完整流程(7步)

从库启动后,执行以下握手序列与主库建立复制关系。每一步都有明确的协议语义,理解这些细节对排查复制故障至关重要。

第1步:PING

从库连接主库的 TCP 端口后,首先发送 PING 命令,验证主库存活且能正常响应:

From slave → master: *1\r\n$4\r\nPING\r\n
From master → slave: +PONG\r\n

如果主库未响应或响应超时(受 repl-timeout 控制,默认60秒),从库断开并重试。这一步也是从库判断网络连通性的基础检查。

第2步:AUTH

若主库配置了 requirepass,从库发送 AUTH 命令进行密码认证:

From slave → master: *2\r\n$4\r\nAUTH\r\n$6\r\nsecret\r\n
From master → slave: +OK\r\n

从库侧配置:

masterauth <password>

注意:Redis 6.0+ 引入 ACL,主从复制也可使用 masteruser + masterauth 组合进行 ACL 用户认证:

# redis.conf (slave)
masteruser replication_user
masterauth replication_password

第3步:REPLCONF listening-port

从库上报自己监听的端口,主库用于记录从库信息(INFO replication 中显示):

From slave → master: REPLCONF listening-port 6380
From master → slave: +OK\r\n

第4步:REPLCONF capa

从库声明自己支持的能力(capabilities),主库据此决定使用哪种协议:

From slave → master: REPLCONF capa eof capa psync2
From master → slave: +OK\r\n

第5步:PSYNC

从库发送 PSYNC 命令,请求同步:

# 初次连接(无历史):
From slave → master: PSYNC ? -1

# 重连尝试部分重同步:
From slave → master: PSYNC <replid> <offset>

参数说明:

第6步:主库响应

主库根据请求类型返回两种响应之一:

# 全量同步:
+FULLRESYNC <replid> <offset>\r\n

# 部分重同步:
+CONTINUE <replid>\r\n

FULLRESYNC 响应中包含:

从库收到 FULLRESYNC 后,清空本地数据,准备接收 RDB。

第7步:传输 RDB + 积压命令

全量同步时

  1. 主库触发 BGSAVE(或复用正在进行的 BGSAVE)
  2. 生成 RDB 期间,新写入命令同时写入 repl_backlog 和从库的 client output buffer
  3. RDB 生成完毕后,发送给从库:
    • 磁盘模式:$<rdb_size>\r\n<rdb_data>
    • 无盘模式:$EOF:<40字节界定符>\r\n<rdb_stream><界定符>
  4. RDB 传输完毕后,发送积压的命令流

部分重同步时

主库直接从 repl_backlog 中读取 [slave_offset, master_offset] 范围内的命令,通过已建立的连接发送给从库。


16.2 replid 与 replid2 机制

PSYNC2(Redis 4.0+)引入了双 replid 机制,解决了故障转移后无法部分重同步的问题。

replid 的生成

每个 Redis 实例在成为主库时生成一个新的 replid:

/* server.c */
void createReplicationId(void) {
    getRandomHexChars(server.replid, CONFIG_RUN_ID_SIZE);
    server.replid[CONFIG_RUN_ID_SIZE] = '\0';
}

replid 是40字节的十六进制随机字符串,例如:

8371b4fb1155b71f4a04d3e1bc3e18c4a990aeeb

replid2 与 second_replid_offset

当从库被提升为主库时(故障转移或手动 SLAVEOF NO ONE),发生以下转换:

旧主库: replid=A, offset=10000
从库提升: replid2=A, second_replid_offset=10001, replid=B(新生成), offset=10000

这意味着:新主库记住了"我曾经是谁的从库,以及我在哪个位置开始独立"。

部分重同步的 replid 匹配逻辑

当从库发送 PSYNC <rid> <offset> 时,主库检查:

/* replication.c - masterTryPartialResynchronization() */
if (strcasecmp(rid, server.replid) &&
    (strcasecmp(rid, server.replid2) ||
     psync_offset > server.second_replid_offset))
{
    /* 无法部分重同步,触发全量 */
    goto need_full_resync;
}

条件成立(可以部分重同步):

  1. rid == replid:从库跟随的是当前主库(正常重连)
  2. rid == replid2offset <= second_replid_offset:从库跟随的是旧主库,且偏移量在切换点之前(故障转移后重连)

这是 PSYNC2 相对于 PSYNC1 的核心改进:故障转移后其他从库可以直接跟随新主,无需全量同步

查看当前 replid 状态

127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
master_replid:8371b4fb1155b71f4a04d3e1bc3e18c4a990aeeb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1234567
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:104857600
repl_backlog_first_byte_offset:134568
repl_backlog_histlen:1099999

16.3 全量同步 vs 部分重同步

全量同步触发条件

以下任一条件满足时触发全量同步(FULLRESYNC):

  1. 从库首次连接主库(offset = -1)
  2. 从库的 replid 与主库的 replid 和 replid2 均不匹配
  3. 从库的 offset 落在 repl_backlog 覆盖范围之外
  4. 主库的 repl_backlog 被禁用(repl-backlog-size 0

全量同步代价高昂:

部分重同步触发条件

同时满足以下所有条件时执行部分重同步(CONTINUE):

  1. slave_replid == master_replidslave_replid == master_replid2
  2. slave_offset 在 repl_backlog 覆盖范围内:
master_repl_offset - repl_backlog_size <= slave_offset <= master_repl_offset
  1. 主库的 repl_backlog 已初始化(至少有一个从库连接过)

部分重同步的数据量估算

# 从库断连时间(秒)
disconnect_time = 30

# 主库写入速率(bytes/s)
write_rate = 50 * 1024 * 1024  # 50 MB/s

# 断连期间积累的数据量
missed_data = disconnect_time * write_rate  # 1500 MB

# 若 repl_backlog_size = 100MB,则无法部分重同步
# 若 repl_backlog_size = 2000MB,则可以部分重同步

16.4 repl_backlog 环形缓冲区

数据结构

repl_backlog 是一个环形(circular)字节缓冲区,存储主库最近写入的命令序列:

/* server.h */
typedef struct {
    char *buf;              /* 环形缓冲区指针 */
    long long histlen;      /* 已存储的有效数据长度 */
    long long idx;          /* 下一个写入位置 */
    long long offset;       /* 对应 master_repl_offset 的起始偏移 */
    size_t size;            /* 缓冲区总大小(repl-backlog-size) */
} replicationBacklog;

写入逻辑:当 idx 到达末尾时,从头覆盖旧数据(环形)。

生产配置建议

# 默认值仅1MB,生产环境严重不足
repl-backlog-size 1mb    # 默认,不推荐

# 生产推荐:根据公式计算
repl-backlog-size 512mb  # 中等写入量(~10MB/s),可容忍约50秒断连
repl-backlog-size 1gb    # 高写入量,建议值

# 积压缓冲区释放延迟:最后一个从库断开后,多久释放 backlog
repl-backlog-ttl 3600    # 默认3600秒,建议保持或调大

大小选择公式

repl_backlog_size = 最大可容忍断连时间(s) × 写入速率(bytes/s) × 2

乘以2是为了留有余量(COW 期间写入量可能翻倍)。

监控 backlog 使用率

redis-cli info replication | grep -E 'repl_backlog|offset'
# repl_backlog_size: 536870912        # 512MB
# repl_backlog_first_byte_offset: 134568
# repl_backlog_histlen: 536870912     # histlen == size,说明 backlog 已满(环形覆盖中)
# master_repl_offset: 2147483647

histlen == size 时,backlog 处于满载状态,旧数据正被覆盖。此时若从库 offset 落后过多,部分重同步将失败。


16.5 无盘复制(diskless replication)

传统磁盘模式 vs 无盘模式

维度 磁盘模式 无盘模式
流程 BGSAVE → 写磁盘 → 发送文件 BGSAVE → 直接通过 socket 发送
磁盘 I/O 有(写 + 读)
适用场景 磁盘快、网络慢 磁盘慢(如 EBS)、网络快
多从库并发 复用同一 RDB 文件 每个从库需要独立 fork(或等待)

配置

# 启用无盘复制
repl-diskless-sync yes

# 等待时间(秒):等待更多从库连接后批量发送(减少 fork 次数)
repl-diskless-sync-delay 5

# 传输速率限制(0为不限制)
repl-diskless-sync-max-replicas 0

无盘模式下的多从库处理

时间线:
T=0: 从库A连接,主库等待 repl-diskless-sync-delay 秒
T=3: 从库B连接(在等待窗口内)
T=5: 等待结束,主库 fork 一次,同时向 A 和 B 发送 RDB 流
T=10: 从库C连接(错过窗口),等待下一次 BGSAVE

16.6 复制缓冲区(client output buffer)

与 repl_backlog 的区别

这是两个完全不同的缓冲区,经常被混淆:

缓冲区 repl_backlog client output buffer(replica)
归属 全局(主库) 每个从库连接独立
用途 支持部分重同步 主库向从库实时推送命令
满了之后 旧数据被覆盖(不影响复制) 从库连接被强制关闭
大小配置 repl-backlog-size client-output-buffer-limit replica

client output buffer 的三个限制

client-output-buffer-limit replica 256mb 64mb 60

格式:client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

恶性循环场景

主库大量写入(如 BGSAVE 期间 COW 导致 output buffer 积压)
→ 从库 client output buffer 溢出(>256MB)
→ 主库强制断开从库连接
→ 从库重连,发起 PSYNC,因 backlog 不足触发全量同步
→ 主库再次 BGSAVE(负载更高)
→ output buffer 再次溢出
→ 循环

解决方案

# 1. 调大 output buffer 上限
client-output-buffer-limit replica 512mb 128mb 120

# 2. 使用无盘复制减少 BGSAVE 期间的写放大
repl-diskless-sync yes

# 3. 增大 repl-backlog-size(断连后能部分重同步)
repl-backlog-size 1gb

# 4. 限制主库写入速率(极端情况)
# 使用 CONFIG SET hz 和 slowlog 监控

16.7 复制相关配置汇总

# ============ 主库配置 ============
# 积压缓冲区大小(核心参数)
repl-backlog-size 512mb

# 无盘复制
repl-diskless-sync yes
repl-diskless-sync-delay 5

# 从库 output buffer 限制
client-output-buffer-limit replica 256mb 64mb 60

# 至少 N 个从库复制延迟 < M 秒,否则拒绝写入(见第17章)
min-replicas-to-write 1
min-replicas-max-lag 10

# ============ 从库配置 ============
# 主库地址
replicaof 192.168.1.1 6379

# 主库密码
masterauth your_password

# 从库是否只读(强烈建议 yes)
replica-read-only yes

# 从库连接超时
repl-timeout 60

# 从库优先级(Sentinel 选主时使用,越小优先级越高)
replica-priority 100

# ============ 共同配置 ============
# TCP 保活
repl-backlog-ttl 3600
tcp-keepalive 300

16.8 生产排查:复制中断诊断流程

步骤1:确认从库状态

redis-cli -h slave_ip info replication
# master_link_status: down  ← 复制中断
# master_last_io_seconds_ago: 120  ← 多久没收到主库数据
# master_sync_in_progress: 1  ← 是否在全量同步中

步骤2:确认主库记录的从库状态

redis-cli -h master_ip info replication
# slave0:ip=192.168.1.2,port=6380,state=online,offset=1234567,lag=0
# slave1:ip=192.168.1.3,port=6381,state=sync,offset=0,lag=0
# ↑ state=sync 表示正在全量同步

步骤3:确认 backlog 是否足够

# 从库当前 offset
slave_offset=$(redis-cli -h slave_ip info replication | grep master_repl_offset | cut -d: -f2)

# 主库 backlog 起始位置
backlog_start=$(redis-cli -h master_ip info replication | grep repl_backlog_first_byte_offset | cut -d: -f2)

# 判断:slave_offset >= backlog_start → 可以部分重同步
echo "slave_offset: $slave_offset, backlog_start: $backlog_start"

步骤4:查看 slowlog 和 latency

redis-cli -h master_ip slowlog get 10
redis-cli -h master_ip latency history event

步骤5:确认 output buffer 使用情况

redis-cli -h master_ip client list | grep replica
# id=42 addr=192.168.1.2:43210 ... omem=67108864 ...
# omem 是 output buffer 已用内存(67MB,接近 soft limit 64MB)

16.9 Redis 7.x 复制改进

Redis 7.0 引入了 共享复制缓冲区(Shared Replication Backlog)

传统架构:
主库 → repl_backlog(全局)
主库 → client_output_buffer_slave1(per-slave)
主库 → client_output_buffer_slave2(per-slave)
问题:相同数据存了3份,内存浪费

Redis 7.0 新架构:
主库 → shared_replication_buf(全局共享)
slave1 和 slave2 都指向同一块共享内存中的不同位置
优点:内存使用从 O(N×size) 降为 O(size)

配置(7.0+):

# 共享复制缓冲区总大小限制
repl-backlog-size 512mb
# 注:7.0 中这个参数同时控制全局 backlog 和 per-slave 的共享缓冲区

本章小结

概念 关键参数/命令 生产建议
握手流程 PING/AUTH/REPLCONF/PSYNC 确保 masterauth 配置正确
replid2 INFO replication 故障转移后确认 replid 切换正常
全量同步 FULLRESYNC 监控频率,避免频繁触发
repl_backlog repl-backlog-size 生产至少 512MB,高写入量 1GB+
无盘复制 repl-diskless-sync 云环境强烈推荐开启
output buffer client-output-buffer-limit 根据从库数量和写入量调整

掌握 PSYNC2 协议细节是深入理解 Redis 高可用架构的前提。下一章将分析复制延迟、数据一致性与数据丢失的具体场景。

本章评分
4.5  / 5  (18 评分)

💬 留言讨论