Replica 设置
第一步
replicationSetMaster
1. 自身已经是 replica,该命令 change master,先断开与先前 master 的关系
2. 当前自身所有的 blocked client 全部 free
3. 当前节点如果有从节点,那么所有从节点全部断开
4. 之前处理中的一些主从“hand shake”任务,暂且取消,执行新的主从“hand shake”任务
5. 缓存 master 的处理
由于 Replica 与 Master 之间存在一个 hand shake 过程,在这个过程成功结束之前,主从关系并未真正确立,此时,replica 中的设置的 master 以 cached 状态存在,待完成主从关系的确立之后,由 cached 状态,转变为正式的状态
此时 Replica repl_state 置为 REPL_STATE_CONNECT 状态
第二步
hand shake
hand shake 过程,主要是在 replicationCron
轮询任务中完成的,状态机如图所示。
主要工作如下:
1. PING master
2. 确认是否需要验证密码,master-auth 配置是否正确
3. 发送port(replica 本节点端口)
4. 发送ip
(port 与 ip 都会检查配置,如图,默认不需要配置。port使用监听port,ip使用监听ip(ip REPLCONF 直接skip))
5. 发送 CAPA 信息
6. 发送 PSYNC 命令,此时完成一次数据的主从同步
7. 主从之间开启 ACK heartbeat
Master 端的 hand shake 处理
主要工作如下:
1. PING master <===> master 视为一般客户端处理
2. 确认是否需要验证密码,master-auth 配置是否正确 <===> master 视为一般客户端处理
3. 发送port(replica 本节点端口) <===> replconfCommand 处理,slave_listening_port
4. 发送ip <===> replconfCommand 处理,slave_ip
5. 发送 CAPA 信息 <===> replconfCommand 处理,slave_capa
6. 发送 PSYNC 命令,此时完成一次数据的主从同步 <===> syncCommand 处理,sync 中将该 client 添加入其 server.slave list 中存储
7. 主从之间开启 ACK heartbeat
<===> 在 replica 发送第一个 ACK 的时候,master 缓存该 client 作为一个 ONLINE 的 slave
master 对 PSYNC 命令的处理
当 replica 刚刚设置,在与 master hand shake 的过程中,最后一步便是 PSYNC,此时,master 中与该 replica 的连接(以 redis 中的 client 类型实例表示),该 client 中的 flag 中暂无 SLAVE 标记。
master 端 该 replica 连接接上来的 client 的 replstate 状态机跳转如下:
PSYNC 过程的时序图
PSYNC 命令从 replica 端到 master 端之后, master 端处理该命令的 command callback 是 syncCommand
该命令中:
1. 做一系列的检查,由于是刚刚连接上来的 replica 端,那么 master 端的缓存变量都是初始化的状态,所以检查最终走向的是全量复制的分支。
2. master 端此时初始化该 replica client 的replstate 状态为 bgsave wait start ,表示等待 bgsave 开始
3. 开始后台的 bgsave,核心操作与 rdb 的 bgsave 是一致的
4. 开启 bgsave 的后台任务之后, replica client 的 replstate 状态被置为 bgsave wait end 表示等待 bgsave 处理结束
5. PSYNC command 此时就会得到回复。
接下来的操作,便不在此命令的直接处理的 callback 中。
前文 rdb 的相关 blog 中提到过,redis 会在主进程的 serverCron 轮询任务中以非阻塞 wait 方式处理子进程的退出,第一是为了防止子进程编程僵尸进程,另外,还有一个重要的工作就是“善后”。
其中一个 rdb 子进程退出的重要的善后操作:
backgroundSaveDoneHandler 中 call backgroundSaveDoneHandlerDisk ,这个函数,也要说明一下:redis 自 5.0 开始了所谓的无磁盘持久化,采用 socket 的方式,将 rdb 文件以流的方式,发送给对应的备份 replica 。目前还是处于试验性质。
在这个函数的末尾, 直接调用 updateSlavesWaitingBgsave 函数。
这个函数里面干啥了?
updateSlavesWaitingBgsave
针对连接到 master 端的 replica client 的 replstate 为 wait bgsave end 状态的连接:
1. 只读方式打开 rdb 文件
2. 初始化一些发送的状态变量
3. 状态机更新: SLAVE_STATE_SEND_BULK
4. 注册该 replica client 中的 tcp client fd 到aeEventLoop (底层就是 epoll)中,注册的是 写事件,aeEventLoop callback sendBulkToSlave
说明: redis 采用的是 nonblock I/O + epoll LT 模式的异步模型,一般的命令的回复,比如 SET key value 命令,都比较短,比如就是 +OK\r\n 。这样的命令回复,一般都是采用类似 epoll 的回射模型进行处理(不完全是回射,内部有一些异步方式处理,能写就写,不能写就pending,挂载write callback,可写时再写,写完摘除写事件),在 redis 中就是在进入下一次的 epoll_wait 中有个 beforeSleep 的callback,在这个 callback 中,进行“回射”。前文 blog 中,epoll 相关问题简单解答中,LT 模式下,写任务,一般都是回射方式处理,而不采用 epoll 监听,此时为什么要注册呢?
原因其实很简单,就是此时的场景与一般写任务的场景不同,LT 模式下,当少量数据回复的时候,采用回射模型,第一 LT 模式规避了 epoll 中 ET 模式下的 epoll_wait hang 问题,第二读多写少的场景,基本都是可写状态,“写总是成功”,所以,无需监听没有必要的事件去浪费epoll资源。但是,此时的场景是发送 rdb 文件,可能会很大,极容易将 tcp 的发送缓冲区写满,此时,要么,采用同步阻塞写,要么非阻塞方式监听写事件,等到下一次可写事件到来,继续写。写完,将 fd 从 epoll 上面摘下来即可。
5. sendBulkToSlave: 读一个本地文件中的内容,通过 socket 发送给客户端,同时记录已经发送的数据量。发完了以后,从 epoll 中把该 fd 的写事件监听去除,调用 putSlaveOnline
6. putSlaveOnline 中:
更新状态机:SLAVE_STATE_ONLINE
此时还会挂载一个写事件,核心是 writeToClient,但是,最终如果有回复内容,那么就回复客户端,如果没有,依旧会将该写事件摘除。
为什么要监听写事件?因为回射模型,总得有请求,才有机会回射,对于全量复制的场景,请求早就回复了,PSYNC 的回复,如果还有内容,要往客户端发送,注册了callback,能够尽快回复,而不是等到下次 epoll_wait 之前的 before sleep中去写。回复内容写成功,那么该事件就被及时的摘除了。
SLAVE_STATE_ONLINE 状态
已经成功连接的 replica 的状态
遗留问题
在 bgsave 做全量 sync 期间,如果有新的 key 值 set 进 master 会怎么样?
在 write rdb to replica 的过程中,直到发送完 rdb 文件之前,如果有新的 key 值 set 进 master 会怎么样?
在状态机跳入 wait bgsave start 时,如果是第一个 replica ,那么此时,同时创建 backlog 缓冲区。
但是这个 backlog 缓冲区,在第一个 replica 过来,触发 bgsave 过程中,即便存储了数据,也不会发送给该 replica,bgsave 过程中的命令或者在 write rdb to replica 的过程中的命令,缓存到 backlog 中的同时,也会缓存到 master 端该 replica 的 client reply 的缓存中。该 backlog 的主要是用于 partial sync 功能。
以上过程中的新的 key 值,怎么被处理的呢?
过程与 AOF 的“投喂过程”类似。都会被“投喂”到对应的 client 中 reply 的缓存中。
如前文所述,第一次连接,全量sync,过程中的新 key 值,缓存在 client 中的 reply 缓存中,那 backlog 有什么用呢?
主要是用于“断线重连”,当 replica 与 master 的 tcp 连接出现问题,在重连之后,在这种丢失连接的期间,backlog的作用就会显现,这些backlog中的内容就会同步给replica,用以恢复replica连接丢失导致丢失的数据。
数据真的不会丢吗?
假设,断线时间过长,而master此时有大量新增数据,那 backlog 会是什么样子的呢?
工程实现上,可以让 backlog 动态扩容,也可以静态的方式,循环利用。
Redis 中,采用的是后者,因为前者存在潜在的风险:
第一,并没有合适的时机去清空 backlog的数据,毕竟,backlog中的数据,是在 replica 与 master 的 tcp 连接出现问题,才会启用,时机太不固定了。
第二,动态扩容,由于清空的时机不太稳定,那么扩容会带来内存溢出的风险。以为 tcp 的连接一直保持完好,那么就一直有新增数据添加入 backlog,却一直不会被消费。
所以 Redis 采用了固定大小的 backlog 缓冲区循环利用。以上的内存相关的风险被规避了,但是如果tcp长时断连,新增数据过多,那么就会存在数据丢失的风险。
这种情况下的数据丢失风险,redis中也是有考虑的,就是 partial sync 会失败,而触发全量 sync。
既有rdb文件要主从同步,又有新增数据 pending 在 client 的 reply 中,那么,会不会先把 client 的 reply 中缓存的新数据先发送给客户端呢?
不会。
第一:前文已述,先注册的发送 rdb 文件的写 callback
第二:写完之后,才会跳转 ONLINE 状态,在这种状态下,重新注册发送 reply 的写事件
第三:在 ONLINE 状态,replica client 才会被添加入 pending。后续才会走正常的如同其他普通 client 的 reply的写事件处理流程
也就是说,wait bgsave start 之后,除非过程中出错,否则,在 rdb 文件被发送完毕之前,master 端是不会写其他任何内容到 replica 端。
其他的内容都是等到这个 rdb 文件发送完了之后再处理。
replica 端
在 hand shake 的最后,发送 PSYNC 命令,进行第一次的全量同步请求。命令发送完毕之后,马上监听 EPOLLIN 事件,注册 callback readSyncBulkPayload 去读取数据。注意,该callback中,直接会将当前replica redis node 的数据库 清空。
当 rdb 的数据全部被同步过来之后,
replica 端就会将该 callback 从 自己的 epoll 中摘除。
replica 端状态机被最终置为 REPL_STATE_CONNECTED 状态。
replica 端将该 tcp 连接 封装进 client 中,作为自身的 server.master 存储,同时挂载 readQueryFromClient 读事件进 epoll 中。
后续的 redis master 端 的数据,以 一个普通 客户端 命令的格式,发送到 replica 端,进行逐一的命令同步。