Redis集群与哨兵

Sentinel

Sentinel(哨兵)是Redis的高可用的一个解决方案。有一个或多个Sentinel实例组成的Sentinel系统。用来监视任意多个主服务器和从服务器。

实现

使用Sentinel的时候,会启动Sentinel用来监视主服务器或者从服务器,应该在Sentinel配置文件中配置要监视的主服务器,之后Sentinel会从和主服务器之间的信息交换中获得主服务器的从服务器的信息。

每个Sentinel都又一个SentinelState结构来表示,通过这个结构来保存所有此Sentinel用到的数据。
此Sentinel会保存所有监视的主服务器的信息(从配置文件知道监视哪些主服务器),用sentinelRedisInstance结构来表示,并且还会保存所有监视的主服务器的所有从服务器的信息(从和主服务器的交流看出),保存在对应主服务器sentinelRedisInstance结构的slave属性中,也是一个sentinelRedisInstance结构。除此之外, Sentinel还会保存其他监视此主服务器的Sentinel的结构信息(从订阅的Sentinel:hello频道得到)。

Sentinel服务器会向自己监视的所有主从服务器发起两个连接,一个是命令连接,一个是订阅连接。 命令连接用来和被监视服务器进行通信,比如服务器以每10秒一次通过命令向从服务器发送INFO命令,并获得此服务器的主服务器和复制偏移量等等。另一个是订阅连接,Sentinel会通过订阅连接向监视的服务器发送一条命令: SUBSCRIBE sentinel:hello。这里就是订阅服务器的这个频道,之后就可以通过这个频道发布的消息来获取其他同样监视此服务器的信息。

Sentinel服务器每两秒一次频率向所有被监视的主从服务器发送一条消息,
PUBLISH sentinel:hello “<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>”
保存了Sentinel本身的一些信息和正在监视的主服务器的信息。这条命令向服务器的sentinel:hello频道发送了一条消息,所有订阅了此频道的服务器都将收到消息。 而Sentinel发现其他对主服务器的监视者也是通过订阅主服务器的此频道来发现的。通过这条信息,能够及时发现其他同样在监视此主服务器的Sentinel。然后更新自己的记录。

主观下线

Sentinel会以每秒一次的频率向所有与它创建了命令连接的服务器(包括主从和Sentinel)发送PING命令,通过返回来判断是否在线。
在Sentinel服务器的配置文件中,有一个这样的配置: sentinel down-after-milliseconds master 50000. 这样的配置就是说,在50000毫秒内服务器都向Sentinel返回了无效回复或者没有回复,那么此Sentinel将会判断此服务器为主观下线状态。注意 这个配置不仅对主服务器有效,对从服务器同样有效。 并且每个Sentinel的配置可能不一样, 因此可能会出现一个Sentinel判断服务器为主观下线,另一个判断为没有主观下线。 因此判断完之后还会在判断是否客观下线。

客观下线

当一个Sentinel判断一个主服务器主观下线了。则会向其他监视此主服务器的Sentinel进行询问,用来判断此服务器是否是真的下线了。当收到同意下线的个数超过在配置的数量的时候。 配置在配置文件中 : sentinel monitor master 127.0.0.1 6379 5 .表明收到5个Sentinel都同意主服务器下线的回复后,那么会设置主服务器为客观下线状态。 (注意因为么个Sentinel的配置不同,所以每个Sentinel判断客观下线的条件也不同)

选举领头Sentinel

选举过程比较简单, 首先每个Sentinel都会有一个运行ID和当前纪元。然后在每个纪元内,每个发现主服务器进入客观下线状态的Sentinel都会要求其他Sentinel将自己选择为头领。并且每个Sentinel都有一此投票机会。 至于这个票投给谁,采用的是先到先得的方式,谁先给我发消息让我投它,我就投谁,投完票之后所有的请求都将被拒绝。 当有一个Sentinel的票数大于所有Sentinel数量的一半,则说明这个Sentinel为头领 。

每个Sentinel都有一个自己的超时时间,如果在给定时间内没有一个Sentinel票数超过一半,那么就将所有的配置纪元加一,开始新一轮的投票,直到选出领头Sentinel。并且Redis在实现的时候对于每个Sentinel在每个纪元的等待超时时间是随机产生的,就是说每个Sentinel在这一轮如果没有选举出结果的话,会等待一个随机1-2秒内的时间,之后在发送,这样保证了能够尽量有第一个Sentinel被选举出来,加快了选举的速度.这样的话就尽可能的第一个Sentinel被选举出来。那样就能够保证在短时间内出现一个票数超过一般的Sentinel。

