Redis设计 - 复制机制

前言

Redis可以通过执行SLAVEOF命令或者配置slaveof选项,让一个服务器去复制(replicate)另一个服务器,被复制的叫主服务器(master),对主服务器进行复制的叫做从服务器(slave)。

如目前有Redis服务器:127.0.0.1:6379和127.0.0.1:6380
在127.0.0.1:6380执行命令:SLAVEOF 127.0.0.1 6379,那么127.0.0.1:6380这台服务器将成为127.0.0.1:6379这台的从服务器。

进行复制中的主从服务器双方的数据库将保存相同的数据,成为“ 数据库状态一致 ”。在Redis2.8之前使用旧版本的复制,2.8后使用新版本的复制功能。

复制功能的实现

1. 旧版复制功能的实现

Redis的复制功能分为同步(sync)命令传播(command propagate)两个操作。

  • 同步操作用于将从服务器的数据库状态更新至主服务器当期所处的数据库状态。
  • 命令传播用于同步操作后,主服务器的数据状态被修改,导致主从不一致时,让主从服务器的数据库状态重新回到一致状态。
1.1 同步

当客户端向从服务器发送SLAVEOF命令,让其对主服务器进行复制时,从服务器首先要执行对主服务器的同步操作,让数据库状态达到主服务器当前状态,通过SYNC命令完成:

  • 从服务器向主服务器发送SYNC命令。
  • 主服务器收到SYNC命令,执行BGSAVE命令,在后台生成RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
  • RDB文件生成后,发送给从服务器,从服务器接收了文件后执行数据恢复操作。
  • 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行后,数据即可更新至和主服务器一致。
主从SYNC命令通信
1.2 命令传播

完成同步操作后,主从只是保持了在主服务器收到SYNC命令时,两个数据库状态的一致,当主服务器接收到客户端的写命令导致数据被修改时,主从的数据库状态就不一致了。

所以,主服务器会将自己执行的写命令,发送给从服务器执行(命令传播),从服务器执行了相同的写命令后 ,主从再次达到一致。

1.3 旧版复制的缺陷

在Redis中,从服务器对主服务器的复制可以分为以下两种情况:

  • 初次复制:从服务器以前没有和当前主服务器复制过。
  • 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连重新连上了主服务器,并继续复制主服务器。

旧版复制的弊端正是在于断线后重新复制,每次的断线都会导致全盘复制,其实从服务器需要同步的有时就差1、2个命令,这样的做法时低效且耗费资源的。

SYNC命令是个十分耗费资源的操作:
1)主服务器需要执行BGSVAE命令来生成RDB文件,这个操作会耗费主服务器大量的CPU、内存和磁盘I/O资源。
2)需要将RDB文件传输给从服务器,需要耗费主从服务器大量的带宽和流量,还会影响主服务器对命令的响应时间。
3)接收到RDB文件后,从服务器要对其进行载入,从服务器在载入期间阻塞没法处理命令请求。

2. 新版复制功能的实现

为了解决旧版本的复制功能在断线重复制情况下的低效问题,Redis从2.8版本开始,使用了PSYNC命令代替SYNC命令。

