走进Redis:哨兵集群

为什么需要哨兵

在 Redis 的主从库模式中,如果从库发生了故障,用户的操作是可以继续进行的,因为写操作是只在主库中进行的。那么,如果主库发生了故障,用户的操作将会收到影响。这时候可能会需要选择一个从库在作为主库继续服务用户的操作。Redis 提供的哨兵机制就是解决主从库模式的 Redis 服务可用性问题的。

故障转移的基本流程

哨兵发现主库下线并选出新主库的流程称为故障转移,这个过程需要解决三个问题:

  1. 主库真的挂了吗?(监控)
  2. 该选择那个从库作为主库?(选主)
  3. 怎么把新主库的相关信息通知给从库和客户端呢?(通知)

监控

哨兵在运行过程中周期性的给所有主从库发送 PING 命令,以此检测它们是否正常运行。如果某个实例对 PING 命令的没有在down-after-milliseconds 应答,那么,哨兵会把它标记为“主观下线”。

哨兵不会对主观下线的从库做额外的处理,如果是主库主观下线,那么哨兵会进行后续的选主和通知操作,这些操作会有额外的计算和通信开销。为了减少误判,哨兵通常会采用集群部署。当一个哨兵将 master 标记为主观下线后,会和其它哨兵实例通过命令 SENTINEL is-master-down-by-addr 交流,如果有大于 quorum 个哨兵确认 master 已下线,则该 master 会被标记为”客观下线”;否则,会重新将 master 标记为上线状态。

💡 quorum 通过 sentinel.conf 配置得到,和哨兵集群中实例的数量有关。例如,若共有 3 个实例,则该值可设置为 2 ,最好将该值设置为 $N/2+1$。该值越小,判断 master 客观下线的条件越宽松;反之则判断 master 客观下线的条件越严格。通常哨兵集群中实例的数量为奇数,避免出现应答下线和未下线数量相同的情况。

//raw.githubusercontent.com/LooJee/medias/master/images202208091027768.png

//raw.githubusercontent.com/LooJee/medias/master/images202208091036894.png

选主

选新主库的过程大致可以分为以下几个步骤:

哨兵 Leader 选举

哨兵 Leader 是本次故障转移的执行者,每个哨兵都有机会成为 Leader。具体流程如下:

  1. 当哨兵将 master 节点标记为客观下线后,会将当前纪元(epoch,该值类似 raft 协议的 term 值)加一,表示开始一次新的 leader 选举。
  2. 首先会给自己投一票,然后会再次发送 is-master-down-by-addr 给其它哨兵。这个命令的使用方式如下:sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>。这里主要看 runid,确认主节点是否客观下线时,该值为 *;开始参选 Leader 时,传的参数为本节点的 runid。
  3. 收到命令的哨兵,如果没有同意过其他节点的sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝。并且同意过其它节点后,该节点也不会发起投票。
  4. 如果该哨兵发现自己的票数已经大于等于max(quorum, num(sentinels)/2+1),那么它将成为领导者。
  5. 如果此过程没有选举出领导者,将进入下一次选举。选举超时时间可以查看配置 failover-timeout

//raw.githubusercontent.com/LooJee/medias/master/images202208101819212.png

筛选

  1. 首先会判断从库当前的状态,过滤掉当前主观下线以及断线的节点;
  2. 5 秒内没有回复过哨兵节点 PING 请求的节点;
  3. 与主节点断连次数超过 10 次的节点。是否断连是根据配置项 down-after-milliseconds 来判断的,主从节点 down-after-milliseconds 毫秒内都没有连接上,则会被记为一次断连。

打分

  1. 通过配置项 slave-priority 来给不同的从库设置不同的优先级。可以使用命令 info Replication 来查看该配置。如果有一个从库的优先级最高,那么该从库会直接选为新主库;否则,将会进行第二轮打分。
  2. 与旧主库同步程度最接近的从库得分高。在主从同步中提到过,从库会用 slave_repl_offset 来记录当前从 master 的复制进度,该配置通过 info Replication 可以查看。哨兵会选出复制进度值最大的从库作为新的主库,如果多个从库复制进度并列第一,那么需要进行第三轮打分。
  3. ID 号最小的从库得分高。最后,哨兵会查看从库的 runId ,将 runId 最小的从库作为新主库。runId 通过命令 info Server 可以查看。

//raw.githubusercontent.com/LooJee/medias/master/images202208101427306.png

通知

当选出新主库之后,哨兵有三个角色需要通知:

  1. 告诉新主库它已经称为了新主库。哨兵会向目标库发送命令 slaveof no one ,收到命令后,目标库会将自己的角色( role )提升为master
  2. 告诉其它从库新主库的地址。哨兵会向其它从库发送 slaveof host port 来告诉从库新主库的地址,从库会执行 replicaof 命令开始从新主库同步。
  3. 通知客户端主库变化。当新主库切换完成后,哨兵会在频道 +switch-master 中发送消息,通知外部客户端新主库地址。

哨兵集群

上面讨论主从切换的时候有提到哨兵集群来减少主库客观下线误判的可能性。哨兵监控一个 master 节点是通过下面这个命令来完成的:

sentinel monitor <master-name> <ip> <redis-port> <quorum>

这个命令中并没有指定其它哨兵的地址信息,那么哨兵是如何组成一个集群的呢?

基于 pub/sub 机制的哨兵集群

