redis:高可用 复制、哨兵、集群

前言

redis通常不会是部署单个的,不然不会造成单点故障,那么redis有哪些高可用方案呢?

主从复制

用户可以通过SLAVEOF命令或者配置,让一个服务器去复制另一个服务器。被复制的服务器称为主服务器,进行复制的服务器称为从服务器。这样你在主服务器上增加键值,同时可以在从服务器上读取。

复制的过程又分为同步和命令传播两个步骤。

同步

同步将从服务器的数据库状态更新到主服务器当前的数据库状态。

客户端向从服务器发送SLAVEOF命令时,从服务器会向主服务器发生SYNC命令进行同步,步骤如下:

  1. 从服务器向主服务器发生SYNC命令。
  2. 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并用一个缓冲区记录从现在开始执行的所有写命令。
  3. 主服务器的BGSAVE命令执行完毕后,主服务器将BGSAVE生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将从服务器的数据库状态更新到主服务器执行BGSAVE命令时的数据库状态
  4. 主服务器将缓冲区的所有写命令发送给从服务器,从服务器执行这些写命令,将数据库状态更新至主服务器当前数据库状态。

image.png

命令传播

同步操作完成之后,主服务器和从服务器的数据库状态是一致的,但主服务器又接收到客户端写命令后,主从数据库之间又产生了数据不一致,这时通过命令传播达到数据库一致。

PSYNC同步的优化

2.8之前的同步每次都是全量同步,而如果是从服务器只是断开连接了一会,事实上是不用从头开始同步的,只需要将断开连接这会的数据同步即可。所以2.8版本开始使用PSYNC来代替SYNC命令。

PSYNC分成全量同步和部分同步两种情况,全量同步就是处理初次同步的状态,而部分同步就是处理断线重连这种情况。

部分同步的实现

部分同步主要使用了以下三部分:

  • 主服务器的复制偏移量和从服务器的复制偏移量
  • 主服务器的复制积压缓冲区
  • 服务器的运行ID

复制偏移量

主服务器的复制偏移量:主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量+N 从服务器的复制偏移量:从服务器每次收到主服务器传播的N个字节数据,就将自己的复制偏移量+N 如果主从服务器处于一致状态,那么它们的偏移量总是相同的,如果偏移量不相等,那么说明它们处于不一致状态。

复制积压缓冲区

复制积压缓冲区由主服务器维护的一个固定长度的FIFO队列,默认大小1MB,达到最大长度后,最先入队的会被弹出,给新入队的元素让位置。
redis命令传播的时候不但会发送给从服务器,还会发送给复制积压缓冲区。 image.png 当从服务器重连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器根据复制偏移量来决定使用部分同步还是全量同步。 如果offset偏移量之后的数据还在复制积压缓冲区,那么使用部分同步,反之使用全量同步。
(书上没说是怎么判断的,我猜测应该是拿主复制偏移量减去从复制偏移量,如果大于1MB就说明有数据不在缓冲积压区?)

服务器的运行ID

服务器启动时会生成一个40位随机的字符作为服务器运行ID。

从服务器对主服务器初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器会将这个运行ID保存下来。从服务器断线重连的时候,会将保存的运行ID发送过去,如果从服务器保存的运行ID和当前主服务器的运行ID相同,那么会尝试部分同步,如果不同会执行全量同步。

PSYNC的整体流程

image.png

心跳检测

在命令传播阶段,从服务器会默认以每秒一次的频率,向主服务器发送命令:
REPLICONF ACK <replication_offset>
其中replication_offset就是从服务器当前的复制偏移量。 发送REPLICONF ACK命令对于主从服务器有三个作用:

  1. 检测主从服务器的网络连接状态。
  2. 辅助实现min-slaves选项。
  3. 检测命令丢失。

检测主从服务器的网络连接状态

主从服务器可以通过发送和接收REPLICONF ACK命令来检查两者之间的网络连接是否正常:如果主服务器超过一秒钟没有收到从服务器发来的REPLICONF ACK命令,那么主服务器就知道主从之间出现问题了。

