redis sentinel 实现高可用

sentinel 是 redis 官方维护的一套高可用方案。

redis 进行读写分离之后,如果 master 节点宕机,就会导致整个服务不可用。使用 sentinel,在 master 节点宕机之后,sentinel 会自动从 slave 节点中选择出一个新的 master 替代挂掉的 master 节点,保障服务稳定。

sentinel 是独立于 redis 运行的程序,通常为了保证 sentinel 本身的可用性,会同时运行多个 sentinel,多个 sentinel 监管 redis 服务,发现 master 节点故障之后,就选择一个新的 master 节点。

sentinel 作用描述起来很简单,但实际上隐去了很多技术问题,比如:

为了明白以上问题,我们先搭建一套 sentinel 高可用实验,先感受下整个 sentinel 环境,然后再来分析下 sentinel 的细节。

使用 docker-compose 测试 sentinel 高可用

项目源码可以从这里直接获取,懒癌患者可以直接 clone 下来,根据 README.md 的说明直接测试。

├── README.md
├── docker-compose.yml
├── redis-master
│   ├── dockerfile
│   └── redis.conf
├── redis-sentinel1
│   ├── dockerfile
│   └── sentinel.conf
├── redis-sentinel2
│   ├── dockerfile
│   └── sentinel.conf
├── redis-slave1
│   ├── dockerfile
│   └── redis.conf
├── redis-slave2
│   ├── dockerfile
│   └── redis.conf
├── redis-slave3
│   ├── dockerfile
│   └── redis.conf
└── src
    ├── dockerfile
    ├── main.go
    └── makefile

整个实验包括 3 个 slave 节点、1 个 master 节点以及 2 个 sentinel 节点,还有一个用 go 编写的客户端测试程序,使用 docker-compose 来编排,可以先忽略整个实验代码的细节,直接进入 src 目录执行 make build && make run 如果 7 个 container 都启动成功,并且查看 go 编写的客户端 container 稳定输出 Jaychen 说明实验搭建成功。

因为 docker-compose 的特性,这里都没有通过 ip 来进行 container 互联,而是通过 docker-compose 中定义的服务名来访问其他服务,如果你不理解,可以把 docker-compse 定义的 service name 当成域名处理。

启动有问题可以在下方评论或者联系我处理

查看 go 代码发现,客户端不是直接连接 redis,而是连接 sentinel。

// 不是直接连接 redis,而是连接 sentinel
// 这里使用 go 编写客户端,不使用 go 可以不关心细节
client := redis.NewFailoverClient(&redis.FailoverOptions{
    MasterName:    "mymaster",
    SentinelAddrs: []string{"redis-sentinel1:26379", "redis-sentinel2:26379"},
})

有了 sentinel 之后,客户端访问 redis 的过程变成下面这种:

sentinel 监控

实验搭建完毕之后,看下 sentinel 的配置文件,位于 redis-sentinel1/sentinel.conf 中,配置项不多,我们核心关注👇两个

sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 60000

第一个配置项的命令格式为 sentinel monitor <name> <redis-master-ip> <redis-master-port> <quorum>,有 4 个参数,含义分别如下:

比较难以理解的是 quorum,上面说过为了保证 sentinel 的稳定性,通常会部署多个 sentinel 程序,那么多个 sentinel 同时监控 redis 集群,会存在有些 sentinel 认为 master 节点宕机,有些节点认为 master 节点正常的问题,这个时候如何断定 master 节点的状态?quorum 就是用来处理这个问题的,当超过 quorum 个 sentinel 认为 master 节点宕机了,那么就认为 master 节点真的宕机了。

第二个配置的格式为 sentinel down-after-milliseconds <name> <milliseconds>。要明白这个配置之前,需要先知道一个问题:sentinel 怎么知道 master 节点宕机了?在系统启动之后,sentinel 会每隔 1s 就向 master、slave 和其他 sentinel 发送 ping 指令保证所有节点都在线的状态。sentinel down-after-milliseconds 用来规定当 sentinel 的 ping 超过多少毫秒没有收到回复则认为该节点宕机。

明白上面两个基本配置之后,看下 sentinel 的输出日志。

执行 docker logs sentinel_container_id 可以查看 sentinel 的日志,sentinel 日志是我们分析 sentinel 执行过程的关键:

43:X 17 Dec 06:57:12.906 # Sentinel ID is 8d7fae02b4be87526432656d16f013b17e0e0884
43:X 17 Dec 06:57:12.906 # +monitor master mymaster 172.22.0.2 6379 quorum 2
43:X 17 Dec 06:57:12.908 * +slave slave 172.22.0.4:6381 172.22.0.4 6381 @ mymaster 172.22.0.2 6379
43:X 17 Dec 06:57:12.910 * +slave slave 172.22.0.3:6383 172.22.0.3 6383 @ mymaster 172.22.0.2 6379
43:X 17 Dec 06:57:12.912 * +slave slave 172.22.0.5:6382 172.22.0.5 6382 @ mymaster 172.22.0.2 6379
43:X 17 Dec 06:57:14.960 * +sentinel sentinel fe1974a7a154bc67cc5946b784918622f429ad64 172.22.0.7 26379 @ mymaster 172.22.0.2 6379

逐条说明下日志的含义:

sentinel 选举和故障恢复