PSYNC具有 完整重同步(full resynchronization 和 部分重同步(partial resynchronization) 两种模式:

  • 完整同步用于初次复制情况,和旧版本原理一致。
  • 部分重同步则用于处理断线后重复制的情况,当从服务器短线后进行重连,如果条件允许,主服务器可以将断线期间的命令重新发送给从服务器,这样从服务器就可以恢复数据了,避免全盘复制。
部分重同步过程
2.1 部分重同步的实现

部分重同步由三个部分构成:

  • 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量。
  • 主服务器的复制积压缓冲区(replication backlog)。
  • 服务器的运行ID(run ID)。

2.1.1 复制偏移量
执行复制的双方,主服务器和从服务器分别维护一个复制偏移量:

  • 主服务器每次向从服务器发送N个字节的数据,就将字节的复制偏移量的值加上N。
  • 从服务器每次收到主服务器传来的N个字节数据时,就将自己复制偏移量的值加上N。

主从服务偏移量同步示例如下:

一开始主从的偏移量都为10086

image.png

之后主服务器向从服务器传播长度33字节的数据

image.png

通过对比主从服务器的复制偏移量,程序便能知道主从服务器是否处于一致的状态。

2.1.2 复制积压缓冲区

问:如果从服务器断线后,复制偏移量落后于主服务器,那么主服务器是如何补偿从服务器断线期间丢失的那部分数据呢?
答:和主服务器的复制积压缓冲区有关。

复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将命令写入到复制积压缓冲区中。

主服务器向复制积压缓冲区写数据

主服务器中还保存着一部分最近执行的命令,并且复制缓冲区会为队列中的每个字节记录相应的复制偏移量,如下图所示:

复制积压缓冲区构造

当从服务器重新连接上主服务器时,从服务器通过PSYNC命令将自身的offset发送给主服务器,主服务器根据offset来决定下一步的操作:

  • 如果offset偏移量之后的数据,仍然在缓冲区内,则只需要将缓冲区之后的内容发送给从服务器即可
  • 如果偏移量之后的数据已经不再缓冲区里面,说明从服务器已经掉线很久了,或者则只能进行完全同步操作。

关于积压缓冲区大小设置
Redis为复制积压缓冲区设置的默认大小为1MB,如果主服务器需要执行大量写命令,又或者主从服务器断线后重连接所需的时间比较长,那么这个大小也许并不合适。如果复制积压缓冲区的大小设置得不恰当,那么PSYNC命令的复制重同步模式就不能正常发挥作用,因此,正确估算和设置复制积压缓冲区的大小非常重要。
复制积压缓冲区的最小大小可以根据公式second * write_size_per_second来估算:

  • 其中second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算);
  • write_size_per_second则是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和)。

例如,如果主服务器平均每秒产生1 MB的写数据,而从服务器断线之后平均要5秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于5MB。为了安全起见,可以将复制积压缓冲区的大小设为2 * second * write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理。

2.1.3 服务器运行ID

除了复制偏移量和复制积压缓冲区之外,实现部分重同步还需要用到服务器运行ID:

  • 每个redis服务器,启动的时候都会生成一个ID,由40个随机的16进制字符组成。
  • 从服务器对主服务器首次复制时,主服务将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。断线重连的时候需要带上这个ID:

主服务器收到的ID和自身相同,那么说明复制的目标就是当前的主服务器
如果不同,说明从服务器之前复制的并不是当前这个主服务器,这时将执行完整同步操作。

2.2 PSYNC命令的实现

在了解了部分重复制的原理之后,就可以来了解PSYNC命令的完整细节了。其调用方式有两种:

  • 如果从服务器首次复制,则直接发送PSYNC ? -1命令,进行全量同步。
  • 反之,发送PSYNC 格式的命令,由主服务器判断是否可以执行部分同步。

根据情况,主服务器会返回如下三种情况的回复:

  • 如果主服务器返回 FULLRESYNC,表示将执行完整同步操作,从服务器需要将runid保存起来,下次发送PSYNC命令需要用到,而offset则作为初始化偏移量。
  • 如果主服务器返回 CONTINUE回复,表示从服务器可以进行增量同步操作,只需等待主服务器发送缺少的命令即可。
  • 如果主服务器返回-ERR回复,表示主服务器的版本低于2.8,无法识别这个命令,从服务器将向主服务器发送SYNC命令,执行全量同步操作。
PSYNC同步流程

复制的详细步骤

向从服务器发送SLAVEOF ,就可以让从服务器去复制主服务器。

下面将复制的步骤详细展开

1. 设置主服务器的地址和端口

当客户端执行slaveof命令时,服务器首先要将主服务器的IP地址和端口号保存在masterhost属性和masterport属性里面:

struct redisServer {
    // 主服务器地址
    char  *masterhost;
    // 主服务器端口
    int masterport;
};

slaveof 命令时一个异步命令,完成属性设置之后,从服务器将向发送slaveof命令的客户端返回OK,表示复制指令已经被接收,然而真正的复制工作在OK返回之后才开始执行。