辅助实现min-slaves选项

redis的min-slaves-to-writemin-slaves-max-lag两个选项可以防止主从服务器在不安全的情况下执行写命令。

min-slaves-to-write 3
min-slaves-max-lag 10
复制代码

如果配置如上,就表示如果从服务器的数量少于3个,或者3个从服务器的延迟都大于或等于10秒时,那么主服务器就将拒绝执行写命令。

检测命令丢失

如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么从服务器向主服务器发送REPLICONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的偏移量,那么主服务器可以根据从服务器的复制偏移量,在复制缓冲区当中找到从服务器缺少的数据,将这些数据重写发送给从服务器。

主从复制总结

其实主从复制就是多备份了一份数据,因为即使有RDB和AOF进行持久化,但是可能主服务器上整个机器挂掉了,而主从复制可以将主从服务器部署在两台不同的机器上,这样即使主服务器的机器挂掉了,也可以手动切换到从服务器继续服务。

sentinel

主从虽然实现了数据的备份,但当主服务器挂掉时,需要手动的将从服务器切换成主服务器。而sentinel就可以实现当主服务器挂掉时,自动将从服务器切换成主服务器。

image.png
sentinel系统可以监视所有的主从服务器,假设server1现在下线。当server1的下线时长超过用户设定的下线时长上限时,sentinel系统就会对server1执行故障转移:

  1. 首先sentinel系统会挑选server1下的其中一个从服务器,并将这个选中的从服务器升级成新的主服务器。
  2. 之后,sentinel系统会向server1属下的所有从服务器发送新的复制命令,让他们成为新主服务器的从服务器。当所有从服务器复制新的主服务器时,故障转移操作执行完毕。
  3. 另外,sentinel还会监视已下线的server1,在它重新上线时,将它设置为新的主服务器的从服务器。

初始化sentinel状态

struct sentinelState {
    char myid[CONFIG_RUN_ID_SIZE+1]; 
    // 当前纪元,用于实现故障转移
    uint64_t current_epoch;
    // 保存了所有被这个sentinel监视的主服务器
    // 字典的键是主服务器的名字
    // 字典的值是指向sentinelRedisInstance结构的指针
    dict *masters;
    // 是否进入了TILT模式
    int tilt;         
    // 目前正在执行的脚本数量
    int running_scripts;   
    // 进入TILT模式的时间
    mstime_t tilt_start_time;   
    // 最后一次执行时间处理器的时间
    mstime_t previous_time;     
    // 一个fifo队列,包含了所有需要执行的用户脚本
    list *scripts_queue;            
    char *announce_ip;  
    int announce_port; 
    unsigned long simfailure_flags; 
    int deny_scripts_reconfig;
    char *sentinel_auth_pass;   
    char *sentinel_auth_user;    
    int resolve_hostnames;      
    int announce_hostnames;     
} sentinel;
复制代码

初始化sentinel状态的masters属性

masters记录了所有被sentinel监视的主服务器的相关信息,其中字典的键是被监视服务器的名字,而值是被监视服务器对应着sentinelRedisInstance结构。sentinelRedisInstance被sentinel服务器监视的实例,可以是主服务器、从服务器或其他sentinel实例。

typedef struct sentinelRedisInstance {
    // 标识值,记录实例的类型,以及该实例的当前状态
    int flags;  
    // 实例的名字
    // 主服务器名字在配置文件中设置
    // 从服务器和sentinel名字由sentinel自动设置,格式是ip:port
    char *name; 
    // 运行id
    char *runid;   
    // 配置纪元,用于实现故障转移
    uint64_t config_epoch;  
    // 实例的地址
    sentinelAddr *addr; /* Master host. */
    // 实例无响应多少毫秒之后,判断为主观下线
    mstime_t down_after_period; 
    // 判断这个实例为客观下线所需的支持投票数量
    unsigned int quorum;
    // 执行故障转移,可以同时对新的主服务器进行同步的从服务器数量
    int parallel_syncs; 
    // 刷新故障迁移状态的最大时限
    mstime_t failover_timeout;  
    // 除了自己外,其他监视主服务器的sentinel
    // 键是sentinel的名字,格式是ip:port
    // 值是键对应的sentinel的实例结构
    dict *sentinels;  
    // ...
} sentinelRedisInstance;
复制代码

