一、主从
Redis服务器可以通过SLAVEOF命令来让自己变为从服务器,跟随一个主服务器,从服务器会同步主服务器上的数据,这就是Redis的主从复制功能。
Redis的主从复制功能主要分为同步(psync)和命令传播(command propagate)两个操作:
- 同步用于将从服务器的数据库状态更新到和主服务器相同,psync同步分为完整重同步和部分重同步两种。
- 命令传播用于在主服务器的数据库状态被修改,导致主从不一致的时候,用来使两者重新一致。
同步用于一个新的从服务器跟随一个主服务器,这是从服务器上没有任何数据,这时候完全重同步主服务器上的数据。当同步完成后,后面就可以通过命令传播来保持一致。如果一台本来就跟随主服务器的从服务器宕机了一小会,此时重连以后,使用部分重同步。
1.1 同步:
完整重同步:
当一个新的slave(从机)跟随一个master(主机)时,slave需要同步master的数据,步骤如下:
- slave通过salveof命令,跟随一个主机
- slave向master发送psync命令
- master收到psync命令后,发现复制偏移量相差较大,执行bgsave备份,使用子进程生成一个RDB文件,并同时使用一个缓冲区记录从开始备份到现在所执行的所有写命令
- master执行完bgsave命令后,将RDB文件发送给slave
- slave接收并载入RDB文件,执行RDB文件恢复,将自己的数据库更新为master的数据库某个时刻的状态
- master将记录在缓冲区中的所有写命令发送到slave
- slave接收这些命令,然后执行,让自己的数据库状态和master保持一致。
- 接下来,使用命令传播继续保持一致
部分重同步:master和slave都维护一个复制偏移量,master每次向slave传播N个字节的数据后,就将自己的复制偏移量加N,slave每次收到主服务器传播来的N个字节的数据后,执行完毕就将自己的复制偏移量加N。
这样,可以很轻松的判断master和slave是否处于一致状态。
当一个slave重连后,向master发送psync命令,master发现复制偏移量相差不多,则使用部分重同步。
master在命令传播时,不仅将命令发送给所有的slave,还会将命令写入复制积压缓冲区,这是一个先进先出的队列,容量为1MB。这个缓冲区记录了对应的命令和master执行这条命令时master的复制偏移量。当slave发送来psync命令后,读出slave的复制偏移量,和复制积压缓冲区中的对比,如果slave的复制偏移量(假设为offset)之后的第一条命令(offset+1)还在缓冲区内,则从这条记录开始,将之后的命令发送给slave。如果不在,则执行完全重同步。
1.2 命令传播:
master每执行一条命令都将其发送给所有从机。
这是因为Redis的每台服务器都有一个唯一ID,slave跟随master进行第一次同步时,master会将自己的ID发送给slave。
slave断线重连后,会向当前的master发送自己之前保存的masterID,如果一致,则说明slave断线之前就连接的该master,继续部分重同步或者完全重同步。如果不一致,则master直接对slave进行完全重同步。
1.3 psync命令:
当slave第一次连接master时,向master发送psync ? -1,主动要求完全重同步。
如果是重连的,则向master发送psync < runid> < offset>,runidI表示之前的master ID,offset表示之前的复制偏移量。
master如果回复:
- FULLRESYNC < runid> < offset>则表示对slave进行完全重同步,runid为master ID,offset为master的复制偏移量,其作为slave的复制偏移量新的初值。
- CONTINUE,则表示进行部分重同步。
1.4 具体步骤
- slave使用slaveof命令设置master的ip和端口,master的ip地址和端口号会保存到redisServer的masterhost属性和masterport属性中。
//命令示例:127.0.0.1:12345>slaveof 127.0.0.1 6379
struct redisServer{
char *masterhost;
int masterport;
}
- slave根据masterhost属性和masterport属性,向master发起套接字连接,如果连接成功,那么处理器会为该套接字关联一个专门用于处理复制工作的文件事件处理器。master接受slave的套接字连接以后,会将slave看作一个连接到master的客户端对待。
- slave连接成功后,向master发送ping命令,检查master是否正常。 master如果没有问题,则会回复一个pong
- 接下来进行身份验证,slave根据自己有无设置masterauth选项来选择是否进行身份验证,如果要进行身份验证,则向master发送一条AUTH命令,参数为masterauth的值。如果不进行身份验证,则不会发送AUTH。master如果设置requirepass选项,表示master需要slave进行验证,如果没设置则不需要验证。
如果master没有设置requirepass,slave没有设置masterauth,则可以继续正常进行。
如果master设置了requirepass,slave也设置了masterauth,且二者一样,则正常进行,如果不一致,则返回invalid password错误。
如果master没设置,slave设置了,master返回no password is set错误
如果master设置了,slave没设置,则返回noauth错误 - 如果验证通过,则slave向master发送slave监听的端口号,master会将其记录到clients对应该slave的表项的slave_listening_port属性中(slave在master看来,就是一个客户端,所以slave状态会记录到clients数组中)
- slave向master发送psync进行同步。
- master和slave进入命令传播模式。这地方有个小小疑问,master是如何区分slave和普通客户端的呢?猜测可能redisClient还维护了一个标志,用来标志这个客户端实际上是一个slave,进行命令传播的时候会遍历所有客户端,将命名发送给标记是slave的客户端。
- 进入命令传播阶段后,slave会默认每秒一次向master发送心跳命令,即
replconf ACK < replication_offset>,replication_offset为slave当前的复制偏移量。心跳可以检测主从服务器的连接状态,辅助实现min-slaves选项,还能检测命令丢失。
二、哨兵(Sentinel)
哨兵:Redis高可用的解决方案之一,由一个或多个redis服务器变为特殊的sentinel服务器来监视多个主服务器,以及跟随这些主服务器的从服务器。当监听的某个主服务器断线后,将这台主服务器的某个从服务器升级为主服务器,由这台新的主服务器代替原来的那一台主服务器提供服务。
整体流程:
-
启动并初始化Sentinel
使用redis-sentinel < 配置文件路径>或者redis-server < 配置文件路径> --sentinel命令,启动一台sentinel服务器。首先,初始化服务器(Sentinel本质是一台运行在特殊模式下的redis服务器),将普通redis服务器的代码替换为sentinel专用代码(不载入redis的普通命令表,而是载入sentinel特有的命令),初始sentinel状态,根据配置文件,初始化sentinel监听的主服务器列表(通过一个字典来保存被监听的master的信息),最后创建连向被监听的主服务器的网络连接(一个命令连接和一个订阅连接)。 -
获取主服务器信息
Sentinel默认会以每十秒一次的频率,通过命令连接向所有master发送INFO指令,获取master的ID及master的slave等信息 -
获取从服务器信息
Sentinel从主机获取到从机信息后,会检查Sentinel中保存主机信息的数据结构的slaves属性,检查是否有该从机的状态信息,如果没有,新建,有则更新。
Sentinel除了更新对应的数据结构外,也会向从机建立网络连接,并每十秒接收一次从机的INFO,
-
向主服务器和从服务器发送消息
Sentinel除了10s/次的INFO命令外还会以2s/次的频率,向所有被监视的master和slave发送publish命令,将sentinel自己的信息及它监视的主机的信息发送到服务器的订阅频道上。 -
接收来自主服务器和从服务器的频道消息
当一个Sentinel将自己的信息和监听主机的信息发送到某个服务器的某个频道上后,redis服务器会将这些信息广播到所有订阅该频道的客户端(所有订阅这个频道的sentinel在这里相当于该redis服务器的客户端),所以所有监视该服务器的sentinel都会接收到该消息(包括自己)。(redis服务器相当于是一个桥梁,用于sentinel集群的消息传递) -
更新sentinels字典
Sentinel会创建一个字典来保存其他sentinel的状态,当sentinel和redis服务器的订阅连接有消息时,会接收到该消息,这条消息包含其他监听这台服务器的sentinel状态或者这条消息来自自己(因为自己也订阅了该频道,所以自己通过命令连接向redis发送了信息后,redis广播时也会给发送这消息的sentinel也发送一份)。如果sentinel的ip和ID与自己的相同,则这条消息不被处理,如果是别的sentinel的信息,则更新自己的sentinels字典中对应sentinel状态。当sentinel通过频道消息发现一个新的sentinel时,即字典中没有保存该sentinel的信息,则会在字典中创建对应sentinel的实例结构,还会创建一个连接新sentinel的命令连接。最终使得监听同一个master的sentinel形成一个相互连接的网络。
-
检测主观下线状态
Sentinel会以默认1s一次的频率向所有与它创建了命令连接的服务器(包括master,slave,其他的sentinel)发送PING命令,来判断对应实例是否还在线。当对应的实例超过一定时间没有回复或者回复错误,则该sentinel会判断该实例主观下线。(时间范围可以通过down-after-milliseconds设置) -
检查客观下线状态
当某一个Sentinel判断某实例主观下线后,会向其他监听该服务器的sentinel进行询问,如果它们中有足够数量的认为该实例下线了(配置文件中quorum参数),则sentinel就会将该服务器判定为客观下线,然后进行故障转移操作。 -
当某服务器被sentinel判断为主观下线后,所有监听该服务器的sentinel会进行选举,选举一个主sentinel进行故障转移操作。
故障转移:
- 在已下线master属下的所有从服务器里面,挑选一个从服务器,将其转换为主服务器。
- 让已下线主服务器的所有从服务器改为复制新的主服务器
- 将已下线主机设置为新主服务器的从服务器,当旧主重连后,会成为新主的从服务器。
有个小小疑问记录在这里: 如果某个哨兵被判断下线了会发生什么呢?故障转移还是?
哨兵挂掉了,应该不会进行任何处理。每个哨兵都有特殊的标记,表示自己是个哨兵,所以当哨兵没有回复pong的时候,发送ping的哨兵可能会询问其他哨兵,如果这个哨兵确实断线了,可能后面就将其移除哨兵集群了(sentinels字典中找到指向断线sentinel的指针,然后将其对应的数据结构的状态设置为断线),等待其重新上线。
三、集群
集群是Redis提供的分布式数据库方案,集群通过分片来进行数据存储,并提供复制和故障转移功能。
Redis集群一般由很多个节点组成,节点之间通过握手完成连接。
握手操作:使用CLUSTER MEET < ip> < port>,可以让节点与ip和port所指定的节点进行握手,握手成功,ip所指向的节点就会加入当前节点所在的集群。
//EG:
/*
127.0.0.1:7000>cluster meet 127.0.0.1:7001
命令执行成功后,就可以将127.0.0.1:7001所表示redis服务器进程加入到127.0.0.1:7000所在的集群
*/
当开启集群模式后(配置文件的cluster-enabled选项为true),节点除了使用redisServer结构体来保存服务器的状态,redisClient结构体来保存客户端的状态以外,会额外使用clusterNode、clusterLink、clusterState结构体来保存集群相关的数据。
//保存节点的当前状态
struct clusterNode{
mstime_t ctime;//节点创建的时间(该服务器加入集群的时间)
char name[REDIS_CLUSTER_NAMELEN];//节点的名字,由40个16进制字符组成
int flags;//节点标识,使用不同的值表示集群中不同的角色(例如master、slave)以及节点状态(上线/下线)
uint64_t configEpoch;//节点当前的纪元,用于实现故障转移,当master宕机后,会选择纪元最大,且复制偏移量最大的slave当新的主节点
char ip[REDIS_IP_STR_LEN];//节点的ip地址
int port;
clisterLink *link;//保存该节点连接集群其他节点所需要的信息
//...其他属性
};
//保存连接集群其他节点所需的相关信息
struct clusterLink{
mstime_t ctime;//连接创建时间
int fd;//TCP套接字描述符
sds sndbuf;//输出缓冲区,保存待发送到其他节点的消息
sds rcvbuf;//输入缓冲区,保存从其他节点接受到的消息
struct clusterNode *node;//与这个连接相关的节点,如果没有就是Null,表示目前没有连接节点
}clusterLink;
//记录在当前节点的视角下,集群目前的状态
struct clusterState{
clusterNode *myself;//指向当前节点的指针
uint64_t currentEpoch;//集群当前的纪元,用于实现故障转移
int state;//集群当前的状态,在线/下线
int size;//集群至少处理着一个一个槽的节点数量(即集群中master的数量,每个master负责一部分槽)
dict *nodes;//集群节点名单,键为节点名字,值为对应的clusterNode
//...
}clusterState;
3.1 槽指派:
Redis集群通过分片的方式来存储数据库中的数据,所以将整个集群所代表的数据库分为16384个槽,数据库中的每个键都属于这16384个槽中的一个,集群中的每个节点负责处理0个或多个槽(最多16384,即集群中只有一个节点)。当集群的所有槽都在被处理,那么集群处于在线状态,如果有一个槽没有得到处理,那么集群就处于下线状态。
可以通过使用CLUSTER ADDSLOTS < slot>[slot …]来将一个或多个槽指派给节点负责。
/*
EG:
127.0.0.1:7000>cluster addslots 0 1 2 3 ... 5000
表示将0-5000这5000个槽交给127.0.0.1:7000这个redis服务器来负责
*/
//同时custerNode结构体会记录该节点所负责的槽有哪些
struct clusterNode{
//...
//slots为一个二进制位数组,含有16384个二进制位,从0-16383为索引,如果对应的索引上的二进制位为1,
//表示该槽由本节点负责,为0表示本节点不负责该槽,前面的127.0.0.1:7000的示例中
//表示127.0.0.1:7000的clusterNode结构体中该数组的0-4999位全部为1
unsigned char slots[16384/8];
int numslots;//记录本节点负责处理的槽的数量
//...
}
/*
当为某节点分配好槽之后,该节点将自己负责的槽记录到clusterNode的slots属性之后,还会将自己的slots数组发送到集群的其他节点上,其他节点收到该slots数组后,首先在clusterState结构体的字典中找到发送该slots数组的节点的clusterNode,然后将slots更新到该clusterNode的slots属性中去。
*/
//同时clusterState不仅记录了每个节点负责的槽,还记录了整个集群的槽分配情况
struct clusterState{
//..
//包含16384个项,记录集群中的每个槽分配给了哪个节点,如果slots[i]指向NULL,表示该槽未分配
//如果指向一个clusterNode,则表示该槽分配给了这个clusterNode所代表的节点
clusterNode *slots[16384];
}
这时会有一个问题,即我们给某些节点指派槽的时候可能不知道槽的分配情况,可能将某个已分配的槽再重新分配给某个节点,这时会发生什么呢?
此时Redis会返回错误,addslots命令在执行时,会遍历clusterState的slots数组,如果要分配的某个槽已经分配过了,就返回错误,如果都没有分配,再重新遍历,将这些槽指派给当前节点,将clusterState的slots数组中对应分配槽的指针指向本节点的clusterNode,同时将本节点的clusterNode中的slots数组中对应的二进制置为1,并且设置numslots属性。
3.2 提供服务:
槽分配完毕,集群处于上线状态,就可以提供服务了。
客户端向某节点发送与数据库键相关的命令时,收到命令的节点会计算出命令要处理的键属于哪个槽,并检查这个槽是否由自己负责。如果不是自己负责的,那么会向客户端返回一个MOVED回复,将客户端转到正确的节点,并重新发送要执行的命令。
MOVED的格式为: MOVED < slot> < ip>:< port>
需要注意的是,集群中所有的节点只能使用0号数据库。
3.3 槽的重新分配
通过redis-trib完成,具体流程如下图:
如果在重新分配槽的过程中,客户端访问某个key,这个key所属的槽刚好在进行迁移,这时候该槽的数据会一部分在源节点,一部分在目标节点,此时源节点会先在自己的数据库中查找执行的键,如果找到,执行执行客户端的命令,如果没找到,则返回一个ASK错误,指引客户端转到正在导入槽的目标节点,并再次发起要执行的命令。
3.4 集群的故障与恢复
Redis集群中节点也是分为主节点和从节点的,主节点用于处理槽,从节点则用于复制自己跟随的主节点,并在主节点下线时,替代主节点继续处理请求。
向一个节点发送CLUSTER REPLICATE < node_id>就可以将收到命令的节点设置为node_id所指向节点的从节点。
具体过程:
- 收到该命令的节点会在自己的nodes字典中找到node_id对应节点的clusterNode结构,并将自己的cluster.myself.slaveof指针指向这个clusterNode,表示自己以后要复制这个节点的数据。
- 然后该节点会修改自己的clusterState.myself.flags,将自己的标识设置为REDIS_NODE_SLAVE,表示自己从原来的主节点变为从节点了。
- 该节点最后会根据cluster.myslef.slaveof指向的clusterNode中的ip和端口,来进行复制命令。(cluster.myself是一个指向表示本节点的clusterNode的指针)
最后,一个节点变为从节点,并开始复制主节点这一消息会告知到集群中的其他所有节点。其群众所有节点都会在表示主节点的clusterNode结构体的slaves属性中加入从节点的clusterNode。
集群中的每个节点都会向其他所有节点发送ping命令,如果有一台机器没有及时回复pong,那么本节点就会在clusterState的nodes字典中找到代表那台没有回复的机器的clusterNode,然后将其标志设置为疑似下线,然后将消息广播给其他节点。
其他节点收到消息后,会从nodes字典中找到那台疑似下线的节点的clusterNode,然后将某机器认为你已下线的报告添加到一个链表中,如果该链表中存储的疑似下线报告超过集群半数以上的主节点数目,那么就将标记该主节点为已下线,开始故障恢复。(从节点,挂掉了没关系)
选择从节点当新主的时候,使用的是raft来进行选择。然后其他的和Redis的主从模式基本类似。