0%

Redis 设计与实现-哨兵

Sentinel(哨岗、哨兵)是Redis的高可用性(high availability)解决方案:由一个或多个Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进人下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

图16-1展示了一个Sentinel系统监视服务器的例子,其中:

  • 用双环图案表示的是当前的主服务器 server1。

  • 用单环图案表示的是主服务器的三个从服务器server2、serveI3及server4。

  • server2、server3、server4 三个从服务器正在复制主服务器 server1,而 Sentinel 系统则在监视所有四个服务器。

假设这时,主服务器server1进入下线状态,那么从服务器server2、server3、server4 对主服务的复制操作将被中止,并且Sentinel系统会察觉到server1已下线,如图16-2所示(下线的服务器用虚线表示)。

当server1的下线时长超过用户设定的下线时长上限时,Sentinel系统就会对 server1 执行故障转移操作。

  • 首先,Sentinel系统会挑选server1属下的其中一个从服务器,并将这个被选中的上服务器升级为新的主服务器。

  • 之后,Sentinel系统会向server1属下的所有从服务器发送新的复制指令,让它们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。

  • 另外,Sentinel还会继续监视已下线的server1,并在它重新上线时,将它设置为新的主服务器的从服务器。

1. 启动并初始化 Sentinel

下面讲述一个 Sentinel 启动时,所需要执行的步骤。

1.1 初始化服务器

首先,因为Sentinel本质上只是一个运行在特殊模式下的Redis服务器,所以启动Sentinel的第一步,就是初始化一个普通的Redis服务器。不过,因为Sentinel执行的工作和普通Redis服务器执行的工作不同,所以Sentinel的初始化过程和普通Redis服务器的初始化过程并不完全相同。表16-1展示了Redis服务器在Sentinel模式下运行时,服务器各个主要功能的使用情况。

1.2 使用 Sentinel 专用代码

启动Sentinel的第二个步骤就是将一部分普通Redis服务器使用的代码替换成Sentinel专用代码。比如说,普通Redis服务器使用redis.h/REDIS_SERVERPORT常量的值作为
服务器端口:

1
#define REDIS_SERVER_PORT 6379

而Sentinel则使用sentinel.c/REDIS_SENTINEI_PORT常量的值作为服务器端口:

1
#define REDIS_SERVER_PORT 26379

除此之外还有命令表的不同等等…

1.3 初始化 Sentinel 状态

在应用了Sentinel的专用代码之后,接下来,服务器会初始化一个sentinel.c/sentinelState结构(后面简称“Sentinel状态“),这个结构保存了服务器中所有和Sentinel功能有关的状态(服务器的一般状态仍然由redis.h/redisServer结构保存)。

1.4 初始化 Sentinel 状态的 masters 属性

Sentinel状态中的masters字典记录了所有被Sentinel监视的主服务器的相关信息,其中:

  • 字典的键是被监视主服务器的名字。

  • 而字典的值则是被监视主服务器对应的sentinel.c/sentinelRedisInstance结构。

每个sentinelRedisInstance结构(后面简称“实例结构“)代表一个被Sentinel监视的Redis服务器实例(instance),这个实例可以是主服务器、从服务器、或者另外一个Sentinel。

sentinelRedisInstance.addr 属性是一个指向sentinel.c/sentinelAddr结构的指针,这个结构保存着实例的IP地址和端口号。假设现在 Sentinel 存在名为 master1 和 master2 的两台主武器,那么Sentinel将为主服务器master1创建如图16-5所示的实例结构,并为主服务器master2创建如图16-6所示的实例结构,而这两个实例结构又会被保存到Sentinel状态的masters字典中,如图16-7所示。

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

初始化Sentinel的最后一步是创建连向被监视主服务器的网络连接,Sentinel将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息。对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接:

  • 一个是命令连接、这个连接专门用于向主服务器发送命令,并接收命令回复。

  • 另一个是订阅连接,这个连接专门用于订阅主服务器的 _sentinel_:hello频道。

图16-8展示了一个Sentinel向被它监视的两个主服务器master1和master2创建命令连接和订阅连接的例子。接下来的一节将介绍Sentinel是如何通过命令连接和订阅连接来与被监视主服务器进行通信的。

2. 获取主服务器信息

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

主服务器返回的从服务器信息,会被用于更新主服务器实例结构的slaves字典,这个字典记录了主服务器属下从服务器的名单:

  • 字典的键是由Sentinel自动设置的从服务器名字,格式为ip:port。
  • 至于字典的值则是从服务器对应的实例结构。

Sentinel在分析INFO命令中包含的从服务器信息时,会检查从服务器对应的实例结构是否已经存在于slaves字典:

  • 如果从服务器对应的实例结构已经存在,那么Sentinel对从服务器的实例结构进行更新。
  • 如果从服务器对应的实例结构不存在,那么说明这个从服务器是新发现的从服务器,Sentinel会在slaves字典中为这个从服务器新创建一个实例结构。

假如现在存在主服务器master和三个从服务器slave0、slave1和slave2,Sentinel将分别为三个从服务器创建它们各自的实例结构,并将这些结构保存到主服务器实例结构的slaves字典里面,如图16-10所示。

3. 获取从服务器信息

当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构之外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。举个例子,对于图16-10所示的主从服务器关系来说,Sentinel将对slave0、slave1和slave2三个从服务器分别创建命令连接和订阅连接,如图16-11所示。

在创建命令连接之后,Sentinel在默认情况下,会以每十秒一次的频率通过命令连向从服务器发送INFO命令,根据这些信息,Sentinel会对从服务器的实例结构进行更新。

4. 向主服务器和从服务器发送消息

在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器的 _sentinel_:hello发送频道连接命令 PUBLISH。

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

当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令:

1
SUBSCRIBE _sentinel_:hello

sentinel对_sentinel_:hello频道的订阅会一直持续到Sentinel与服务器的这接断开为止。这也就是说,对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的_sentinel_:hel1o频道发送信息,又通过订阅连接从服务器的_sentinel_:hello频道接收信息,如图16-13所示。

对于监视同一个服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收到,这些信息会被用于更新其他Sentinel对发送信息Sentinel的认知,也会被用于更新其他Sentinel对被监视服务器的认知。例如 sentine1 向服务器发送的消息可以被其他 sentinel 接收到:

5.1 更新 sentinels 字典

Sentinel为主服务器创建的实例结构中的sentinels字典保存了除Sentinel本身之外,还保存则所有同样监视这个主服务器的其他Sentinel的资料。图16-15展示了Sentinel 127.0.0.1:26379为主服务器127.0.0.1:6379创建的实例结构,以及结构中的sentinels字典。

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

当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:Sentinel A有连向Sentinel B的命令连接,而Sentinel B也有连向Sentinel A的命令连接。图16-16展示了三个监视同一主服务器的Sentinel之间是如何互相连接的。

6. 检测主观下线状态

在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。

Sentinel 配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进人主观下线所需的时间长度:如果一个实例在down-after-milliseconds毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构 sentinelRedisInstance,在结构的flags属性中打开SRI_S_DOWN标识、以此来表示这个实例已经进入主观下线状态。

7. 检测客观下线状态

当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。

8. 选举领头 Sentinel

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。

以下是Redis选举领头Sentinel的规则和方法:

  • 所有在线的Sentinel都有被选为领头Sentinel的资格,换句话说,监视同一个主服务器的多个在线Sentinel中的任意一个都有可能成为领头Sentinel。
  • 每次进行领头Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元(configuration epoch)的值都会自增一次。配置纪元实际上就是一个计数器,并没有什么特别的。
  • 在一个配置纪元里面,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改。
  • 每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自已设置为局部领头Sentinel。
  • Sentinel设置局部领头Sentinel的规则是先到先得:最先向目标Sentinel发送设置要求的源Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝。
  • 如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。举个例子,在一个由10个Sentinel组成的Sentinel系统里面,只要有大于等于10/2+1=6个Sentinel将某个Sentinel设置为局部领头Sentinel,那么被设置的那个Sentinel就会成为领头Sentinel。
  • 因为领头Sentinel的产生需要半数以上Sentinel的支持,并且每个Sentinel在每个配置纪元里面只能设置一次局部领头Sentinel,所以在一个配置纪元里面,只会出现一个领头Sentinel。
  • 如果在给定时限内,被选举为局部领头次数最多的 Sentinel 将会被选举为领头 Sentinel。如果没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。

9. 故障转移

在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤:

  • 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。

  • 让已下线主服务器属下的所有从服务器改为复制新的主服务器。

  • 将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。

------ 本文结束------