Redis设计与实现读书笔记---复制/Sentinel/集群

复制

复制模式:用户通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器区复制另一个服务器。

旧版复制功能

Redis的复制功能分为同步(sync)和命令传播(command propagate);

同步操作用于将从服务器的数据状态更新至主服务器当前所在的数据库状态;

命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器状态不一致的情况时,通过命令传递,将主从服务器的数据库状态从新回到一致状态;

同步

从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成;

  1. 从服务器向主服务器发送SYNC命令;
  2. 主服务器收到SYNC命令后,执行BGSAVE命令,后台生成RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令;
  3. 当主服务器的BGSAVE命令执行完毕时,主服务器将生成的RDB文件通过网络发送从服务器,从服务器接收RDB文件,并将自身的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态;
  4. 主服务器再将缓冲区中的写命令发送给从服务器,以使从服务器的数据库状态与主服务器一致;

命令传播

主从服务器之间存在网络延时,当主服务器的数据库状态因为客户端的命令更新时,从服务器需要通过主服务器的命令传播获取客户端的命令来更新自身的数据库状态。

旧版复制功能的问题

redis中,从服务器对主服务器的复制主要有两种:

  1. 初次复制:从服务器第一次连接上主服务器,或从服务器当前要复制的主服务器与上一次复制的主服务器不同;
  2. 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断复制,但服务器通过自动重连连接上主服务器,并继续复制主服务器;

旧版复制功能对断线重连这种情况的支持不友好,每次断线重连都要重新生成RDB文件,这样不符合高性能的特点,应该需要区分断线重连是否丢失很多数据,如果中间丢失了较大部分的数据,这时候需要全量复制,否则应该为增量复制。

新版复制功能的实现

Redis2.8版本开始,通过PSYNC命令替代SYNC命令来实现复制时的同步操作;

PSYNC主要分为完整重同步(full resynchronization)和部分重同步(partial resynchronization)

完整重同步主要作用:主服务器创建并发送RDB文件给从服务器,向从服务器发送保存在缓冲区中的写命令;

部分重同步主要作用:根据从服务器断线时,缺失的记录位置,将缺失的命令发送给从服务器;

部分重同步的实现

部分重同步功能由三部分构成:

  1. 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量;
  2. 主服务器的复制积压缓冲区(replication backlog);
  3. 服务器的运行ID(run ID).

复制积压缓冲区:由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB;

从服务器的复制偏移量在主服务器的偏移量offset之后,进行部分复制;

复制积压缓冲区的最小大小计算公式:second*write_size_per_second

second:从服务器断线后重新连接上主服务器所需的平均时间;

write_size_per_second:主服务器平均每秒产生的写命令数据量

主服务器运行ID:每隔Redis服务器都有自己运行的ID,运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成;

从服务器在对主服务器初次复制时,主服务器会将自己的运行ID传送给从服务器,从服务器将次ID保存,当从服务器断线重连时,将保存的ID发送主服务器,主服务器通过ID比对确定从服务器部分重同步还是完整重同步。

PSYNC命令执行的流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hyuux6XS-1633517443612)(…/pic/image-20210917234509974.png)]

复制的实现

  1. 设置主服务器的地址和端口:从服务器将主服务器的IP地址和端口信息保存到服务器状态masterhost属性和masterport属性中;
  2. 建立套接字连接:从服务器作为主服务器的客户端,主服务器作为从服务器的服务器与客户端,相互进行消息传递;
  3. 发送PING命令:确定网路环境适合进行消息交换;
  4. 身份验证:主服务器设置requirepass选项,从服务器设置masterauth选项,才需要验证。
  5. 发送端口信息:从服务器将自身监听的端口号通过REPLCONF listen-port 123命令发送给主服务器,主服务器收到命令后,将端口信息维护在客户端状态中;
  6. 同步:主从服务器通过发送命令或者返回命令回复来将主从服务器的状态维持一致。
  7. 命令传播:在从服务器已经将数据库状态更新到主服务器最新状态的前提下,后续的主服务器状态都通过命令传播更新从服务器的数据状态;
  8. 心跳检测:从服务器默认以每秒一次的频率向主服务器发送命令,REPLCONF ACK <replication_offset>主要用于检测主从服务的网络连接状态,辅助实现min-slaves选项,检测命令丢失。

min-slaves-to-write:从服务器少于设定数量,主服务器拒绝执行写命令

min-slaves-max-lag:从服务器的延时大于或等于设定数量时,主服务器拒绝执行写命令

Sentinel

Redis高可用性的解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及主服务器下的从服务器,当被监视的主服务器下线的时候,自动从其所管理的从服务器中选举一台出来升级为主服务器。