创建连向主服务器的网络连接

初始化sentinel的最后一步是创建连向被监视主服务器的网络连接,会创建两个连向主服务器的连接。 image.png 命令连接:专门向主服务器发送命令,并接收命令回复。
订阅连接:专门用于订阅主服务器的_sentinel_:hello频道。

获取主服务器信息

sentinel默认会每10秒,通过命令连接向被监视的主服务器发送INFO命令,并通过回复获取主服务器当前的信息。回复可以获得以下信息。

  • 主服务器的run_id
  • 主服务器下所有从服务器的信息。

根据这些信息可以更新sentinelRedisInstance下的name字典和runid字段。

获取从服务器信息

sentinel也会创建连接到从服务器的命令连接和订阅连接。 image.png
sentinel默认会每10秒,通过命令连接向从服务器发送INFO命令,并通过回复获取从服务器当前的信息。回复如下:
image.png

  • 从服务器的运行ID
  • 从服务器的角色role
  • 主服务器的ip和端口
  • 主服务器的连接状态master_link_status
  • 从服务器的优先级slave_priority
  • 从服务器的复制偏移变量

根据info的回复信息,sentinel可以更新从服务器的实例结构。

向主服务器和从服务器的订阅连接发送信息

默认情况下,sentinel会每2秒一次,向被监视的主服务器和从服务器发送命令。 image.png s_ip:sentinel的ip地址
s_port:sentinel的端口号
s_runid:sentinel的运行id
s_epoch:sentinel当前的配置纪元
m_name:主服务器的名字
m_ip:主服务器的ip地址
m_port:主服务器的端口号
m_epoch:主服务器当前的配置纪元
向sentinel_:hello频道发送信息,也会被监视同一个服务器的其他sentinel监听到(包括自己)。

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

sentinel之间会互相创建命令连接。监视同一个嘱咐其的多个sentinel将形成相互连接的网络。 image.png

sentinel之间不会创建订阅连接。

检测主观下线状态

sentinel会每秒一次向所有与它创建了命令连接的实例(主服务器、从服务器、其他sentinel)发送ping命令,通过实例的回复来判断实例是否在线。
有效回复:实例返回+PONG、-LOADING、-MASTERDOWN其中一种。
无效回复:以上三种回复之外的其他回复,或者指定时长内没回复。
某个实例在down-after-milliseconds毫秒内,连续向sentinel返回无效回复。那么sentinel就会修改这个实例对应的实例结构,在结构的flags属性中打开SRI_S_DOWN标识,标识该实例进入主观下线状态。(down-after-milliseconds可以在sentinel的配置文件中配置)

检测客观下线状态

当sentinel将一个主服务器判断为主观下线后,为了确认这个主服务器是否真的下线,还会想其他同样监视这个主服务器的其他sentinel询问,看其他sentinel是否也认为该主服务器下线了。超过一定数量就将主服务器判断为客观下线。

询问其他sentinel是否同意该服务器下线

SENTINEL is-master-down-by-addr <ip><port><current_epoch><runid>

通过SENTINEL is-master-down-by-addr命令询问,参数意义如下图:

image.png

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

其他sentinel接收到SENTINEL is-master-down-by-addr命令后,会根据其中主服务器的ip和端口,检查主服务器是否下线,然后返回包含三个参数的Multi Bulk的回复。 image.png sentinel统计其他sentinel同意主服务器已下线的数量,达到配置的数量后,则将主服务器的flags属性的SRI_O_DOWN标识打开,表示主服务器已经进入客观下线状态。