哨兵成功和主节点建立连接之后,会在主节点上创建一个名为 “sentinel:hello” 的频道,然后会将自己的地址端口等信息发布到该频道,其它哨兵就可以从这个频道获取监控同一个主节点的哨兵信息,互相建立网络连接,形成一个集群。

💡 我们可以用 redis-cli 连接上主节点,然后用命令psubscribe * 监听主节点上的所有频道,会看到哨兵不断在 __sentienl__:hello 频道发送自己的信息。

//raw.githubusercontent.com/LooJee/medias/master/images202208111100489.png

获取从库信息

redis 主库会保存从库的信息,哨兵会向主库发送 INFO 命令来获取从库列表。哨兵就可以根据从库列表的地址信息和每个从库建立连接,然后监控从库的状态。

//raw.githubusercontent.com/LooJee/medias/master/images202208111107427.png

哨兵获取从库信息主要是INFO 命令查看主库的 replication 信息,典型的一个 replication 信息如下所示:

127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=172.26.0.3,port=6379,state=online,offset=217874,lag=0
slave1:ip=172.26.0.2,port=6379,state=online,offset=217874,lag=0
master_failover_state:no-failover
master_replid:51567b12a9c1ba1d82846a8fb8fd84404b60eaf8
master_replid2:0baa276d98298739b2e0755640fd5b50d2828b26
master_repl_offset:217874
second_repl_offset:17993
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:429
repl_backlog_histlen:217446

从信息里可以看到主库的 role 为 master ,已连接的从库数量(connected_slaves)为 2,然后 slave0 和 slave1 就是从库的信息。

哨兵会定期给主库和从库发送 PING 命令检测网络状态,如果超时未应答就会标记为主观下线,如果主观下线的是主库,就会开启故障转移流程。

客户端访问 redis-server

客户端如何访问哨兵集群监控的 redis-server 呢?主要有以下几个步骤:

  1. 连接哨兵集群。客户端初始化时,需要提供哨兵的地址,客户端会连接到第一个可用的哨兵地址。
  2. 连接主库。客户端给哨兵发送 SENTINEL get-master-addr-by-name <master-name> 获取主库地址,然后和主库建立连接。为了确认连接的确实是主库,客户端还需要发送 info replication 命令来查看 role 是否是 master,如果不是的话就要从第一步重新开始。
  3. 连接从库。客户端给哨兵发送 SENTINEL replicas <master-name>来获取从库信息。相应的,客户端也可以通过 info 命令来确认目标的角色。
  4. 监听主库迁移。哨兵通过 pub/sub 来通知客户端集群中的事件。哨兵提供的订阅频道很多,具体可以查看//redis.io/docs/manual/sentinel/#pubsub-messages 。客户端使用命令 psubscribe * 来订阅所有的频道,来了解主从切换的进度。当频道 switch-master 有新消息时,表示新主库已经切换完成。

//raw.githubusercontent.com/LooJee/medias/master/images202208111154890.png

实验

可以使用以下 docker-compose 来简单模拟故障迁移的实验:

version: "3"
services:
  redis-master:
    image: redis:7
    ports:
      - "16379:6379"
    container_name: "redis-master"
    command: redis-server
    networks:
      - sentinel-network
  redis-slave-1:
    image: redis:7
    ports:
      - "6380:6379"
    container_name: "redis-slave-1"
    command: redis-server --replicaof redis-master 6379
    depends_on:
      - redis-master
    networks:
      - sentinel-network
  redis-slave-2:
    image: redis:7
    ports:
      - "6381:6379"
    container_name: "redis-slave-2"
    command: redis-server --replicaof redis-master 6379
    depends_on:
      - redis-master
    networks:
      - sentinel-network
  redis-sentinel:
    image: bitnami/redis-sentinel:latest
    environment:
      - REDIS_MASTER_HOST=redis-master
      - REDIS_SENTINEL_QUORUM=2
      - REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000
    depends_on:
      - redis-master
      - redis-slave-1
    ports:
      - '26379-26381:26379'
    networks:
      - sentinel-network
networks:
  sentinel-network:

使用命令 docker-compose up --scale redis-sentinel=3 来启动 docker-compose。启动之后会打出如下的日志:

redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.825 * Sentinel new configuration saved on disk
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.825 # Sentinel ID is 787487fa6116b997caa0a44b011793b58e265a54
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.825 # +monitor master mymaster 172.27.0.2 6379 quorum 2
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.827 * +slave slave 172.27.0.3:6379 172.27.0.3 6379 @ mymaster 172.27.0.2 6379
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.840 * Sentinel new configuration saved on disk
redis-sentinel_1  | 1:X 11 Aug 2022 03:59:21.840 * +slave slave 172.27.0.4:6379 172.27.0.4 6379 @ mymaster 172.27.0.2 6379

日志表示这样一个流程:哨兵初始化完成 ⇒ 监控主库 ⇒ 监控从库。

此时,如果连接上主库,然后订阅 __sentinel__:hello 频道,可以看到如下的消息:

//raw.githubusercontent.com/LooJee/medias/master/images202208111349300.png

集群内的哨兵都在往 __sentinel__:hello 发送自己的服务信息。

可以使用命令 docker stop redis-master 来模拟主库下线的情况,在执行这个命令之前可以先连到哨兵节点 redis-cli -p 26379 ,然后用命令 psubscribe * 来订阅哨兵的频道,这样模拟主库下线时就能看到哨兵通过频道通知客户端的消息了:

//raw.githubusercontent.com/LooJee/medias/master/images202208111358072.png

结语

Redis 哨兵的相关原理暂时告一段落,欢迎大家交流。

Tags: