Redis Source Code Read Log( 16 Redis 主从同步))

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使用监听portip使用监听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 过程中,即便存储了数据,也不会发送给该 replicabgsave 过程中的命令或者在 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 端,进行逐一的命令同步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值