Redis高可用的基石——主从复制
Redis 支持主从同步,提供 Cluster 集群部署模式,通过 Sentinel 哨兵来监控 Redis 主节点的状态,以此来保证 Redis 的高可用,而主从复制正是高可用的基石。
一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。从节点的数据来自主节点,实现原理就是主从复制机制。
主从复制过程
一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。从节点的数据来自主节点,实现原理就是主从复制机制。
主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段。
连接建立阶段
- 保存主节点信息
- 建立socket连接:slave 将根据指定的 IP 地址和端口,向 master 发起套接字(socket)连接,master 在接受(accept) slave 的套接字连接之后,为该套接字创建相应的客户端状态,此时连接建立完成。
- 发送ping命令:slave 向 master 发送一个 PING 命令,以检査套接字的读写状态是否正常、 master 能否正常处理命令请求。
- 身份验证:slave 向 master 发送 AUTH password 命令来进行身份验证。
- 发送从节点端口信息:在身份验证通过后后,slave 将向 master 发送自己的监听端口号,master 收到后记录在 slave 所对应的客户端状态的 slave_listening_port 属性中。
数据同步阶段
主从复制包括全量复制,增量复制两种。一般当 slave 第一次启动连接 master,就采用全量复制;而后续 master持续将写命令,异步复制给 slave,采用增量复制。数据同步阶段使用全量复制。
slave 将向 master 发送 PSYNC
命令,表示要进行数据同步。master 收到该命令后判断是进行部分重同步还是完整重同步,然后根据策略进行数据的同步。
psync 命令包含两个参数,分别是主服务器的 runID 和复制进度 offset。
- runID,每个 Redis 服务器在启动时都会自动生产一个随机的 ID 来唯一标识自己。当从服务器和主服务器第一次同步时,因为不知道主服务器的 runID,所以将其设置为 “?”。
- offset,表示复制的进度,第一次同步时,其值为 -1。
全量复制
执行了 replicaof
命令后,从服务器就会给主服务器发送 psync
命令,表示要进行数据同步。主服务器收到 psync
命令后,会用 FULLRESYNC
作为响应命令返回给对方。FULLRESYNC
响应命令表示采用全量复制的方式:
- 主节点会执行 bgsave 命令生成 RDB 文件,并将 RDB 文件发送给从节点;从节点首先清除自己当前的旧数据,然后载入接收的 RDB 文件,将数据库状态更新至主节点执行 bgsave 时的数据库状态,期间基于旧的数据版本对外提供服务;
- 同时在生成 RDB 的过程是不会阻塞主线程的,那么为了保证这期间的写操作命令能够同步到从节点,主服务器会将在 RDB 文件生成后收到的写操作命令,写入到 replication buffer 复制缓冲区里;
- 主节点在生成的 RDB 文件发送后,会将 replication buffer 复制缓冲区里记录的所有写操作命令发送给从节点,然后从节点执行这些写操作,将数据库状态更新至主节点的最新状态;
分摊主节点压力
主从节点在第一次数据同步的过程中,主节点会做两件耗时的操作:生成 RDB 文件和传输 RDB 文件。主节点是可以有多个从节点的,如果从节点数量非常多,而且都与主节点进行全量同步的话,就会带来两个问题:
- 由于是通过 bgsave 命令来生成 RDB 文件的,那么主节点就会忙于使用 fork() 创建子进程,如果主节点的内存数据非大,在执行 fork() 函数时是会阻塞主线程的,从而使得 Redis 无法正常处理请求;
- 传输 RDB 文件会占用主节点的网络带宽,会对主节点响应命令请求产生影响。
主服务器生成 RDB 和传输 RDB 的压力可以分摊到从服务器。在「从服务器」上执行 replicaof <目标服务器IP> 6379
这条命令,使其作为目标服务器的从服务器:此时如果目标服务器本身也是「从服务器」,那么该目标服务器不仅可以接受主服务器同步的数据,也会把数据同步给自己旗下的从服务器,从而减轻主服务器的负担。
命令传播阶段
当主从节点完成了全量数据同步之后,就会进入命令传播阶段,双方之间会维护一个 TCP 连接。后续 master 只要一直将自己执行的写命令发送给 slave ,而 slave 只要一直接收并执行 master 发来的写命令,就可以保证 master 和 slave 数据库状态保持一致了。
并且这个 TCP 连接是长连接,目的是避免频繁的 TCP 连接和断开带来的性能开销。
在命令传播阶段,每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。从节点会向主节点发送REPLCONF ACK命令,频率是每秒1次;命令格式为:REPLCONF ACK {offset},其中offset指从节点保存的复制偏移量。
增量复制
主从服务器在完成第一次同步后,就会基于长连接进行命令传播。但如果主从服务器间的网络连接断开了,就无法进行命令传播了,客户端就可能从「从服务器」读到旧的数据。那么如果此时断开的网络又恢复正常了,或者从节点宕机又恢复了之后,要怎么继续保证主从服务器的数据一致性呢?
在 Redis 2.8 之前,如果主从服务器在命令同步时出现了网络断开又恢复的情况,从服务器就会和主服务器重新进行一次全量复制,很明显这样的开销太大了。
所以,从 Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。
- 从服务器在恢复网络后,会发送 psync 命令
psync {runID} {offset}
给主服务器,此时的 psync 命令里的 offset 参数不是 -1; - 主服务器收到该命令后,然后用
CONTINUE
响应命令告诉从服务器接下来采用增量复制的方式同步数据; - 然后主服务将主从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。
而这些增量数据则是通过复制偏移量以及复制积压缓存区实现的:
- 复制积压缓冲区 repl_backlog_buffer:是一个环形缓冲区,用于主从服务器断连后,从中找到差异的数据;
- 复制偏移量 replication offset:主节点和从节点分别维护各自的复制偏移量 (offset),标记复制积压缓冲区的同步进度,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。
在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到复制积压缓冲区里,因此缓冲区里会保存着最近传播的写命令。网络断开后,当从服务器重新连上主服务器时,从服务器会通过 psync 命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:
- 如果判断出从服务器要读取的数据还在复制积压缓冲区里,那么主服务器将采用增量同步的方式;
- 相反,如果判断出从服务器要读取的数据已经不存在复制积压缓冲区里(因为是环形缓冲区),那么主服务器将采用全量同步的方式。
当主服务器在复制积压缓冲区中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 复制缓冲区,这个缓冲区我们在全量复制时也提到过,它是缓存将要传播给从服务器的命令。
repl_backlog_buffer 复制积压缓冲区的默认大小是 1M,并且由于它是一个环形缓冲区,所以当缓冲区写满后,主服务器继续写入就会覆盖之前的数据。那么在网络恢复时,如果从服务器想读的数据已经被覆盖了,主服务器就会采用全量同步,这个方式比增量同步的性能损耗要大很多。
因此为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,我们应该调整下复制积压缓冲区大小,尽可能的大一些,减少出现从服务器要读取的数据被覆盖的概率,从而使得主服务器采用增量同步的方式。
**服务器运行ID(runid)**:每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。
参考资料