选举领头sentinel

当一个主服务器被判断成客观下线时,监视这个下线主服务器的各个sentinel就会协商选举一个新的领头sentinel,由这个sentinel进行故障转移操作。

image.png 确认主服务器进入客观下线状态后,会再次发送SENTINEL is-master-down-by-addr命令来选举出领头sentinel。

选举规则

  • 监视同一个主服务器的多个在线sentinel中每一个都可能成为领头sentinel。
  • 每次进行领头sentinel选举之后,无论选举是否成功,所有sentinel的配置纪元(configuration epoch)的值都会自增一次。(配置纪元,其实就是一个计数器)
  • 在一个配置纪元里,所有sentinel都有将某个sentinel设置成局部sentinel的机会,一旦设置在这个配置纪元里就不能再更改。
  • 所有发现主服务器客观下线的sentinel都会要求其他sentinel将自己设置为局部领头sentinel,也就是都会发送SENTINEL is-master-down-by-addr命令,尝试让其他sentinel将自己设置成局部领头sentinel。
  • 当一个sentinel向另一个sentinel发送SENTINEL is-master-down-by-addr命令时,如果runid参数的值不是*,而是源sentinel的runid,就表示要目标sentinel将自己设置成领头sentinel。
  • sentinel设置局部领头的规则是先到先得,第一个设置为局部领头sentinel后,其他的请求都被拒绝。
  • 目标sentinel在接收到一条SENTINEL is-master-down-by-addr命令后,将向源sentinel返回一个命令回复。回复中leader_runid参数和leader_epoch参数分别记录了目标sentinel的局部领头sentinel的runid和配置纪元。
  • 源sentinel接收到回复之后,会比较返回的配置纪元是否和自己的配置纪元相同,如果一样再继续比较返回的局部领头sentinel的runid是否和自己的runid相同,如果一致就表示目标sentinel将自己设置成了局部领头sentinel。
  • 如果某个sentinel被半数以上的sentinel设置成了局部领头sentinel,那么它就成为领头sentinel。
  • 领头sentinel需要半数以上支持,并且每个配置纪元内只能设置一次,那么一个配置纪元里,只会出现一个领头sentinel
  • 如果在一定时限内,每一个sentinel被选举成领头sentinel(没人没获取半数以上选票),那么各个sentinel在一段时间之后再次选举,直到选出领头sentinel

故障转移

故障转移包含以下三个步骤:

  1. 在已下线的主服务器下所有从服务器里,挑选出一个从服务器转换成主服务器。
  2. 让已下线的主服务器属下的所有从服务器改为复制新的主服务器。
  3. 将已经下线的主服务器设置为新服务器的从服务器,旧的主服务器重新上线后,它就成为新的主服务器的从服务器。

选出新的主服务器

已下线的主服务器下所有从服务器里,挑选出一个从服务器,向这个从服务器发送SLAVEOF no one命令,将这个从服务器转换成主服务器。

挑选新主服务器的规则

领头的sentinel会将已下线主服务器的所有从服务器保存到一个列表里面,然后对这个列表进行过滤,挑选出新的主服务器。

  1. 删除列表中所有处于下线或者断线状态的从服务器。
  2. 删除列表中所有最近五秒内没有回复过领头sentinel的INFO命令的从服务器
  3. 删除所有与已下线服务器连接断开超过 dwon-after-milliseconds * 10毫秒的服务器
  4. 然后根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的服务器。
  5. 如果有多个相同最高优先级的从服务器,那么就根据复制偏移量进行排序,选出最大偏移量的从服务器(复制偏移量最大也代表它保存的数据最新)
  6. 如果复制偏移量也相同,那么就根据runid进行排序,选其中runid最小的从服务器

发送slaveof no one 命令之后,领头sentinel会每秒一次向被升级的从服务器发送info命令(平常是每10秒一次),如果返回的回复role从原来的slave变成了master,那么领头sentinel就知道从服务器已经升级成主服务器了。

