redis设计与实现之复制篇

当客户端向从服务器发送 SLAVEOF 命令, 要求从服务器复制主服务器时, 从服务器首先需要执行同步操作, 也即是, 将从服务器的数据库状态更新至主服务器当前所处的数据库状态(以2.8版本为分水岭进行对新旧复制功能的讲解)

1
2
#以下命令就能得知12345为从服务器,6379为主服务器
127.0.0.1:12345> slaveof 127.0.0.1 6379

旧版复制功能的实现

Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作

  • 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态
  • 命令传播操作则用于在主服务器的数据库状态被修改, 导致主从服务器的数据库状态出现不一致时, 让主从服务器的数据库重新回到一致状态
同步

如下图展示了同步的过程


大体流程为

  • 从服务器向主服务器发送 SYNC 命令
  • 收到 SYNC 命令的主服务器执行 BGSAVE 命令, 在后台生成一个 RDB 文件, 并使用一个缓冲区记录从现在开始执行的所有写命令
  • 当主服务器的 BGSAVE 命令执行完毕时, 主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器, 从服务器接收并载入这个 RDB 文件, 将自己的数据库状态更新至主服务器执行 BGSAVE 命令时的数据库状态
  • 主服务器将记录在缓冲区里面的所有写命令发送给从服务器, 从服务器执行这些写命令, 将自己的数据库状态更新至主服务器数据库当前所处的状态
命令传播

主从处于一直状态下的结构图


这是如果客户端向主服务器发送命令 DEL k3,如下图造成不一致状态

这时候需要经过命令传播把命令发送给从服务,从而达到一致状态

旧版复制功能的缺陷

从上面的流程可以看出,Slave从库在连接Master主库时,Master会进行内存快照,然后把整个快照文件发给Slave,也就是没有象MySQL那样有复制位置的概念,即无增量复制,这会给整个集群搭建带来非常多的问题。

比如一台线上正在运行的Master主库配置了一台从库进行简单读写分离,这时Slave由于网络或者其它原因与Master断开了连接,那么当 Slave进行重新连接时,需要重新获取整个Master的内存快照,Slave所有数据跟着全部清除,然后重新建立整个内存表,一方面Slave恢复的 时间会非常慢,另一方面也会给主库带来压力

sync命令是一个非常耗费资源的操作,每次执行sync命令主服务器都会执行以下动作

  • 主服务器只需要执行bgsave来生成rdb文件,这个生成操作会耗费主服务器大量的cpu、内存和磁盘IO资源
  • 主需要将rdb文件发送给从,发送操作会耗费主从服务器大量的网络资源(带宽和流量),并对主服务器相应命令请求的时间产生影响
  • 接收到rdb文件的从需要载入主服务器发来的rdb文件,并且在载入期间会因为阻塞而没办法处理命令请求

新版复制功能的实现

从2.8版本开始,用psync命令(完整重同步和部分重同步两种模式)代替sync命令来执行复制时的同步操作
完整重同步没啥好说的,和sync差不过
部分重同步结构图如下

部分重同步的实现

部分重同步功能由以下三部分构成

  • 主服务器的复制偏移量和从服务器的复制偏移量
  • 主服务器的复制积压缓冲区
  • 服务器运行ID
复制偏移量

主从都会维护一个偏移量

  • master每次向slave传播N字节,会将其偏移量+N
  • slave每次接受master的N字节,会将其偏移量+N

如下图(通过主从的复制偏移量很容易得知主从是否处于一致状态)


考虑下图的一种情况


如果从服务器A断线重连,会执行哪个完整还是部分?如果执行部分,主是如何补偿从A这部分断线期间丢失的数据?这个问题的答案和复制积压缓冲区有关

复制积压缓冲区

复制积压缓冲区是由master维护的一个固定长度先进先出队列(和经常说的队列一样),默认大小位1M,当master进行命令传播时,他不仅会将写命令发给所有slave,还会将写命令入队到复制积压缓冲区里面,如下图所示


复制积压缓冲区结构图如下(会带有偏移量)

当slave重连master时,slave会通过psync经offset发给master,master会根据offset来决定对slave执行何种同步操作

  • offset之后的数据仍然存在于积压缓冲区里面,master会执行部分重同步操作
  • offset之后的数据不存在于积压缓冲区里面,master会执行完成重同步操作

复制积压缓冲区的最小大小 = second(断线重连所需要的时间) write_size_per_second(每秒产生的写命令数据量)
保险起见可以
2,可以保证绝大部分断线情况都能用部分重同步来处理,缓冲区大小的设置可查看repl_backlog_size来配置

服务器运行ID

redis都会有一个自己运行ID(由40个随机的十六进制字符组成),初次复制,master会将自己运行ID传送给slave,slave会将这个ID保存起来,断线重连时会将此ID传送给master,若master ID = slave ID 则执行部分重同步,否则则执行完整重同步

psync命令的实现

psync命令的调用方法有两种

  • slave从未复制过master或者之前执行过slaveof no one命令,那么slave会向master发送psync ? -1 进行完整重同步
  • 相反,会执行psync runid offset (runid是上一次复制的master的运行ID,offset则是slave当前的复制偏移量)

psync中master向slave返回以下三种回复

  • 若master返回 + fullresync runid offset,表示执行了完整重同步,runid是master运行ID,slave会把这个id保存起来,offset为master的偏移量,slave也会保存起来作为自己的偏移量
  • 若master返回 + continue,表示执行了部分重同步,从只需要等着master将自己缺少的那部分数据发送过来即可
  • 若master返回 -err,则表示redis版本低于2.8,识别不了psync命令

如下图所示

复制的实现

设置主服务器的地址和端口
1
2
3
4
5
6
7
8
9
10
# 执行命令
127.0.0.1:12345> slaveof 127.0.0.1 6379
# slave服务器需要将127.0.0.1保存到masterhost中,6379保存到masterport中
struct redisServer {
# 主服务器的地址
char *masterhost;
# 主服务器的端口
char *masterport;
}
简历套接字连接

如下图所示

发送PING命令

如下图所示


ping命令的作用

  • 检查套接字读写是否正常
  • 检查master能够正常处理命令请求

ping命令可能会遇到的问题如下图所示

身份验证

slave收到master的pong之后就会验证身份

  • 如果slave设置了masterauth,那么进行身份验证(slave会向master发送 auth masterauth)
  • 如果slave未设置masterauth,那么不进行身份验证

如下图所示

发送端口信息

身份验证完事儿之后,slave会执行replconf listening-port port-number,向master发送slave的监听端口号(case:12345)
master接收到这个port后,会将port记录在slave服务器所对应的客户端状态的slave_listening_port属性中

1
2
3
typedef struct redisClient {
int slave_listening_port;
}

同步

slave将向master发送psync命令,如下图所示

命令传播

完成同步之后,master进入命令传播阶段,会一直将写命令发送给slave即可

心跳检测

命令传播阶段,slave默认以1s的频率,向master发送命令
replconf ack replication_offset(slave当前的复制偏移量)
发送replconf ack的三个作用

  • 检查master-slave网络连接状态
  • 辅助实现min-slaves选项
  • 检测命令丢失
检测master的网络状态

如果1s还未返回任何信息,就知道连接出现问题了,在master上执行info replication命令,可以查看明细

检测命令丢失

如下图所示,通过master的偏移量和slave的偏移量得知那部分命令丢失了,并且master会主动set key value至slave,redis2.8版本之前的即使命令丢失也不知道