当 master 节点挂掉之后,sentinel 会从 slave 节点从选择一个成为新的 master 节点对外提供写服务。

接下来我们尝试干掉 master 节点:docker stop redis-master-slave_redis-master_1,再次观察 sentinel 的日志:

43:X 17 Dec 07:06:14.248 # +sdown master mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.249 # +odown master mymaster 172.22.0.2 6379 #quorum 1/1
43:X 17 Dec 07:06:14.249 # +new-epoch 1
43:X 17 Dec 07:06:14.249 # +try-failover master mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.252 # +vote-for-leader 8d7fae02b4be87526432656d16f013b17e0e0884 1
43:X 17 Dec 07:06:14.255 # fe1974a7a154bc67cc5946b784918622f429ad64 voted for 8d7fae02b4be87526432656d16f013b17e0e0884 1
43:X 17 Dec 07:06:14.314 # +elected-leader master mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.315 # +failover-state-select-slave master mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.399 # +selected-slave slave 172.22.0.5:6382 172.22.0.5 6382 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.399 * +failover-state-send-slaveof-noone slave 172.22.0.5:6382 172.22.0.5 6382 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.459 * +failover-state-wait-promotion slave 172.22.0.5:6382 172.22.0.5 6382 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.529 # +promoted-slave slave 172.22.0.5:6382 172.22.0.5 6382 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.530 # +failover-state-reconf-slaves master mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:14.599 * +slave-reconf-sent slave 172.22.0.4:6381 172.22.0.4 6381 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:15.434 * +slave-reconf-inprog slave 172.22.0.4:6381 172.22.0.4 6381 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:15.434 * +slave-reconf-done slave 172.22.0.4:6381 172.22.0.4 6381 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:15.517 * +slave-reconf-sent slave 172.22.0.3:6383 172.22.0.3 6383 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:15.608 * +slave-reconf-inprog slave 172.22.0.3:6383 172.22.0.3 6383 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:16.680 * +slave-reconf-done slave 172.22.0.3:6383 172.22.0.3 6383 @ mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:16.770 # +failover-end master mymaster 172.22.0.2 6379
43:X 17 Dec 07:06:16.771 # +switch-master mymaster 172.22.0.2 6379 172.22.0.5 6382
43:X 17 Dec 07:06:16.772 * +slave slave 172.22.0.4:6381 172.22.0.4 6381 @ mymaster 172.22.0.5 6382
43:X 17 Dec 07:06:16.772 * +slave slave 172.22.0.3:6383 172.22.0.3 6383 @ mymaster 172.22.0.5 6382
43:X 17 Dec 07:06:16.772 * +slave slave 172.22.0.2:6379 172.22.0.2 6379 @ mymaster 172.22.0.5 6382
43:X 17 Dec 07:07:16.796 # +sdown slave 172.22.0.2:6379 172.22.0.2 6379 @ mymaster 172.22.0.5 6382

解释下前两条日志中的 sdownodown

sdown 全称为:subjectively down(主观下线)。sentinel 在 ping master 节点在超过 down-after-milliseconds ms 没有收到回复之后,该 sentinle 节点主观上认为该 master 节点已经宕机。第一条日志输出表明该 sentinel 认为 master 节点已经挂了。

odown 全称为:objectively down(客观下线)。当超过 quorum 个 sentinel 都 sdown 的认为 master 节点宕机,那么就客观认为该 master 节点挂掉,接下去进行选举和故障恢复。

master 节点挂掉之后会 sentinel 会进行故障恢复,学习故障恢复之前先要明白故障恢复具体要做些什么事情?

上面提到 sentinel 本身就是一个集群,如果故障恢复由多个 sentinel 同时进行,那么就可能出现争议,所以故障恢复只能由一个 sentinel 进行,所以在进行故障恢复之前,sentinel 本身之间要进行一次选举,多个 sentinel 进行投票选举出一个领头羊,由这个领头羊进行故障恢复。

sentinel 选举使用的是 Raft 算法,这篇文章不深入 raft 算法,之后会新开一个坑来讲讲。

接下来根据上面的日志来分析下故障恢复的过程。

在故障恢复中有一个纪元 (epoch) 的概念,可以理解为每次故障恢复时候产生的版本号,每次进行一次故障恢复,该 epoch 都会进行一次自增。这是为了保证集群中所有的 sentinel 都有一份最新的配置。

具体来说,假设一个集群有 5 个 sentinel,在某次故障恢复中有 3 个 sentinel 同意选举,于是选举开始,epoch 更新为 2,故障恢复之后领头羊 sentinel 把新的 master 节点信息传播到集群的每个 sentinel,但是这个时候有一个 sentinel 挂了,没有收到新的 master 节点信息,在 sentinel 重新上线之后会比对自己保存的 epoch,发现低于集群中其他 sentinel 节点,于是可以断定自己的配置信息过时了,就从其他 sentinel 更新 redis 的配置和 epoch。

上面日志中 new-epoch 表明产生一个新的纪元,并且更新了纪元的值。 try-failover 信息说明开始进行故障恢复,故障恢复的第一步就是 sentinel 之间进行领头羊选举,在日志中输出为 vote-for-leader。最后日志 promoted-slave 说已经把一个 slave 节点提升为新的 master 节点。后续的日志表示:其他的 slave 节点都连接到新的 master 节点,并且进行数据同步。

*****
Written by JayChen on 12 December 2018