启动命令:redis-sentinel /sentinel.conf 或者 redis-server /sentinel.conf --sentinel

Sentinel启动的步骤:

  1. 初始化服务器:通过服务器的资源库加载Sentitnel启动需要内容;
  2. Sentinel专用代码替换Redis服务器代码:加载Sentinel使用的命令,不加载普通服务器使用的命令;
  3. 初始化Sentinel状态:维护Sentinel的状态信息;
  4. 根据给定的配置文件,初始化Sentinel的监视主服务器列表;
  5. 创建连向主服务器的网络连接:Sentinel将成为主服务器的客户端,可以向主服务器发送命令,并从命令回复中获取相关信息。

获取主服务器信息

Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的服务器发送INFO命令,并通过解析INFO命令的回复来获取主服务器当前的信息。

获取从服务器信息

当Sentinel发现主服务器有新的从服务器出现时,Sentinel除为新的从服务器创建相应的结构实例外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。

向主服务器和从服务器发送信息

默认情况下,Sentinel以每两秒一次的频率,向被监视主服务器和从服务器的Sentinel发送信息,起到信息通知的作用;

接收来自主服务器和从服务器的频道信息

当Sentinel和主服务器或者从服务器建立订阅连接后,Sentinel会接收_ Sentinel _:hello频道的内容,并根据内容更新服务器实例的信息结构。

更新Sentinels字典

Sentinel为主服务器创建的实例结构中的Sentinels字典保存了除Sentinel本身之外,同样保存其他监视主服务器的Sentinel信息,当Sentinel从订阅频道中接收到其他的Sentinel的信息时,会根据信息内容更新实例结构中的信息内容。

创建连向其他Sentinel的命令连接

当Sentinel通过频道消息发现一个新的Sentinel时,会将新的Sentinel在Sentinels字典中创建相应的实例结构,Sentienl之间也会创建命令连接,最终监视同一个主服务器的多个Sentinel之间连成了相互连接的网络。

Sentinel之间只创建命令连接不创建订阅连接,Sentinel通过命令连接来进行主观下线和客观下线,通过订阅连接获取主从服务器的Sentinel信息

检测主观下线

