第三部分 多机数据库的实现
15.0 第15章 复制
- 用户可以通过执行slaveof选项,让一个服务器去复制另一个服务器,被复制的服务器为主服务器,对主服务器进行复制的服务器称之为从服务器。主从服务器的数据库保存相同的数据,概念上称“数据库状态一致”
- 服务器127.0.0.1:12345成为127.0.0.1:6379的从服务器,之后在6379主服务器设置键值,在12345服务器上也可以获取该键值。如果在主服务器上删除了键值,主从服务器最后也会删除键值
// for example
127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
ok
- 章节内容
- redis在2.8版本前使用的旧版复制功能的实现原理
- redis在2.8版本开始使用的新版复制功能(部分重同步来解决旧版复制功能的低效)
- SLAVEOF命令的具体实现步骤
15.1 旧版复制功能
- redis复制功能分为同步和命令传播
15.1.1 同步
- 当客户端向从服务器发送SLAVEOF命令,首先需要将从服务器的数据库状态更新到主服务器的数据库状态
- 主要步骤:
- 从服务器向主服务器发送SYNC命令
- 收到命令的主服务器执行BGSAVE命令,在后台生成RDB文件,并使用一个缓冲区开始记录从现在开始的所有写命令
- 主服务器的BGSAVE执行完毕,主服务器会将该文件发送给从服务器,从服务器载入该文件,并将自己的数据库状态更新成主服务器执行BGSAVE命令时的数据库状态
- 主服务器将记录在缓冲区的写命令发送给从服务器,从服务器执行这些写命令,然后将自己的数据库状态更新
15.1.2 命令传播
- 同步操作执行完之后,只要主服务执行了修改数据的操作,比如主服务器删除了数据,那么主从数据就不一致了
- 为了实现一致,主服务器需要对从服务器执行命令传播,从服务器收到删除命令执行后,主从数据就一致了
15.2 旧版复制功能存在的问题
- 复制的两种情况
- 初次复制:从服务器以前没有复制过任何主服务器,或者此时从服务器需要复制的主服务器和上一次复制的不同
- 断线后重复制:在命令传播阶段主从服务器因为网络原因中断了复制,但从服务器通过自动重连接上了主服务器,并继续复制主服务器
- 主从服务器断开后,主服务器执行了少量的写操作。重新连接之后,主服务器收到从服务器的SYNC命令,主服务器执行BGSAVE命令,继续使用缓冲区记录写命令,等BGSAVE执行完之后,向从服务器发送RDB文件。从服务器载入RDB文件+缓冲区命令,主从完成同步。【其实只需要同步断开连接过程中命令】
- SYNC命令耗费资源
- 主服务器执行BGSAVE保存RDB文件
- 主服务器要将RDB打给从服务器
- 从服务器要载入RDB,在载入期间,从服务器会因为阻塞没办法处理命令请求
15.3 新版复制功能
- 新增命令PSYNC,两种模式:完整重同步和部分重同步
- 完整重同步:初次复制,主服务器创建并发送RDB文件,向从服务器发送保存在缓冲区里面的写命令进行同步
- 部分重同步:断线后重复制。条件满足情况下,主服务器将主从服务器断开连接期间执行的写命令发送给从服务器,从服务器接受并执行了这些写命令,可以讲数据库更新到主服务器的状态
15.4 部分重同步的实现
- 三个组成部分
- 主服务器的复制偏移量,从服务器的复制偏移量
- 主服务器的复制积压缓冲区
- 服务器的运行ID
15.4.1 复制偏移量
- 主从服务器会分别维护一个复制偏移量
- 主服务器每次向从服务器传播N字节的数据,就在自己的复制偏移量上加上N
- 从服务器每次收到主服务器传播来的N字节的数据,也在自己的复制偏移量上加上N
- 通过对比复制偏移量,可以清楚地知道主从服务器是否处于一致状态
- 如果断开重连主从复制偏移量不相同,那通过复制加压缓冲区来同步,断线之后从数据库丢失的那部分数据
15.4.2 复制积压缓冲区
- 复制积压缓冲区:主服务器维护的一个固定长度先进先出的队列,默认1MB
- 当主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会把写命令(每个字节记录对应的复制偏移量)入队到复制积压缓冲区里面
- PSYNC具体怎么实现
+ offset偏移量之后的数据在复制积压缓冲区里面,主服务器对从服务器执行部分重同步操作【主服务器会告诉从服务器,通过发送CONTINUE回复】
+ offset偏移量之后的数据已经不存在复制积压缓冲区,主服务器对从服务器执行完整重同步操作
+ 复制积压缓冲区的大小:2*second*write_size_per_second
+ second:服务器断线之后重新连接上主服务器的平均时间
+ write_size_per_second:服务器平均每秒产生的写命令数据量
15.4.3 服务器运行ID
- 每个服务器都会有自己的运行ID,在服务器启动时自动生成
- 作用
- 从服务器对主服务器进行第一次复制的时候,主服务器会将自己的ID发给从服务器,从服务器会将ID保存起来
- 从服务器断线重连接,会再跟主服务器发送之前保存的ID。
- 如果从服务器发过来的ID和主服务器本身的ID一致,说明之前连接的就是这个主服务器,可以继续尝试部分重同步操作(复制积压缓冲区,offset)
- 如果从服务器发过来的ID和主服务器本身的ID不一致,说明之前连接的不是这个主服务器,必须进行完整重同步
15.5 PSYNC命令的实现
- 两种调用方法
- 如果从服务器没有复制过主服务器,第一次复制向主服务器发送PSYNC ? -1
- 如果从服务器已经复制过主服务器,第一次复制向主服务器发送PSYNC
- 主服务器会回复三种
+ FULLRESYNC :完整重同步,runid是主服务器的运行id,offset是主服务器当前的复制偏移量
+ CONTINUE:主服务器将与从服务器执行部分重同步操作,从服务器等着主服务器将缺少的部分数据发过来
+ ERR:主服务器版本低于2.8,不能识别PSYNC,这时候从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作
15.6 复制的实现
15.6.1 设置主服务器的地址和端口
- SLAVEOF是一个异步命令,完成masterhost和masterport的设置属性之后,从服务器向发送SLAVEOF命令的客户端返回OK,表示复制指令已经被接收。实际的复制工作在OK返回之后真正执行
//code0 server.h
struct redisServer {
/* Replication (slave) */
char *masteruser; /* AUTH with this user and masterauth with master */
char *masterauth; /* AUTH with this password with master */
char *masterhost; /* Hostname of master */
int masterport; /* Port of master */
}
15.6.2 建立套接字连接
- 从服务器创建的套接字能成功连接到主服务器,从服务器会为这个套接字关联一个专门用于执行后续复制工作的文件事件处理器(接收RDB文件、接收主服务器传播来的写命令)
- 主服务器accept从服务器套接字,为套接字创建相应的客户端状态,将从服务器认为是一个连接到主服务器的客户端来对待,从服务器有两个身份(从服务器向主服务器发送命令请求,主服务器向从服务器返回命令回复)
- 从服务器server
- 客户端client(从服务器是主服务器的客户端)
15.6.3 发送PING命令
- 从服务器成为主服务器的客户端,首先会向主服务器发送PING命令
- 检查主从服务器的通信,发送PING命令可以检查套接字的读写状态
- 发送PING,检查主服务器是否能正常处理命令请求
- 从服务器发送PING之后,可能会有三种情况
-
主服务器向从服务器返回命令回复,但从服务器不能规定时间内读出内容,表明:主从服务器之间的网络连接状态不佳,不能进行后续的复制工作。此时,从服务器断开并重新创建向主服务器连接的套接字。
-
主服务器向从服务器返回一个错误,表示主服务器(正在处理一个超时运行的脚本)暂时没法处理从服务器的命令请求,不能执行后续的操作。此时,从服务器断开并重新创建向主服务器连接的套接字。
-
从服务器读取到PONG,表示主从服务器连接正常,且主服务器可以正常处理从服务器发送的命令请求,继续执行之后的步骤
-
15.6.4 身份验证
- 从服务器收到主服务器PONG回复之后,下一步决定是否进行身份验证
- 从服务器设置masterauth选项,进行身份验证
- 从服务器向主服务器发送一条AUTH命令,参数是从服务器masterauth的值
- 从服务器没有设置masterauth,不进行身份验证
- 从服务器设置masterauth选项,进行身份验证
- 从服务器在身份验证过程中的四种结果
- 主服务器没有设置requirepass选项,从服务器没有设置masterauth,主服务器继续执行从服务器的命令,开始复制
- 从服务器通过AUTH命令发送的密码和主服务器requirepass选项所设置的密码相同,主服务器继续执行从服务器的命令,开始复制。如果主从设置的不相同,主将返回invalid password
- 主服务器设置了requirepass选项,从服务器没有设置masterauth,主服务器将返回一个NOAUTH错误
- 主服务器没有设置requirepass选项,从服务器设置masterauth,主服务器将返回一个no password is set错误
- 身份验证发生错误怎么办?
- 从服务器会中止目前的复制工作,并从创建套接字开始重新执行复制,直到身份验证通过;或者从服务器放弃复制
15.6.5 发送端口信息
- 从服务器执行命令REPLCONF listening-port ,向主服务器发送从服务器的监听端口号
- 主服务器收到端口,会将端口记录在客户端状态的slave_listening_port属性中
- 端口信息在主服务器执行INFO replication命令时打印出从服务器的端口号
//code1 server.h
typedef struct client {
//...
int slave_listening_port; /* As configured with: SLAVECONF listening-port */
char slave_ip[NET_IP_STR_LEN]; /* Optionally given by REPLCONF ip-address */
int slave_capa; /* Slave capabilities: SLAVE_CAPA_* bitwise OR. */
multiState mstate; /* MULTI/EXEC state */
//...
} client;
15.6.6 同步
- 同步之前,从服务器是主服务器的客户端;执行同步之后,主服务器也会成为从服务器的客户端
- PSYNC执行的完整重同步,主服务器成为从服务器的客户端,能将保存在缓冲区的写命令发给从服务器
- PSYNC执行部分重同步,主服务器成为从服务器的客户端,能向从服务器发送保存在复制积压缓冲区里面的写命令
- 互为客户端的作用:互相发送命令请求和回复
15.6.7 命令传播
- 主服务器将写命令发送给从服务器,从服务器执行主服务器发来的写命令,保证数据的一致
15.7 心跳检测
- 命令传播过程中,从服务器会默认以每秒一次的频率,向主服务器发送命令:REPLCONF ACK <replication_offset>
- 检测主从服务器的网络连接状态
- 辅助实现min-slaves
- 检测命令丢失
15.7.1 检测主从服务器的网络连接状态
- 主服务器超过一秒没有收到从服务器发来的REPLCONF ACK,主服务器了解到主从服务器之间出现问题
- 向主服务器发送INFO replication,lag那一栏,我们可以看到从服务器最后一次向主服务器发送REPLCONF ACK距离现在多少秒(lag=2,表示2秒前发过,理论上lag在0或1跳动)
15.7.2 辅助实现min-slaves
- 防止主服务器在不安全的情况下执行写命令
- min-slaves-to-write 3
- min-slaves-max-lag 10
- 从服务器少于3个,或者3个从服务器的lag值大于等于10,主服务器拒绝执行写命令
15.7.3 检测命令丢失
- 网络故障,主服务器发给从服务器的命令丢失。从服务器发送REPLCONF ACK ,主服务器发现从服务器当前的偏移量少于自己的复制偏移量,之后主服务器在复制积压缓冲区里面找到从服务器缺少的数据,然后把数据重新发送给从服务器