1 Redis高可用常见的有三种方式:
-
主从复制
-
哨兵模式
-
Redis集群
2 复制
在Redis中,可以通过执行SLAVEOF命令或者设置slaveof
选项,让从服务器来备份主服务器上的数据。 Redis的复制功能主要分为同步和命令传播。 同步主要是指从服务器的状态更新为主服务器的状态。同步具体又细分为完整重同步和部分重同步(Redis 2.8以后的版本才有)。
2.1 完整重同步
一般是从服务器向主服务器发送命令请求,主服务向从服务器发送RDB文件及缓冲区保存的写命令,从服务器根据RDB文件和缓冲区保存的写命令恢复数据库状态。 具体步骤如下:
-
从服务器向主服务器发送SYNC命令。
-
收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
-
当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
-
主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。
如下图所示:
2.2 命令传播
在同步操作执行完毕之后的这个时间点,主从服务器两者的数据库将达到一致状态。但之后当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改,并导致主从服务器状态不再一致,所以命令传播就是主服务器执行一条写命令后,也会把这条写命令发送给从服务器执行。
2.3 部分重同步(Redis2.8以后版本才有)
对于从服务器初次复制的场景来说,完整重同步+命令传播已经可以完美得满足需求了,但是如果从服务器是断线后重复制,因为从服务器本身拥有主服务器的数据,只是缺少断线期间的数据修改,采用完整重同步+命令传播会效率比较低。所以就有了部分重同步只会向从服务器发送断线期间的写命令。
部分重同步的实现由三部分组成,复制偏移量,复制积压缓冲区,服务器运行ID。
-
复制偏移量 指的是执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量: ·主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。 ·从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。 这样通过对比主从服务器的复制偏移量可以知道主从服务器目前的数据状态。
-
复制积压缓冲区 复制积压缓冲区是由主服务器维护的一个固定长度先进先出队列,保存了主服务器执行的写命令。默认大小为1MB。队列长度是固定的,当元素满了时,会将最先入队的元素弹出,再将新元素放入队列。
-
服务器运行ID 每个Redis服务器,不论主服务器还是从服务,都会有自己的运行ID。当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。
当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID: ·如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作。 ·相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前复制的主服务“·相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作。
2.4 PSYNC命令的实现
PSYNC有两种模式,可以进行完整重同步和部分重同步。从服务器在开始一次新的复制时将向主服务器发送 PSYNC ? -1
命令,主动请求主服务器进行完整重同步。如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送 PSYNC <runid> offset
命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作。具体流程图如下:
2.5 复制的实现
向从服务发送SLAVEOF
命令,可以让一个从服务器复制主服务。 例如:向从服务器发送下面的命令,复制地址为127.0.0.1,端口为6379的主服务器的数据。
SLAVEOF 127.0.0.1 6379
实现步骤如下:
- 在从服务redisServer的设置主服务器的地址masterhost和端口masterport
struct redisServer {
// 主服务器的地址
char *masterhost;
// 主服务器的端口
int masterport;
};
-
建立Socket连接
-
发送PING命令
-
身份验证
-
发送端口信息
-
同步
-
命令传播
-
心跳检测,检测主从服务器的网络连接状态,“辅助实现min-slaves配置选项,检测命令丢失。
3 哨兵
哨兵系统指的是由一个或多个哨兵实例组成的哨兵系统可以监视任意多个主服务器及属下的所有从服务器,在被监视的主服务器进入下线状态时,自动将某个从服务器升级为主服务器,并且由新的主服务代替旧的主服务器继续处理命令请求。
3.1 启动哨兵服务器
哨兵服务器本质上搜索一个运行在特殊模式下的Redis服务器,哨兵服务器启动的实现主要分为三步:
-
初始化服务器。与普通Redis服务器不同的是,初始化时不需要通过载入RDB文件或者AOF文件还原数据库状态。下图为哨兵服务器的主要功能:
-
使用哨兵专用代码。也就是将一部分普通Redis服务器使用的代码替换为哨兵服务器使用的代码。例如:普通Redis服务器使用redis.c/redisCommandTable作为服务器的命令表,因为SET,DBSIZE等很多命令哨兵服务器不会执行,所以哨兵服务器使用sentinel.c/sentinelcmds作为服务器的命令表,并且其中的INFO命令会使用Sentinel模式下的专用实现sentinel.c/sentinelInfoCommand函数,而不是普通Redis服务器使用的实现redis.c/infoCommand函数。
-
初始化哨兵服务器的状态。普通服务器的一般状态仍然由redis.h/redisServer结构保存,哨兵服务器会初始化一个sentinel.c/sentinelState结构,这个结构保存了服务器中所有和Sentinel功能有关的状态。
struct sentinelState {
// 当前纪元,用于实现故障转移
uint64_t current_epoch;
// 保存了所有被这个哨兵服务器监视的主服务器
// 字典的键是主服务器的名字
// 字典的值则是一个指向sentinelRedisInstance结构的指针
dict *masters;
// 是否进入了TILT模式?
int tilt;
// 目前正在执行的脚本的数量
int running_scripts;
// 进入TILT模式的时间
mstime_t tilt_start_time;
// 最后一次执行时间处理器的时间
mstime_t previous_time;
// 一个FIFO队列,包含了所有需要执行的用户脚本
list *scripts_queue;
} sentinel;
保存了被监视的主服务器信息的sentinelRedisInstance结构
typedef struct sentinelRedisInstance {
// 标识值,记录了实例的类型,以及该实例的当前状态
int flags;
// 实例的名字
// 主服务器的名字由用户在配置文件中设置
// 从服务器以及Sentinel的名字由Sentinel自动设置
// 格式为ip:port,例如"127.0.0.1:26379"
char *name;
// 实例的运行ID
char *runid;
// 配置纪元,用于实现故障转移
uint64_t config_epoch;
// 实例的地址
sentinelAddr *addr;
// SENTINEL down-after-milliseconds选项设定的值
// 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
mstime_t down_after_period;
// SENTINEL monitor <master-name> <IP> <port> <quorum>
// 选项中的quorum参数
// 判断这个实例为客观下线(objectively down)所需的支持投票数量
int quorum;
// SENTINEL parallel-syncs <master-name> <number>选项的值
// 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs;
// SENTINEL failover-timeout <master-name> <ms>选项的值
// 刷新故障迁移状态的最大时限
mstime_t failover_timeout;
// ...
} sentinelRedisInstance;
- 创建连向主服务器的网络连接。哨兵服务器会创建两个连向被监视的主服务器的异步网络连接,一个命令连接,用于向主服务器发送命令,并接受命令回复。另一个是订阅连接,用于订阅主服务器的sentinel:hello频道。通过这个频道,哨兵服务器可以通过主服务器了解其他哨兵服务器,从服务器等信息。
3.2 获取主服务器信息
哨兵服务器会以每十秒一次的频率,通过命令链接向被监视的主服务器发送INFO命令,并根据回复来获取主服务器的当前信息。 能获取到的信息如下:
-
主服务器的相关信息(运行ID,角色等)
-
主服务器下属的所有从服务器的信息(服务器的角色,IP,端口,在线状态等) 根据这些信息,哨兵对象可以更新自身的sentinelRedisInstance结构中的主服务器和从服务器信息。(可以自动发现从服务器的信息)
3.3 获取从服务器信息
当哨兵对象发现新的从服务器出现时, 会为它创建实例结构,而且会创建连接到从服务器的命令连接和订阅连接。并且会以每十秒一次的频率通过命令连接向从服务器发送INFO命令,获取从服务器及它所属的主服务器的信息。主要获得的信息如下:
-
从服务器的运行ID,角色role。
-
主服务器的IP地址,端口号master_port,主从服务器的连接状态,从服务器的优先级slave_priority,从服务器的复制偏移量slave_repl_offset。
下图展示了根据上面的INFO命令回复对从服务器的实例结构进行更新之后的情况:
3.4 接收来自主从服务器的频道消息
在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送PUBLISH的命令,格式如下:
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>
参数包括哨兵服务器的IP,端口,运行ID,配置纪元。主服务器的名字,IP,端口,配置纪元。 以下是一条Sentinel通过PUBLISH命令向主服务器发送的信息示例:
"127.0.0.1,26379,e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,0,mymaster,127.0.0.1,6379,0
这个示例包含了以下信息: Sentinel的IP地址为127.0.0.1端口号为26379,运行ID为e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,当前的配置纪元为0。 ·主服务器的名字为mymaster,IP地址为127.0.0.1,端口号为6379,当前的配置纪元为0。
当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令:
SUBSCRIBE __sentinel__:hello
Sentinel对sentinel:hello频道的订阅会一直持续到Sentinel与服务器的连接断开为止。对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的sentinel:hello频道发送信息,又通过订阅连接从服务器的sentinel:hello频道接收信息。当sentinel1向服务器的sentinel:hello频道发送一条信息时,所有订阅了sentinel:hello频道的Sentinel(包括sentinel1自己在内)都会收到这条信息,然后对相应主服务器的实例结构进行更新。
Sentinel为主服务器创建的实例结构中的sentinels字典保存了除Sentinel本身之外,还保存了所有同样监视这个主服务器的其他Sentinel的信息。当一个Sentinel收到其他Sentinel发来的信息时,会对消息解析,更新sentinels字典。
·sentinels字典的键是其中一个Sentinel的名字,格式为ip:port,比如对于IP地址为127.0.0.1,端口号为26379的Sentinel来说,这个Sentinel在sentinels字典中的键就是"127.0.0.1:26379"。 ·sentinels字典的值则是键所对应Sentinel的实例结构,比如对于键"127.0.0.1:26379"来说,这个键在sentinels字典中的值就是IP为127.0.0.1,端口号为26379的Sentinel的实例结构。
3.5 创建连向其他Sentinel的命令连接
当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:Sentinel A有连向Sentinel B的命令连接,而Sentinel B也有连向Sentinel A的命令连接,如下图所示:
Sentinel之间不会创建订阅连接,Sentinel在连接主服务器或者从服务器时,会同时创建命令连接和订阅连接,但是在连接其他Sentinel时,却只会创建命令连接,而不创建订阅连接。因为Sentinel需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新Sentinel,所以才需要建立订阅连接,而相互已知的Sentinel只要使用命令连接来进行通信就足够了。
3.6 检测主观下线状态
在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。
-
有效回复:实例返回+PONG、-LOADING、-MASTERDOWN三种回复的其中一种。
-
无效回复:实例返回除+PONG、-LOADING、-MASTERDOWN三种回复之外的其他回复,或者在指定时限内没有返回任何回复。
Sentinel配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进入主观下线所需的时间长度:如果一个实例在down-after-milliseconds毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构,在结构的flags属性中打开SRI_S_DOWN标识,以此来表示这个实例已经进入主观下线状态。
3.7 检测客观下线状态
当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel发送SENTINEL is-master-down-by-addr
命令,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。 客观下线状态的判断条件 Sentinel配置中有一个quorum属性,当有quorum个数的Sentinel认为主服务进入下线状态时,Sentinel便将主服务器判定位客户下线。(每个Sentinel的quorum可以不同)。
3.8 选举领头Sentinel
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。 以下是Redis选举领头Sentinel的规则和方法:
-
每个Sentinel(源Sentinel)向另一个Sentinel(目标Sentinel)发送SENTINEL is-master-down-by-addr命令,要求后者将前者设置为局部领头Sentinel,每个Sentinel设置局部领头Sentinel规则都是先到先得,最先向目标Sentinel发送设置要求的源Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝。
-
局部领头一旦设置,目标Sentinel会将配置纪元+1,并且给源Sentinel回复局部领头Sentinel的运行ID和配置纪元。
-
源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一致,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel。
-
如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。
-
如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。
3.9 故障转移
在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤:
-
在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。 挑选规则:
1)删除列表中所有处于下线或者断线状态的从服务器
2)删除列表中所有最近五秒内没有回复过领头Sentinel的INFO命令的从服务器
3)删除所有与已下线主服务器连接断开超过
down-after-milliseconds
10毫秒的从服务器:down-after-milliseconds
选项指定了判断主服务器下线所需的时间,而删除断开时长超过down-after-milliseconds
10毫秒的从服务器,则可以保证列表中剩余的从服务器都没有过早地与主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的。 之后,领头Sentinel将根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器。 -
领头Sentinel向从服务器发送
SLAVEOF
命令,让已下线主服务器属下的所有从服务器改为复制新的主服务器。 3.当这个旧的主服务器重新上线时,领头Sentinel向它发送SLAVEOF
命令将已下线主服务器设置为新的主服务器的从服务器。
4 集群
Redis集群是一个分布式数据库方案,可以通过分片来进行数据共享,并提供复制和故障转移功能。
4.1节点
节点只是一个运行在集群模式下的Redis服务器,由启动时配置中的cluster-enabled属性决定。节点通过cluster meet
命令加入集群。
4.2 集群数据结构
使用clusterNode结构可以保存了一个节点的当前状态,例如创建时间,名字,配置纪元,IP,端口等。
struct clusterNode{
//创建节点的时间
mstime_t ctime;
//节点的名字,由40个十六进制字符组成
char name[REDIS_CLUSTER_NAMELEN];
//节点标识
//使用各种不同的标识值记录节点的角色(比如主节点或者从节点)
//以及节点目前所处的状态(比如在线或者下线)
int flags;
//当前节点的配置纪元,用于实现故障转移
uint64_t configEpoch;
//节点的IP地址
char ip[REDIS_IP_STR_LEN];
//节点的端口号
int port;
//保存连接节点所需的有关信息
clusterLink *link;
//...
}
clusterNode结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区等。(有点类似于redisClient结构中用于连接客户端的套接字,缓冲区)
typedef struct clusterLink{
//连接的创建时间
mstime_t ctime;
//TCP套接字描述符
int fd;
//输出缓冲区,保存着等待发送给其他节点的消息。
sds sndbuf;
//输入缓存区,保存着从其他节点接收到的消息。
sds rcvbuf;
//与这个连接相关联的节点,如果没有的话就为NULL
struct clusterNode *node;
}clusterLink;
每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元等。
typedef struct clusterState{
//指向当前节点的指针
clusterNode *myselt;
//集群当前的配置纪元,用于实现故障转移。
uint64_t currentEpoch;
//集群当前的状态,是在线还是下线。
int state;
//集群中至少处理着一个的槽的节点的数量
int size;
//集群节点名单(包括myself节点)
//字典的键为节点的名字,字典的值为节点对应的clusterNode结构
dict *nodes;
}clusterState;
4.3 槽指派
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),分配给每个节点处理,数据库中的每个键都属于这16384个槽的其中一个。可以使用CLUSTER ADDSLOTS
命令对槽进行分配。
clusterNode结构中的slots属性和numslot属性记录了节点处理哪些槽,numslot属性记录了当前节点处理的槽的数量,slots属性是一个二进制位数组,一共有16384位,数组第i位上的值代表节点是否处理槽i。
struct clusterNode{
//...
unsigned char slots[16384/8];//每个char是8个
int numslots;
//...
}
4.4 记录集群所有槽的指派信息
除了记录自身节点负责的槽位信息以外,clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息。
struct clusterState{
//...
clusterNode *slots[16384/8];
//...
}
slots数组包含16384个项,每个数组项都是一个指向对应的槽所在的clusterNode结构的指针,如果slots[i]指针指向NULL,那么表示槽i尚未指派给任何节点。 clusterNode.slots 与 clusterState.slots 如果只有clusterNode.slots ,想要知道某个槽被指派给哪个节点,需要以O(N)的复杂度对clusterState.Nodes字典中每个节点的slots数组进行遍历,而通过clusterState.slots查找只需要O(1)复杂度。 如果只有clusterState.slots ,想要将某个节点的槽指派信息发送给其他节点,需要以O(N)的复杂度对clusterState.slots数组进行遍历,而通过clusterState.slots可以直接发送。
4.5 在集群中执行命令
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。 当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽所在的节点,如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令。如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED
错误,指引客户端转向(redirect)至正确的节点,则客户端会向正确的节点再次发送之前想要执行的命令,并且之后这个槽的对应的键也会直接往正确的节点发送。
4.6 计算键属于哪个槽
节点使用以下算法来计算给定键key属于哪个槽
def slot_number(key):
return CRC16(key) & 16383
其中CRC16(key)语句用于计算键key的CRC-16校验和,而&16383语句则用于计算出一个介于0至16383之间的整数作为键key的槽号。
4.7 节点数据库的实现
集群节点保存键值对以及键值对过期时间的方式,与的单机Redis服务器保存键值对以及键值对过期时间的方式完全相同。节点和单机服务器在数据库方面的一个区别是,节点只能使用0号数据库。 除了将键值对保存在数据库里面之外,节点还会用clusterState结构中的slots_to_keys跳跃表来保存槽和键之间的关系。slots_to_keys跳跃表每个节点的分值保存键对应的槽位,每个节点的成员都是一个数据库键,如下图所示:
4.8 重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。 重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。 重新分片的实现原理 Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。 redis-trib对集群的单个槽slot进行重新分片的步骤如下:
- redis-trib对目标节点发送
CLUSTER SETSLOTIMPORTING<source_id>
命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。 - redis-trib对源节点发送
CLUSTER SETSLOTMIGRATING<target_id>
命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点。 - redis-trib向源节点发送
CLUSTER GETKEYSINSLOT
命令,获得最多count个属于槽slot的键值对的键名(key name)。 - 对于步骤3获得的每个键名,redis-trib都向源节点发送一个
MIGRATE <target_ip> <target_port><key_name> 0 <timeout>
命令,将被选中的键原子地从源节点迁移至目标节点。 - 重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。每次迁移键的过程如图
- redis-trib向集群中的任意一个节点发送
CLUSTER SETSLOTNODE<target_id>
命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。
4.9 槽迁移相关
typedef struct clusterState {
clusterNode *importing_slots_from[16384];
clusterNode *migrating_slots_to[16384];
} clusterState;
clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽,如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在从clusterNode所代表的节点导入槽i。同理migrating_slots_to数组记录了当前节点正在导出到其他节点的槽。 当对集群进行重新分片时,源节点会对migrating_slots_to数组进行更新,目标节点会对importing_slots_from数组进行更新。
4.10 ASK错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。 当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时: 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点。在向目标节点发送想要执行的命令之前,需要先发送ASKING命令,将节点连接的客户端的REDIS_ASKING标识打开,否则目标节点不会对正在迁移的槽执行相关的命令。(客户端的REDIS_ASKING标识是一个一次性标识,当节点执行了一个带有REDIS_ASKING标识的客户端发送的命令之后,客户端的REDIS_ASKING标识就会被移除。
ASK错误和MOVED错误的区别 ASK错误和MOVED错误都会导致客户端转向,它们的区别在于:
-
MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
-
与此相反,ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现。
4.11复制和故障转移
Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
4.12 故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail,PFAIL)。 集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息, 例如某个节点是处于在线状态、疑似下线状态(PFAIL),还是已下线状态。 一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到主节点C所对应的clusterNode结构,并将主节点B的下线报告(failure report)添加到clusterNode结构的fail_reports链表里面,半数以上的主节点将一个C节点标记为疑似下线时,C节点就会被标记为已下线状态,然后向集群广播一天FAIL消息。
4.13 故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
- 复制下线主节点的所有从节点里面,会有一个从节点被选中。
- 被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。
- 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
- 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
- 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
4.14 选举新的主节点
新的主节点是通过选举产生的,这个选举新主节点的方法和选举领头Sentinel的方法非常相似,因为两者都是基于Raft算法的领头选举(leader election)方法来实现的。 以下是集群选举新的主节点的方法:
- 集群的配置纪元是一个自增计数器,它的初始值为0。
- 当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
- 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
- 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。 - 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条
CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,表示这个主节点支持从节点成为新的主节点。 - 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
- 如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
- 因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
- 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
4.15 消息
集群中的各个节点通过发送和接收消息(message)来进行通信, 节点发送的消息主要有以下五种:
- MEET消息: 当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
- PING消息 集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,“距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
- PONG消息 当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识,例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条PONG“消息,以此来让集群中的其他节点立即知道这个节点已经变成了主节点,并且接管了已下线节点负责的槽。
- FAIL消息 当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
- PUBLISH消息 当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。
网友评论