在Redis中,可以用 slaveof 命令,或者在配置中设置 slaveof 选项,让一个服务器去复制另一个服务器。去复制的服务器称为从服务器(slave),被复制的称为主服务器(master)。
进行复制中的主从服务器双方的数据库将保存相同的数据,称为“数据库状态一致”。主服务器的增删改,在从服务器中都会一并有改动。
目录
旧版复制功能的实现
Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作:
- 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
- 命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。
同步
当客户端向从服务器发送 SLAVEOF 命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,即将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
从服务器对主服务器的同步操作需要通过向主服务器发送 SYNC 命令来完成, 以下是 SYNC 命令的执行步骤:
- 从服务器向主服务器发送 SYNC 命令。
- 收到 SYNC 命令的主服务器执行 BGSAVE 命令, 在后台生成一个 RDB 文件, 并使用一个缓冲区记录从现在开始执行的所有写命令。(RDB+缓冲命令)
- 当主服务器的 BGSAVE 命令执行完毕时, 主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器, 从服务器接收并载入这个 RDB 文件, 将自己的数据库状态更新至主服务器执行 BGSAVE 命令时的数据库状态。
- 主服务器将记录在缓冲区里面的所有写命令发送给从服务器, 从服务器执行这些写命令, 将自己的数据库状态更新至主服务器数据库当前所处的状态。
时间 | 主服务器 | 从服务器 |
---|---|---|
T0 | 服务器启动。 | 服务器启动。 |
T1 | 执行 SET k1 v1 。 | |
T2 | 执行 SET k2 v2 。 | |
T3 | 执行 SET k3 v3 。 | |
T4 | 向主服务器发送 SYNC 命令。 | |
T5 | 接收到从服务器发来的 SYNC 命令, 执行 BGSAVE 命令, 创建包含键 k1 、 k2 、 k3 的 RDB 文件, 并使用缓冲区记录接下来执行的所有写命令。 | |
T6 | 执行 SET k4 v4 , 并将这个命令记录到缓冲区里面。 | |
T7 | 执行 SET k5 v5 , 并将这个命令记录到缓冲区里面。 | |
T8 | BGSAVE 命令执行完毕, 向从服务器发送 RDB 文件。 | |
T9 | 接收并载入主服务器发来的 RDB 文件 , 获得 k1 、 k2 、 k3 三个键。 | |
T10 | 向从服务器发送缓冲区中保存的写命令 SET k4 v4 和 SET k5v5 。 | |
T11 | 接收并执行主服务器发来的两个 SET 命令, 得到 k4 和 k5 两个键。 | |
T12 | 同步完成, 现在主从服务器两者的数据库都包含了键 k1 、 k2 、 k3 、 k4 和 k5 。 | 同步完成, 现在主从服务器两者的数据库都包含了键 k1 、 k2 、 k3 、 k4 和 k5 。 |
命令传播
在同步操作执行完毕之后, 主从服务器两者的数据库将达到一致状态, 但这种一致并不是一成不变的 —— 每当主服务器执行客户端发送的写命令时, 主服务器的数据库就有可能会被修改, 并导致主从服务器状态不再一致。
所以为了让主从服务器再次回到一致状态, 主服务器需要对从服务器执行命令传播操作: 主服务器会将自己执行的写命令 —— 也即是造成主从服务器不一致的那条写命令 —— 发送给从服务器执行, 当从服务器执行了相同的写命令之后, 主从服务器将再次回到一致状态。
旧版复制功能的缺陷
从服务器对主服务器的复制可以分为两种情况:
- 初次复制:从服务器以前没有复制过任何服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不一样。
- 断线重连:处于命令传播阶段的从服务器因为网络原因中断了复制,但从服务器通过自动重连重新连接上了主服务器,并继续复制主服务器。
虽然对于初次复制来说,旧版复制功能能很好的完成任务,但对于断线重连,还需要主服务器bgsave并且自身导入rdb文件,效率比较低。因为主从服务器数据不一致的地方只有从服务器断线重连的这段时间的命令,而不是全部的命令。
sync操作非常耗费资源:
- 主服务器bgsave命令会耗费主服务器大量I/O、内存、CPU。
- 主服务器发送rdb给从服务器,会占用大量网络资源。
- 从服务器接收rdb到完全载入完毕之前,无法对外提供服务。
主从复制
主从复制的实现
同步
为了解决断线重连的复制效率问题,从2.8版本的redis开始,采用psync命令代替sync命令进行主从复制,该命令具有完整重同步和部分重同步两种模式。
完整重同步:完整重同步仅用于初次复制,步骤和sync一致。
部分重同步:部分重同步用于断线重连的情况,主服务器会在从服务器重连后,将断线期间的命令都发送给从服务器,从服务器只需要接收并执行这些命令即可实现同步。部分重同步有三部分构成:
- 主服务器的复制偏移量和从服务器的复制偏移量。
- 主服务器的复制积压缓冲区。
- 服务器的运行ID。
复制偏移量
执行复制的双方-------主从服务器会分别维护一个复制偏移量,主服务器每次向从服务器传播N字节的数据,偏移量就加N,同样从服务器接收N字节也加N。
通过对比这个偏移量,就可以判断是否一致,如果主从服务器处于一致状态,主从服务器的偏移量总是一样的;如果主从服务器的偏移量不相同,主从服务器肯定处于不一致的状态。
复制积压缓冲区
复制积压缓冲区是由主服务器维护的,固定长度的先进先出的队列,默认大小是1MB。当主从复制偏移量不一致,则判断出主从不一致,此时则需要用到复制积压缓冲区,来实现主从同步。
固定长度的运作方式和可变长度的区别是,当新push一个元素时,如果长度超过固定长度,则队列的第一个元素会被弹出。
当主服务器进行命令传播时,除了将命令发送给从服务器,还会将命令写入复制积压缓冲区内。
主服务器的复制积压缓冲区内会保存最近一部分的写命令,并且缓冲区会为队列中的每个字节记录相应的复制偏移量。
当从服务器断线重连时,如果偏移量还在这个缓冲区内,则会使用部分重同步;如果偏移量已经不再这个缓冲区,说明从服务器断线太久,则会采用完全重同步。
复制积压缓冲区大小设置
如果主服务器要大量写操作,或者主从重连速度慢,则1MB不太适合。如果每次断线重连后都要完全重同步,则该缓冲区就没有意义了。大小可以用公式进行估算:second * write_size_per_second。
其中,second 是从服务器断线后重连上的平均时间,write_size_per_second 是主服务器平均每秒的写数据量。
具体大小,可以设置为上述估算值的两倍。修改大小值是通过修改配置文件中的属性进行的,属性名称repl-backlog-size。
服务器运行ID
实现部分重同步还需要用到服务器运行ID。每个redis服务器,无论主从,都有自己的运行ID,ID是在服务器启动的时候随机生成,由40个随机的16进制字符组成。
从服务器会保存主服务器的运行ID,当断线重连时,会发送之前的主服务器运行ID。如果这个ID和当前的主服务器一致,则可以部分重同步;如果不一致,说明之前就不是连的这个主服务器,则要完全重同步。
PSYNC命令执行过程
psync命令调用方法有两种:
- 从服务器以前如果没有复制过任何主服务器,或者执行过slaveof no one命令,则第一次复制会发送psync ? -1命令,表示强制要求主服务器进行完全重同步。
- 如果从服务器以前复制过主服务器,则会发送psync <runid> <offset>,将上一次复制的主服务器的运行ID和从服务器当前自身复制偏移量发送给主服务器,由主服务器判断是要部分重同步还是完全重同步。
主服务器的回复有三种:
- +fullresync<runid> <offset>:则表示要完全重同步。从服务器会记录这个主服务器的runid,作为下次断线重连发送的服务器运行ID;将offset作为从服务器当前的偏移量。
- +continue:表示部分重同步,从服务器只需要等待主服务器将缺少的部分发送过来,再进行同步即可。
- -err:表示主服务器版本低于2.8,不支持psync,则从服务器会再发送sync命令,进行完整同步。
复制原理
复制采用的命令在从服务器上发送 slaveof <masterip> <masterport>(主服务器ip地址和端口号),详细实现步骤如下:
1、设置主服务器的ip和端口
执行上述命令后,从服务器会将主服务器的 ip 和端口设置在自身redisServer结构体中的属性中,ip 设置到char数组类型的masterhost 属性,port 设置到int类型的 masterport 属性。
设置完成后,从服务器会向客户端返回OK。
2、建立套接字连接
执行slaveof命令后,从服务器将根据ip和端口号,与主服务器建立套接字连接。
从服务器成功连接主服务器后,从服务器将为该连接专门创建一个用于复制工作的文件事件处理器,这个处理器接收rdb文件、处理主服务器的命令传播等。
主服务器接收到从服务器的套接字连接后,为套接字创建相应的状态,并将从服务器看作一个连接到主服务器的客户端,此时从服务器同时具有客户端和服务器两个身份,从服务器可以向主服务器发送命令请求,主服务器会向从服务器发送命令回复。
3、发送PING命令
从服务器连接上主服务器后,会先发送一个PING命令,具有两个作用:
- 检查主从服务器的套接字读写状态是否正常。
- 检查主服务器当前可以正常处理请求。
从服务器会收到主服务器的三种回复的一种:
- 主服务器向从服务器回复内容,从服务器不能在规定时限内(timeout)读取命令内容,则认为主从服务器当前网络不佳,无法处理后续的同步工作。从服务器会断开套接字,再自动重连并重新创建向主服务器的套接字。
- 主服务器向从服务器回复一个错误,表示主服务器当前无法处理从服务器的请求,从服务器会断开并重新创建向主服务器的套接字。
- 从服务器接收到主服务器的PONG回复,表示主从服务器网络正常,主服务器当前可以处理从服务器的命令,则从服务器会进入后续的步骤。
4、身份验证
从服务器收到PONG后,将进入身份验证阶段。如果其有设置masterauth选项,则开始身份验证,否则不验证。如果需要验证,则从服务器接下来会给主服务器发送auth命令,并带上masterauth选项的值作为参数。
身份验证阶段会有以下几个结果:
- 主服务器没有设置requirepass选项,从服务器也没有设置masterauth,则跳过身份验证。
- 主服务器没有设置requirepass,而从服务器设置masterauth,会报noauth错误。
- 主服务器设置requirepass,而从服务器没有设置masterauth,会报nopassword is set错误。
所有错误都会终止当前的复制工作,并从创建套接字开始重新执行复制,直到验证通过。
5、发送端口信息
身份验证通过后,从服务器执行 replconf listening-port <port-number> 命令,向主服务器发送从服务器的监听端口号。
主服务器接收到端口号后,会将其记录在对应客户端redisClient结构体中的属性中,类型是int,名称是slave-listening-port。该属性目前唯一作用是主服务器执行info replication命令时,打印出从服务器的端口号。
6、同步
完成上述步骤后,从服务器会发送psync给主服务器,执行同步操作。
在同步操作之前,只有从服务器是主服务器的客户端,而同步之后,主服务器也是从服务器的客户端,即互为客户端。
这样主服务器才能将保存在缓冲区的写命令(完整重同步)、保存在复制积压缓冲区(部分重同步)中的写命令发给从服务器。
7、命令传播
完成同步后,后续只要主服务器状态改变,都会给从服务器发送写命令,从服务器执行写命令后,就保证主从一致性。
心跳检测
在命令传播阶段,从服务器默认每秒1次的频率,向主服务器发送如下命令:
replconf ACK <replication_offset>
replication_offset是从服务器的当前的复制偏移量,发送此命令有三个作用:
1、检测主从服务器网络状态。
如果主服务器超过1秒没有接收到从服务器的replconf ack命令,表示主从间的连接出问题了。info replication命令可以查看从服务器最后一次发送该命令的时间,属性值是lag,即lag大于1表示出问题了。
2、辅助实现min-slaves选项。
redis的min-slaves-to-write和min-slaves-max-lag两个选项可以在主服务器不安全的情况下,防止执行客户端的写命令。
例如min-slaves-to-write3,min-slaves-max-lag 10,表示少于3个从服务器或三个从服务器的延迟(lag)值都大于等于10秒,主服务器将拒绝执行写命令。
3、检测命令丢失。
由于每次发送replconf ack命令,都带上当前的复制偏移量,这样就可以保证如果主服务器发送给从服务器的命令传播,由于网络问题导致数据丢失的问题能1秒内被主服务器发现。主服务器发现后,会将相应偏移量的内容从复制积压缓冲区取出,并发送给从服务器。
replconf ack命令和复制积压缓冲区的概念,都是从redis2.8开始有的,在此之前的版本,主服务器无法发现从服务器的数据丢失。因为使用主从的情况下,为了数据一致性,尽量使用2.8以上版本的redis。
总结
- Redis 2.8 以前的复制功能不能高效地处理断线后重复制情况, 但 Redis 2.8 新添加的部分重同步功能可以解决这个问题。
- 部分重同步通过复制偏移量、复制积压缓冲区、服务器运行 ID 三个部分来实现。
- 在复制操作刚开始的时候, 从服务器会成为主服务器的客户端, 并通过向主服务器发送命令请求来执行复制步骤, 而在复制操作的后期, 主从服务器会互相成为对方的客户端。
- 主服务器通过向从服务器传播命令来更新从服务器的状态, 保持主从服务器一致, 而从服务器则通过向主服务器发送命令来进行心跳检测, 以及命令丢失检测。
参考文章:
《Redis设计与实现》