修改从服务器的复制目标

通过SLAVEOF命令来使从服务器复制新的主服务器。当sentinel监测到旧的主服务器重新上线后,也会发送SLAVEOF命令使它成为新的主服务器的从服务器。

sentinel总结

sentinel其实就是一个监控系统,,而sentinel监测到主服务器下线后,可以通过选举机制选出一个领头的sentinel,然后由这个领头的sentinel将下线主服务器下的从服务器挑选一个切换成主服务器,而不用人工手动切换。

集群

哨兵模式虽然做到了主从自动切换,但是还是只有一台主服务器进行写操作(当然哨兵模式也可以监视多个主服务器,但需要客户端自己实现负载均衡)。官方也提供了自己的方式实现集群。

节点

每个redis服务实例就是一个节点,多个连接的节点组成一个集群。

CLUSTER MEET <ip><port>

向另一个节点发送CLUSTER MEET命令,可以让节点与目标节点进行握手,握手成功就能将该节点加入到当前集群。

启动节点

redis服务器启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器集群模式。

image.png

集群数据结构

每个节点都会使用一个clusterNode结构记录自己的状态,并为集群中其他节点都创建一个相应的clusterNode结构,记录其他节点状态。

typedef struct clusterNode {
    // 创建节点的时间
    mstime_t ctime; 
    // 节点的名称
    char name[CLUSTER_NAMELEN];
    // 节点标识
    // 各种不同的标识值记录节点的角色(比如主节点或从节点)
    // 以及节点目前所处的状态(在线或者下线)
    int flags;     
    // 节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch;
    // 节点的ip地址
    char ip[NET_IP_STR_LEN];  
    // 保存建立连接节点的有关信息
    clusterLink *link;          
    
    list *fail_reports;  
    // ...
} clusterNode;
复制代码

clusterLink保存着连接节点所需的相关信息

typedef struct clusterLink {
    // ...
    // 连接的创建时间
    mstime_t ctime;           
    // 与这个连接相关联的节点,没有就为null
    struct clusterNode *node;   
    // ...
} clusterLink;
复制代码

每个节点还保存着一个clusterState结构,它记录了在当前节点视角下,集群目前所处的状态,例如集群在线还是下线,集群包含多少个节点等等。

typedef struct clusterState {
    // 指向当前节点clusterNode的指针
    clusterNode *myself;  
    // 集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;
    // 集群当前的状态,上线或者下线
    int state;           
    // 集群中至少处理一个槽的节点数量
    int size;      
    // 集群节点的名单(包括myself节点)
    // 字典的键是节点的名字,字典的值为节点对应的clusterNode结构
    dict *nodes; 
} clusterState;
复制代码

CLUSTER MEET 命令的实现

CLUSTER MEET <ip><port>

  1. 节点 A 会为节点 B 创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes 字典里面。
  2. 之后,节点 A 将根据 CLUSTER MEET 命令给定的 IP 地址和端口号,向节点 B 发送一条 MEET 消息。
  3. 如果一切顺利,节点 B 将接收到节点 A 发送的 MEET 消息,节点 B 会为节点 A 创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。
  4. 之后,节点 B 将向节点 A 返回一条 PONG 消息。
  5. 如果一切顺利,节点 A 将接收到节点 B 返回的 PONG 消息,通过这条 PONG 消息节点 A 可以知道节点 B 已经成功地接收到了自己发送的 MEET 消息。
  6. 之后,节点 A 将向节点 B 返回一条 PING 消息。
  7. 如果一切顺利,节点B将接收到节点A返回的PING消息,通过这条PING消息节点B知道节点A已经成功接收到自己返回的PONG消息,握手完成。

image.png

槽指派

集群的整个数据库被分为16384个槽,每个键都属于16384个槽的其中一个,集群中每个节点处理0个或16384个槽。当所有的槽都有节点在处理时,集群处于上线状态,否则就是下线状态。

CLUSTER ADDSLOTS