故障转移

选出领头Sentinel之后,会将已下线的主服务器执行故障转移操作

  • 在已下线的主服务器的从服务器中挑选一个服务器作为主服务器
  • 让其他从服务器复制新的主服务器
  • 让已下线的主服务器设置为新的主服务器的从服务器

选择从服务器的规则: 首先根据从服务器优先级,其次是 复制偏移量,最后是运行ID。

集群

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

节点

首先,一个集群肯定要包含很多节点。这些节点是怎么连接到一起的呢。 是通过CLUSTER MEET 命令来进行与其他节点握手,握手完成之后,这个两个节点就会处于同一集群了。之后其他节点在与集群中的任意一个节点握手,那么这个节点也就加入到这个集群中去了。

ClusterNode

每个集群中的节点都用一个clusterNode结构来表示。 每个节点都会为自己建立一个clusterNode节点,并且为集群中的其他节点也创建一个clusterNode结构。以此来记录其他节点的状态。

clusterNode有一个clusterState结构,用来记录当前节点的视角下集群的状态。 比如集群是上线还是下线,集群包含有多少个节点,集群当前的纪元配置等等信息。 并且还保存了所有的集群节点名单。 用一个字典表示,键是节点名称,值是表示该节点的clusterNode结构。

槽指派

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

当着16384个槽都有节点在处理的时候,那么整个集群属于上线状态,否则,集群处于下线状态。 可以使用cluster addslots 命令来添加节点要处理的槽。

每个节点的clusterNode会记录每个槽被分配给了谁,并且还会记录自己负责处理那些槽。
使用一个二进制位数组来表示自己处理那些槽。 unsigned char slots[16384/8]. 如果该二进制位为1,表示节点处理此槽,否则不处理。

并且在clusterState结构中还存储了一个槽的指派信息,表明了每个此槽被指派给那个节点。 用一个clusterNode数组表示。
每个数组项都是一个指向clusterNode的指针,如果指向NULL,则表示此槽没有被分配给任何节点,否则表示此槽被分派给了此clusterNode代表的节点。

传播

每个节点会将自己的slots数组发送给其他节点。 这个slots数组包含在发送的消息头中,每个发送的消息都会包含此slots信息。 每当节点收到别人发来的slots数组之后,就会更新对应节点的clusterNode的信息。

这里发现Redis中在两处都记录了槽的指派信息, 一个是在clusterNode的slots数组中,另一个是在clusterState的slots数组中。 但是两个里面的数据是不一样的。 clusterNode的slots数组是二进制位数组。 用char[]类型表示。只用来表示自己处理那些槽,并且如果要告诉其他节点自己处理那些槽,那么就会发送自己clusterNode的solts二进制数组。 这个时间是O(1)。当客户端发来指令,要知道指令是属于哪个槽的,那么就需要去寻找clusterState的slots数组,找到对应的节点然后进行处理。

cluster addslots命令实现

首先查找所有输入的slot是否已经被指派了,如果已经被指派了,那么就会返回错误。

执行命令

先来看一张图

首先会根据键计算键属于哪个槽。是通过计算key的CRC-16检验和的出来的。之后会去检查clusterNode的slots数组的第i项是否为1,如果为1,则说明自己负责此槽,那么就会处理客户端的命令。 否则,去看clusterState中的slots数组的第i项,然后向客户端发出moved错误,指引客户端转向正在处理槽的节点。

节点数据库

每个节点都会保存所有自己处理的槽的所有键值对,并且这些键值对也是用字典来实现, 与普通的单机数据库不一样的是只能使用-号数据库。 并且会使用一个跳跃表来保存槽和键的映射关系。跳跃表的每个节点的分值是槽号,每个节点的成员则是一个数据库键。当向数据库中添加一个键的时候,节点就会将这个键以及对应的槽号关联起来 。 这样就能得到对应槽都包含有那些键值对。 主要是用在集群的重新分片上。

重新分片

Redis可以将任意数量已经指派给某个节点的槽改为指派给另一个节点。

原理

在更改槽指派的时候会使用redis-trib负责执行的。

  • 让目标节点准备好导入源节点中属于槽slot的键值对
  • 之后向源节点发数据,让源节点准备好将槽slot的键值对发送到目标节点
  • 向源节点发送执行获取count个属于槽slot的键值对的键名。并且对于这些获取到的键名发送给源节点,将被选中的键值对都迁移到目标节点。 重复此步骤,直到迁移完成
  • 最后向集群中的任意节点发送集群设置槽命令,将槽slot指派给目标节点。这一指派信息会通过消息发送至整个集群。最终所有的节点都会知道。