2. 建立套接字连接

从服务器获取了主服务器的IP和端口后,创建和主服务器的套接字连接。

1)连接成功后,从服务器将为这个套接字关联一个专门用于处理复制工作的文件事件处理器,负责执行后续的复制工作,如接收RDB文件、接收传播命令等。

2)主服务器则将从服务器当做一个客户端来对待,为该套接字创建相应的客户端状态,即从服务器是主服务器的客户端

3. 发送PING命令

从服务器成为了主服务器的客户端之后,首先发送一个PING命令,它起到两个作用:
1)检查通信状态是否正常
2)检查主服务器是否可以正常处理请求,因为复制工作接下来的步骤都必须在主服务器可以正常处理命令请求的状态下才能进行

收到的回复一般有三种情况:

  • 如果主服务器返回了命令回复,但从服务器因为延迟(timeout)并没有读取到回复内容,则从服务器会断开连接,并尝试重新创建套接字
  • 如果服务器返回一个错误,则表示主服务器暂时无法处理命令请求,从服务器需要断开,并重新创建套接字。
  • 如果从服务器收到“PONG”回复,则说明主服务器状态正常,可以进行后续步骤

4. 身份验证

从服务器收到“PONG”回复后,下一步就是要进行身份验证,如果从服务器开启了masterauth选项的话,则进行身份验证。

从服务器身份验证情况分析:

  • 主服务器没有设置requirepass选项,从服务器也没设置masterauth选项,主服务器将继续执行从服务器发送的命令,复制正常进行。
  • 从服务器通过AUTH命令发送的密码和主服务器requirepass设置的相同,复制工作正常进行;反之,主服务器返回一个invalid password错误。
  • 主服务器设置了requirepass选项,从服务器没有masterauth选项,主服务器将返回NOAUTH错误。如果主服务器没设置requirepass,从服务器却设置了masterauth选项,主服务器返回no password is set错误。
身份验证流程

5. 发送端口信息

从服务器将通过命令REPLICONF listening-port,将自身的监听端口发送给主服务器,主服务器将会记录在redisClient对象的slave_listening_port属性中:

typedef struct redisClient {
    int slave_listening_port;
}

目前该属性的唯一作用就是在执行INFO REPLICATION时候,显示出各个从服务器的端口号。

6. 同步

从服务器将主服务器发送PSYNC命令,执行同步操作。同步操作执行完成之后,主从服务器都是对方的客户端,它们可以相互向对方发送命令请求,或者互相向对方返回命令回复。

主从互为客户端

7. 命令传播

完成同步之后,主从服务器就进入命令传播阶段,这时主服务器只要将自己的写命令发送给从服务器即可。

心跳检测

命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令: REPLCONF ACK (replication_offset就是从服务器当前的复制偏移量)。

这个命令有三个作用:
1)监测主从之间的网络连接状态
2)辅助实现min-slaves选项
3)监测命令丢失

1. 监测主从服务器的网络连接状态

通过主服务器的INFO replication命令,在列出的从服务器列表的lag一栏,我们可以看到相应的从服务器最后一次发送心跳的时间,一般情况下,lag的值应该在0到1秒之间跳动,如果超出了1秒,那么说明主从服务器之间出现了故障。

image.png

2. 辅助实现min-slaves选项

Redis的 min-slaves-to-write 和 min-slaves-max-lag 两个选项配置可以防止服务器在不安全的情况下执行写命令。

如:
min-slaves-to-write 3
min-slaves-max-log 10
那么在从服务器的数量少于3个,或者三个从服务器的延时(lag)值都大于或等于10秒时,主服务器将拒绝执行写命令。

3. 检测命令丢失

如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么从服务器发送的心跳请求,其中包含的偏移量必定小于主服务器的,主服务器会在复制积压缓冲区里面找到丢失的命令,发送给从服务器。

回顾

本篇主要介绍了Redis的复制机制,包括新旧复制功能的实现、如何实现增量同步(命令传播)、新复制功能是如何解决断线重复制问题(offset)。

你可能感兴趣的:(Redis设计 - 复制机制)