默认情况下,Sentinel以每秒一次的频率向所有与它创建了命令连接的实例(主服务器,从服务器,其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。当发现某个实例在down-after-milliseconds时间内没有响应,则认为实例主观下线。

检查客观下线状态

当一个Sentinel对某个主服务器认为下线后,会咨询其他监听同一个主服务器的Sentinel对主服务器状态的判断,当收集到足够多的确认后,Sentinel会认为该主服务器已下线,对主服务器进行故障转移操作。

发送Sentinel is-master-down-by-addr命令

通过此命令询问其他Sentinel是否同意主服务器下线

接收Sentinel is-master-down-by-addr命令

当前Sentinel接收到其他Sentinel发送的SENTINEL is-master-down-by-addr 127.0.0.1 6379 0 *(IP地址、端口号、配置纪元、runid)命令后,会解析命令中得 主服务器地址和端口,然后检擦主服务器是否下线,再向原Sentinel返回信息Sentinel is-master-down-by

客观下线得判断条件:当认为主服务器客观下线的Sentinel的数量达到Sentinel配置中设置的数量,那么就认为主服务器客观下线;

sentinel monitor master 127.0.0.1 6379:认为主服务器客观下线的Sentinel的数量为2(包含当前Sentitnel)

不同的Sentinel可能设置的客观下线数量不一样,这是允许的

选举领头的Sentitnel

当主服务器被判断为客观下线后,监视这个主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel;

选举规则:

  1. 每个发现主服务器主观下线的Sentienl都会向其他Sentinel发送Sentinel is-master-down-by-addr ip port leader-epoch leader-ip
  2. 每个没设置领头Sentinel的Sentinel收到这个信息后,都会将领头Sentinel设置为命令中的leader-ip,然后返回响应Sentinel is-master-down-by ;
  3. Sentinel根据命令返回中的配置纪元和runid确认是否获得选票,当Sentinel在同一配置纪元获得超过半数以上的选票时,就自动升级为领头Sentinel。

故障转移

操作步骤:

  1. 在已下线的主服务器的下属从服务器中选出一个从服务器,将其转为主服务器;
  2. 让已下线的主服务器下属从服务器改为复制新的主服务器;
  3. 将已下线的主服务器设置为新主服务器的从服务器,当下线的服务器重连后,就成了新主服务器的从服务器。

选出新的主服务器

过滤规则:

  1. 删除列表中所有处于下线或者断线状态的从服务器,保证列表中的从服务器都正常在线;
  2. 删除五秒内没回复领头Sentinel的INFO命令的从服务器,保证列表中的从服务器都可以正常通信;
  3. 删除所有与断线主服务器断开时间超过down-after-milliseconds*10毫秒的从服务器,保证列表中的从服务器都是最后和主服务器断开。
  4. 以上规则选出从服务器集合,根据从服务器的优先级对集合中的从服务器排序,选出优先级最高的从服务器;如果存在优先级相同的从服务器还需要比较偏移量,偏移量大的从服务器优先。

过滤后的从服务器执行SLAVEOF no one ,升级为主服务器;

修改从服务器复制目标

将其余的从服务器设置为新的主服务器的从服务器,SLAVEOF 新主服务器。

将旧的主服务器变为从服务器

集群

Redis集群时Redis提供的分布式数据库方案,集群通过分片进行数据共享,并提供复制和故障转移功能。

节点

节点是一个独立的单元,多个节点通讯构成集群。一个节点只能属于一个集群。

集群内节点通过CLUSTER MEET 命令将不属于集群的节点添加到集群中

Redis服务器启动时会根据配置项的cluster-enabled判断是否开启服务器的集群模式,如果为false则按单机模式启动。

节点使用redisServer结构保存服务器的状态,使用redisClient结构保存客户端的状态

集群的数据结构

clusterNode结构保存节点的当前状态

struct clusterNode{
	//创建节点时间
    mstime_t ctime;
    //节点名称
    char name[REDIS_CLUSTER_NAMELEN];
    //节点标识
    int flags;
    //节点当前的配置纪元,用于实现故障转移
    unit64_t configEpoch;
    //节点的IP地址
    char ip[REDIS_IP_STR_LEN]
    //节点的端口号
    int port;
    //保存连接节点所需的有关信息
    clusterLink *link;
}
typedef struct cluster clusterLink{
	//连接的创建时间
    mstime_t ctime;
    //TCP 套接字描述符
    int fd;
    //输出缓冲区
    sds sndbuf;
    //输入缓冲区
    sds rcvbuf;
    //与当前连接关联的节点
    struct clusterNode *node;
}clusterLink

RedisClient结构中的套接字和缓冲区用于连接客户端,clusterLink结构中的套接字和缓冲区用于连接节点。

clusterState结构记录当前节点保存的集群状态

typedef struct clusterState{
	//指向当前节点的指针
    clusterNode *myself;
    //集群当前配置纪元
    uint64_t currentEpoch;
    //集群当前状态
    int state;
    //集群中至少处理着一个槽的节点的数量
    int size;
    //集群节点名单
    dict *nodes;
}clusterState

槽指派

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于16384个槽中的一个,集群中的每个节点可以处理0个或最多16384个槽。

通过向节点发送CLUSTER ADDSLOTS [slot …]命令,可以将一个或多个槽指派给节点负责

clusterNode结构的slots属性和numslot属性记录节点负责处理槽的信息

struct clusterNode{
	unsigned char slots[16384/8];
	int numslots;
	
}

如果slots数组在索引i上的二进制位的值为1,表示节点负责处理槽i

clusterState.slots数组记录了集群中所有槽的指派信息,当程序需要将某个节点的槽指派信息通过消息发送给其他节点,程序只需要将相应节点的clusterNode.slots数组整个发送出去。

clusterState.slots数组记录了集群中所有槽的指派信息,clusterNode.slots数组只记录了clusterNode结构所代表的节点的槽指派信息

在集群中执行命令

当客户端向节点发送与数据库键相关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给自己,如果槽所对应的节点为当前节点,则当前节点执行命令;否则,节点向客户端返回MOVED错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令。

计算键属于哪个槽

计算方法为:CRC16(key)& 16384

通过命令CLUSTER KEYSLOT 命令可以查看一个给定键属于哪个槽。

判断槽是否由节点负责

首先通过clusterState中的slots数组,判断槽是否由当前节点负责,如果不是,从数组中获取clusterNode节点信息,向客户端返回MOVED错误,指引客户端转向负责槽的节点。

集群模式的redis-cli客户端在接收到MOVED错误后,不会打印MOVED错误,而是根据MOVED错误自动进行节点转向,并打印转向信息。

重新分片

redis-trib对集群的单个slot进行重新分片步骤

  1. 对目标节点发送CLUSTER SETSLOT IMPORTING <source_id>命令,让目标节点准备从源节点导入属于槽的键值对;
  2. 对源节点发送CLUSTER SETSLOT MIGRANTING <target_id>命令,让源节点准备号将属于槽slot的键值对迁移至目标节点;
  3. 向源节点发送CLUSTER GETKEYSINSLOT 命令,获得最多count个属于槽slot的键值对的键名(key name);
  4. 将步骤3中获取的键名,配合命令MIGRATE <target_ip> <target_port> <key_name> 0 发送给源节点,将被选中的键原子地从源节点发送到目标节点;
  5. 重复执行步骤3、4,直至所有键值对迁移完毕;
  6. redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT NODE <target_id>命令,将槽slot指派给目标节点,这一指派信息通过消息发送至集群,使集群中所有节点直到槽slot已指派给目标节点。

当客户端向源节点发送一个数据库键相关的命令,并且命令要处理的数据库键恰好处于被迁移的槽,服务器会向客户端返回一个ACK错误,reids-cli的单机模式会打印这个异常。

ASK错误和MOVED错误的区别

MOVED错误代表槽的负责权从一个节点转移到另一个节点。

ASK错误是两个节点在迁移槽的过程中使用的一种临时措施。

复制与故障转移

Redis集群中的节点分为主节点和从节点,主节点负责处理槽,从节点负责复制主节点,并在主节点下线后,代替下线主节点继续处理命令请求。

设置从节点

向节点发送命令CLUSTER REPLICATE <node_id>使当前节点称为目标节点的从节点。

故障检测

集群中的每个节点都会定期向集群中的其他节点发送PING消息,来检测对方是否在线,如果目标节点没在规定时间内回复PONG消息,那么将会被标记为疑似下线(probable fail,PFAIL)

集群中的各个节点会通过相互发送信息的方式来交换来集群中各个节点的状态信息。当主节点收到其他主节点的目标节点下线状态报告,会将状态报告保存在clusterNodeFailReport中。

如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

故障转移

当一个从节点发现正在复制的主节点已进入下线状态,从节点将开始对下线的主节点进行故障转移

  1. 复制下线主节点的所有从节点里面,会有一个从节点被选中;
  2. 被选中的从节点执行SLAVEOF no one命令,成为新的主节点;
  3. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己;
  4. 新的主节点会向集群广播一条PONG消息,这条PONG消息可以让集群中的其他主节点立即知道当前从节点变成为主节点,并且当前主节点已经接管了原本由已下线节点负责处理的槽。
  5. 新的主节点开始接收和处理自己负责的槽有关的命令请求,故障转移完成。

选举新的主节点

规则:

  1. 集群的配置纪元为自增计数器,初始值为0;
  2. 当集群中某个节点开始故障转移操作时,集群配置纪元的值会增加一;
  3. 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票机会,而第一个向主节点要求投票的从节点将获得主节点的投票;
  4. 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTER_TYPE_FAILOVER_AUTH_REQUEST消息要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票;
  5. 如果一个主节点具有投票权,并且这个主节点还没投票,那么主节点将向要求投票的从节点返回一条CLUSTER_TYPE_FAILOVER_AUTH_ACK消息,表示主节点支持从节点成为新的主节点。
  6. 每个参与选举的从节点都会收到CLUSTER_TYPE_FAILOVER_AUTH_ACK消息,并根据收到的消息数量来统计获取到支持的主节点数量;
  7. 如果集群有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,从节点就成为新的主节点。
  8. 在每一个配置纪元中,每个具有投票权的主节点只能投一次票,所以如果N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,从而确保新的主节点只有一个;
  9. 如果一个配置纪元中无法选出从节点作为主节点,那么再次进行选举,开启新的配置纪元;

消息

集群中各个节点通过发送和接收消息来进行信息交互。

消息类型如下:

  1. MEET消息:当发送者接收到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群中;
  2. PING消息:集群中每个节点默认每隔一秒就从已知节点列表中随机选出五个节点,对五个节点中最长时间没发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此外,如果当前时间超过节点的cluster-node-timeout选项设置时长的一半,当前节点也会向这类节点发送消息,防止目标节点存在消息滞后;
  3. PONG消息:当接收者收到发送者发来的MEET消息或者PING消息,为了发送者确定此消息已到达,接收者会返回一个PONG消息;一个节点可以通过集群广播PONG消息来让集群的其他节点立即刷新关于这个节点的认识。
  4. FAIL消息:当一个主节点判断另一个主节点进入FAIL状态,当前节点会向集群广播一条关于目标节点的FAIL消息,所有接收到这个消息的节点都将目标节点标记为下线;
  5. PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条消息的节点都会执行相同的PUBLISH命令。
上一篇:Sentinel隔离和降级-熔断降级原理


下一篇:Sentinel-限流规则入门