CLUSTER ADDSLOTS <slot>...
通过CLUSTER ADDSLOTS命令可以将指定槽指派给当前节点负责,例如:CLUSTER ADDSLOTS 0 1 2 3 4 可以将0至4的槽指派给当前节点

记录节点的槽指派信息

clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽:

typedef struct clusterNode {
         
    unsigned char slots[CLUSTER_SLOTS/8];
    
    int numslots;
    // ...
} clusterNode;
复制代码

slots:是一个二进制数组,一共包含16384个二进制位。当二进制位的值是1,代表节点负责处理该槽,如果是0,代表节点不处理该槽 numslots:numslots属性则记录节点负责处理槽的数量,也就是slots中值为1的二进制位的数量。

传播节点的槽指派信息

节点除了会将自己负责的槽记录在clusterNode中,还会将slots数组发送给集群中的其他节点,以此告知其他节点自己目前负责处理哪些槽。

typedef struct clusterState {
    clusterNode *slots[CLUSTER_SLOTS];
} clusterState;
复制代码

slots包含16384个项,每一个数组项都是指向clusterNode的指针,表示被指派给该节点,如果未指派给任何节点,那么指针指向NULL。

CLUSTER ADDSLOTS命令的实现

image.png

在集群中执行命令

客户端向节点发送与数据库有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查该槽是否指派给了自己。
如果指派给了自己,那么该节点直接执行该命令。如果没有,那么该节点会向客户端返回一个MOCED的错误,指引客户端转向正确的节点,并再次发送执行的命令。 image.png

计算键属于那个槽

image.png CRC16(key)是计算出键key的CRC16的校验和,而 & 16383就是取余,算出0-16383之间的整数作为键的槽号。

判断槽是否由当前节点负责处理

计算出键所属的槽号i后,节点就能判断该槽号是否由自己处理。
如果clusterState.slots[i]等于如果clusterState.myself,那么由自己负责该节点可以直接执行命令。
如果不相等,那么可以获取clusterState.slots[i]指向如果clusterNode的ip和端口,向客户端返回MOVED错误,指引客户端转向负责该槽的节点。

集群模式下不会打印MOVED错误,而是直接自动转向。

重新分片

redis集群重新分配可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,相关槽所属的键值对也会从源节点移动到目标节点。
重新分片操作是在线进行的,在重新分片的过程中,集群不用下线,源节点和目标节点都可以继续处理命令请求。 redis集群的重新分片操作是由redis-trib负责执行。重新分片执行步骤如下:

  1. redis-trib对目标节点发送CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,让目标节点准备好从源节点导入槽slot的键值对。
  2. redis-trib对源节点发送CLUSTER SETSLOT <slot> MIGRTING <target_id>命令,让源节点准备好将属于槽slot的键值对迁移至目标节点。
  3. redis-trib向源节点发送CLUSTER GETKEYSINSLOT <slot> <count>命令,获取最多count个属于槽的键值对的键名称。
  4. 对于步骤3获取的每个键名,redis-trib都向源节点发送一个MIGRTING <target_ip> <target_port> <key_name> 0 <timeout>命令,将被选中的键值对从源节点迁移至目标节点。
  5. 重复执行步骤3和步骤4,直到源节点保存的所以属于槽slot的键值对都被迁移至目标节点。
  6. redis-trib向集群中任何一个节点发送CLUSTER SETSLOT <slot> NODE <target_id>命令,将槽指派给目标节点。这一信息最终会通过消息发送至整个集群。

image.png

CLUSTER SETSLOT IMPORTING 命令实现

typedef struct clusterState {
    // ...
    clusterNode *importing_slots_from[CLUSTER_SLOTS];

} clusterState;
复制代码

importing_slots_from记录了当前节点正在从其他节点导入的槽。importing_slots_from[i]不为null,则指向CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,<source_id>所代表的clusterNode结构。

CLUSTER SETSLOT MIGRTING 命令实现