每个节点都有两个数组来保存当前节点正在从其他节点导入的槽和正在导入其他节点的槽

1
2
3
4
struct clusterState{
clusterNode * importing_slots_from[16384];
clusterNode * migrating_slots_to[16384];
}

ASK错误

在重新分片期间,可能会出现刚好槽的数据被迁移到了目标节点的情况,那么这个时候,节点首先判断自己是否有此键,如果没有,则回去检查自己的migrating_slots_to数组中,此槽是不是在迁移,如果是,则向客户端返回ASK错误,引导客户端去正确的节点去查找Key。

ASK错误是指这一次命令需要去指定的节点执行,下一次客户端还是会去之前的节点请求。
MOVED错误是指这个命令已经被移动到了指定的节点。客户端下一次回去请求新的节点。

复制和故障转移

每个集群中的节点都可以拥有从节点。通过设置clusterState.myself.flags中的属性,打开REDIS_NODE_SLAVE标识。表示这个节点是一个从节点。然后开始向此从节点的主节点复制数据。

并且集群中的所有节点都会在对应主节点的clusterNode结构中的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单.

集群中的每个节点都会定期的向集群中的其他节点发送PING命令,以此来检测对方是否在线. 当发送了PING命令之后,节点没有给到有效回复,则将这个节点标记为疑似下线. 节点之间通过互发消息来共享消息. 如果一个节点A收到了节点B对节点C的疑似下线消息,那么会将这个报告记录下来,当有半数一上的节点认为一个节点疑似下线了.那么就会发送一条消息,标记此节点已经下线了. 这个时候就会选举一个此节点下的从节点来作为主节点.

这个选举方式和Sentinel很像. 每个主节点都有一票.当从节点收到自己的主节点下线之后,会向所有的主节点发送消息.要求所有收到此消息的节点给这个从节点投票,所以这个也是先来先得票. 直到有某一个从节点的票数超过半数,那么就会当选为主节点 . 之后新的主节点撤销掉对已下线主节点的槽指派,并将这些槽全部指派给自己. 在向集群中广播一条消息,让其他节点知道自己已经是主节点了。

复制操作

旧版

旧版复制的实现只能实现全量复制, 从服务器向主服务器发送同步消息,然后主服务器在后台生成RDB文件,并且用一个缓冲区缓存从现在开始执行的所有写命令,生成文件之后,再给从服务器发送过去,从服务器接收到文件会将自己数据库状态更新到主服务器执行BGSAVE命令时的状态。 之后主服务器将自己缓冲区里的命令发送给从服务器。

新版

旧版的缺点

如果从服务器是第一次复制还好,因为必须要全量复制所有的键。 但是如果从服务器状态和主服务器是一致的, 但是中间掉线了, 那么这个时候主服务器可能只是执行了一条写命令。 之后从服务器连上来之后,就需要重新从主服务器中获取所有的键,极大的浪费了资源。 这个时候新版的复制功能就出现了。

实现方法:
新版的主从复制命令为PSYNC,此命令有完整重同步和部分重同步两种模式

完整重同步用来处理初次复制的情况, 部分重同步用于处理断线后重连的情况。

PSYNC命令主要是用复制偏移量和复制积压缓冲区 和服务器的运行ID来实现的。

复制偏移量

主服务器和从服务器都会维护一个复制偏移量

  • 主服务器每发送N个字节的数据,就会将自己的复制偏移量加N
  • 从服务器每接受N个字节的数据,就将自己的复制偏移量加N

复制积压缓冲区

复制积压缓冲区是在主服务器上的,是一个固定长度先进先出的队列,默认大小为1MB。
在主服务器执行完命令之后,不仅会将命令转发给所有的从服务器,还会将命令写入复制积压缓冲区,因此,复制积压缓冲区保存着最近执行的命令。并且会为每个字节都记录相应的偏移量

当从服务器重新连上主服务器之后,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,这个时候主服务器根据offset和自己复制积压缓冲区中保存的偏移量进行对比,如果还保存有offset+1的字节,则执行部分重同步,否则执行完整重同步。

运行ID

运行ID 就是主服务器用来判断之前这个从服务器是否是复制自己的。 这个运行ID是在从服务器第一次复制主服务器的时候主服务器发过去的。

心跳检测

从服务器每秒一次向主服务器发送命令REPLCONF ACK 复制偏移量 。 主要用来检测主从连接状态和命令丢失