typedef struct clusterState {
    // ...
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];

} clusterState;
复制代码

migrating_slots_to记录了当前节点正在迁移至其他节点的槽。migrating_slots_to[i]不为null,则指向迁移至目标节点所代表的clusterNode结构。

ASK错误

在重新分片期间,源节点向目标节点迁移槽的过程中,可能属于这个槽的一部分键值对一部分保存在源节点当中,而另一部分保存在目标节点当中。
客户端向源节点发送一个与数据库键有关的命令,恰好这个槽正在被迁移。
源节点现在自己的数据库中查找指定的键,如果找到,直接执行。
如果没有找到,节点会检查migrating_slots_to[i]查看键是否正在迁移,如果在迁移就返回一个ask错误,引导客户端转向目标节点。

ASKING

客户端收到ask错误之后,会先执行ASKING命令,再向目标节点发送命令。ASKING命令就是打开发送该命令的客户端的REDIS_ASKING标识。一般来说客户端发送的键如果不属于自己负责会返回MOVED错误(槽只迁移部分,这时槽还不属于目标节点负责),但还会检查importing_slots_from[i],如果显示节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING标识,那么它就会破例执行一次该命令。 image.png

集群的故障转移

集群的故障转移效果和哨兵模式类似,也是将从节点升级成主节点。旧的主节点重新上线后将会成为新主节点的从节点。

故障检测

集群中每个节点会定期的向集群中其他节点发送PING消息,检测对方是否在线,如果指定时间内没有收到PONG消息,那么就将该节点标记为疑似下线。clusterState.nodes字典中找到该节点的clusterNode结构,将flags属性修改成REDIS_NODE_PFAIL标识。
集群中各个节点会互相发送消息来交换集群中各个节点的状态,例如:主节点A得知主节点B认为主节点C进入了疑似下线状态,主节点A会在clusterState.nodes字典中找到节点C的clusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports链表当中。
每一个下线报告由一个clusterNodeFailReport结构表示

typedef struct clusterNodeFailReport {
    struct clusterNode *node; 
    // 最后一次收到下线报告的时间
    mstime_t time;            
} clusterNodeFailReport;
复制代码

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

故障转移

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

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

选举新的主节点

新的主节点通过选举产生

  1. 集群的配置纪元是一个自增计数器,它的初始值为0。
  2. 当集群的某个节点开始一次故障转移操作,集群的配置纪元的值加1。
  3. 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,第一个想主节点要求投票的从节点将获得主节点的投票。
  4. 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息,并具有投票权的主节点向这个从节点投票。
  5. 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
  6. 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
  7. 如果集群里有 N 个具有投票权的主节点,那么当一个从节点收集到大于等于 N / 2 + l 张支持票时,这个从节点就会当选为新的主节点。
  8. 因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有 N 个主节点进行投票,那么具有大于等于 N / 2 + l 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
  9. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进人一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

主节点选举的过程和选举领头sentinel的过程非常相似。

数据丢失

主从复制数据丢失

主从复制之间是异步执行的,有可能master的部分数据还没来得及同步到从数据库,然后master就挂了,这时这部分未同步的数据就丢失了。

脑裂

脑裂就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着。此时哨兵可能就会认为master 宕机了,然后开启选举,将其他slave切换成了master,这个时候,集群里面就会有2个master,也就是所谓的脑裂。
此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续向旧master的写数据。
master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据将会清空,重新从新的master复制数据,导致数据丢失。

减少数据丢失的配置

min-slaves-to-writ 1
min-slaves-max-lag 10
复制代码

上述配置表示,如果至少有1个从服务器超过10秒没有给自己ack消息,那么master不再执行写请求。

主从数据不一致

当从数据库因为网络原因或者执行复杂度高命令阻塞导致滞后执行同步命令,导致数据同步延迟,造成了主从数据库不一致。

都看到这了,点个赞再走了吧:)

おすすめ

転載: juejin.im/post/7049625997011845150
おすすめ