26 June 2017

概括

Sentinel(哨兵)是 Redis 2.8 版本发布的一个功能,使用 Sentinel 可以实现高可用的 Redis 集群服务。Sentinel 的作用是实时监控 Redis 集群中的所有服务器,当 Redis 主服务器宕机后,会自动把从服务器切换成主服务器,从而实现自动容灾的效果。

上图是 Sentinel 架构的网络拓扑图,Sentinel 服务器负责监控集群中所有的 Redis 服务器(包括所有的主服务器和从服务器),Sentinel 服务器会定时发送心跳包给所有的 Redis 服务器。当发现有主服务器宕机,就会触发自动容灾过程。

Sentinel 主要功能有以下几个点:

  1. 不断监控 Redis 集群的运行情况。
  2. 当一个主服务器出现问题,能够进行一次自动故障转移操作。Sentinel 会从失效主服务器的其中一台从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器。当失效的主服务器重新启动时,会变为从服务器复制新的主服务器。
  3. 当 Sentinel 完成一次故障转移后会执行一个用户自定义脚本,可以通过这个脚本实现自动更新客户端配置功能(如 PHP 配置文件)。

大概了解了 Sentinel 的功能,接下来我们先来介绍 Sentinel 的配置与使用。

Sentinel 的配置

一般来说,Sentinel 也需要配置多台,这样当某台 Sentinel 宕机也可以继续工作,避免了单点问题。

启动 Sentinel 有两种方法:

1) redis-sentinel /path/to/sentinel.conf
2) redis-server /path/to/sentinel.conf --sentinel

启动 Sentinel 时必须提供一个 sentinel.conf 配置文件,在配置文件中可以指定 Sentinel 监听的端口和要监控的 Redis 服务器。

下面我们介绍一下 Sentinel 有哪些配置项。

运行 Sentinel

接下来我们将会介绍怎么使用 Sentinel 来搭建一个高可用的 Redis 集群。

首先我们启动 3 个 Redis 服务器,其中一个作为主服务器,另外两个作为从服务器。

由于本人机器有限,所以下面将在同一台机器上使用不同的端口运行这些 Redis 服务器。

主服务器的配置(redis-master.conf):

从服务器一的配置(redis-slave1.conf):

从服务器二的配置(redis-slave2.conf):

启动后我们可以通过 ps 命令查看到运行情况:

接下来我们启动 Sentinel 服务器。在本例中我们启动 3 台 Sentinel 服务器,这是为了保证当某台 Sentinel 服务器宕机也不会导致服务停止。

在本例中,我们在同一台机器上部署所有的 Sentinel 服务器,所以需要设置不同的端口,其他配置都一样。所以下面只展示其中一台 Sentinel 服务器的配置:

这是 Sentinel 的自动发现机制,通过这个机制,Sentinel 可以知道所监控的主服务器下的所有从服务器。因为故障转移需要知道从服务器的信息,所以这里是为了故障转移作准备。

现在把 Redis 的主服务器关闭,我们通过查看 Sentinel 的配置文件可以发现,主服务器已经被修改为端口为 6380 的服务器了:

这说明 Sentinel 已经成功进行了故障转移。

另外,Sentinel 提供了故障转移后自动执行用户指定的脚本功能,通过这个功能我们可以实现自动修改客户端配置。

Sentinel 的使用就暂时介绍到这里,接下来我们继续下一部分内容

Raft 协议简介

由于 Sentinel 在故障转移的时候使用了 Raft 协议,所以我们先来简单的了解一下 Raft 协议。

Raft 协议是用来解决分布式系统一致性问题的协议,其设计初衷就是容易实现,保证对于普遍的人群都可以十分舒适容易的去理解。另外,它必须能够让人形成直观的认识,这样系统的构建者才能够在现实中进行必然的扩展。

当 Sentinel 集群发现 master 客观下线了,就会开始故障转移操作,故障转移操作的第一步就是在 Sentinel 集群选举一个 Leader,让 Leader 完成故障转移操作,而选举过程就是使用 Raft 协议来实现的。

接下来介绍一下 Raft 的角色

Raft 协议描述的节点共有三种角色:Leader, Follower, Candidate。在系统稳定运行的时候只有 Leader 和 Follower 两种状态的节点。一个 Leader 节点,其他的节点都是 Follower。Candidate 是系统运行不稳定时期的中间状态,当一个 Follower 对 Leader 的的心跳出现异常,就会转变成 Candidate,Candidate 会去竞选新的 Leader,它会向其他节点发送竞选投票,如果大多数节点都投票给它,它就会替代原来的 Leader 成为新的 Leader,原来的 Leader 会降级成 Follower。

下图是节点状态变化过程(图片来自网络):

接着再来介绍一下 Raft 的 Term(任期)

Raft – Term

Raft 协议提出一个名为 Term 的逻辑时间,将整个系统执行时间划分为若干个不同时间间隔长度的 Term 构成的序列,以递增的数字来作为 Term 的编号。如下图(图片来自网络):

每个 Term 由新一轮 Election(选举)开始,在这个时间内处于 Candidate 状态的服务器会竞争产生新的 Leader,这时会出现两种情况:

  1. 如果某个服务器赢得了选举,那在接下来的 Term 内将成为新的 Leader
  2. 如果没有选举出 Leader,则 Candidate 服务器会递增 Term,开始新一轮选举

Raft 的选举过程

如果系统中存在 Leader,Leader 会周期性的发送心跳来告诉其他服务器它是 Leader,如果 Follower 经过一段时间没有收到任何心跳信息,则可以认为 Leader 不存在,需要进行 Leader 选举。

在选举之前,Follower 改变状态为 Candidate 状态并增加其 Term 编号,然后向集群内的其他服务器发出 RequestVote RPC,这个状态持续到发生下面三个中的任意事件:

  1. 赢得选举:Candidate 接受了超过一半服务器的投票,成为新的 Leader,然后向其他服务器发送心跳告诉其他服务器
  2. 另外有服务器获得选举:Candidate 在等待的过程中接收到自称为 Leader 的服务器发送来的 RPC 消息,如果这个 RPC 的 Term 编号大于等于 Candidate 自身的 Term 编号,则 Candidate 承认 Leader,自身状态变成 Follower;否则拒绝承认 Leader,状态依然为 Candidate
  3. 一段时间过去了,没有新的 Leader 产生:出现这种情况则递增 Term,重新发起选举;之所以会出现这种情况,是因为有可能同时又多个 Follower 转为 Candidate 状态,导致选票分流,都没有获得足够的票数

下图说明了选举的详细过程(图片来自网络)。在下图中,绿色的代表 Candidate 状态、红色的代表 Leader 状态而蓝色代表 Follower 状态:

由于 Sentinel 只使用 Raft 协议来选举 Leader 来进行故障转移,并没有使用 Raft 协议进行来进行数据同步,所以对 Raft 协议的介绍就到此为止。如果对 Raft 协议感兴趣的话,可以参考此网站:http://thesecretlivesofdata.com/raft/

接下来我们要对 Sentinel 的源码进行分析

Sentinel 的源码分析

在启动 Redis 时可以传入 “–sentinel” 参数来启动 Sentinel,在 main() 函数中可以看到处理 Sentinel 的逻辑,如下:

在 main() 函数中,首先会判断是否以 Sentinel 模式启动的,如果是就调用 initSentinelConfig() 函数载入 Sentinel 的配置文件,接着调用 initSentinel() 函数初始化 Sentinel 的运行环境。

我们看看 initSentinel() 做了哪些初始化处理吧:

在 initSentinel() 函数中,首先把 Sentinel 服务器可用的命令导入到命令表中,这就是为什么 Sentinel 与普通 Redis 所支持的命令不一样的原因。接着初始化 sentinel 变量的成员,sentinel 的 masters 成员变量保存的是 Sentinel 服务器监控的主 Redis 服务器。

初始化完 Sentinel 的运行环境后,程序会调用 aeMain() 函数进入主循环。这时 Sentinel 服务器主动去连接被监控的 Redis 服务器。

连接被监控的 Redis 服务器在 Sentinel 的定时器中实现的。而这个定时器就是 serverCron() ,我们看看 serverCron() 这个函数的代码:

如果是 Sentinel 模式,serverCron() 函数会调用 sentinelTimer() 函数。我们再来看看 sentinelTimer() 这个函数主要做了哪些工作:

从代码中可以知道,sentinelTimer() 函数主要调用了五个函数。sentinelTimer() 函数是 Sentinel 的核心内容,下面我们会对这个定时器作深入的分析。

TITL 模式

sentinelCheckTiltCondition() 函数用于检查 Sentinel 是否进入 TITL 模式。Sentinel 非常依赖系统时间,例如它会使用系统时间来判断一个 PING 回复用了多久的时间。然而,假如系统时间被修改了,或者是系统十分繁忙,或者是进程堵塞了,Sentinel 可能会出现运行不正常的情况。

当系统的稳定性下降时,TILT 模式是 Sentinel 可以进入的一种的保护模式。当进入 TILT 模式时,Sentinel 会继续监控工作,但是它不会有任何其他动作,它也不会去回应 is-master-down-by-addr 这样的命令了,因为它在 TILT 模式下,检测失效节点的能力已经变得让人不可信任了。如果系统恢复正常,持续 30 秒钟,Sentinel 就会退出 TITL 模式。

我们来看看怎么进入 TITL 模式的:

从代码可知,当时间发生倒退或者处理时间超过 SENTINEL_TITL_TRIGGER(2 秒)时便会进入 TITL 模式。

接下来介绍 监控检测

监控检测

sentinelHandleDictOfRedisInstances() 函数的主要工作是遍历所有 Redis 和 Sentinel 服务器,并调用 sentinelHandleRedisInstance() 对其进行处理,sentinelHandleRedisInstance() 函数的主要工作包括:
1.建立与 Redis 服务器或者其他 Sentinel 服务器的连接
2.发生 info/ping/hello 等消息
3.检查主 Redis 服务器是否下线,如果下线便进行故障转移

下面看看 sentinelHandleRedisInstance() 函数的代码:

在 Sentinel 内部,对于每个 Redis 或者其他 Sentinel 服务器都会使用使用一个 sentinelRedisInstance 的结构体来保存其数据与信息。
sentinelRedisInstance 结构体的成员比较多,我们要注意的是以下几个成员:

sentinels 成员保存的是监控这个服务器的 Sentinel 服务器列表,slaves 成员保存的是这台服务器的从服务器列表(如果是主服务器的话),master 成员保存的是这台服务器的主服务器(如果是从服务器的话)。

我们可以通过下图形象的表示它们之间的关系:

从上图可以知道,Sentinel 通过这个关系可以找到所有监控的服务器(包括主从 Redis 服务器和其他 Sentinel 服务器)。有了这些信息,Sentinel 就可以方便地对这些服务器进行监控和通信。

sentinelHandleRedisInstance() 调用了 sentinelReconnectInstance() 与其他服务器进行连接。sentinelReconnectInstance() 的主要工作是为对端服务器建立连接。下面来看看代码:

sentinelReconnectInstance() 函数首先与对端服务器建立一个命令连接,然后发送一个 ping 命令给对端服务器。如果对端服务器是一个 Redis 实例,那还会建立一个发布订阅连接,用于订阅其 “Hello 频道”。

Sentinel 服务器会定时向 “Hello 频道” 发布一些监测数据,订阅 “Hello 频道” 的服务器可以从中获取 Sentinel 的监测结果。如下图:

Sentinel 服务器向 “Hello 频道” 发布的数据包括:当前 Sentinel 的 IP 地址、端口、runid 和当前配置版本,以及被监控 Reids 的名称、IP、端口和当前配置版本。所以,监听同一台 Redis 服务器的所有 Sentinel 可以通过 “Hello 频道” 来互相交换信息。新加入到集群的 Sentinel 只需要监听 “Hello 频道” 就可以知道所有 Sentinel 的 IP 地址和端口,从而可以主动连接它们,这就是 Sentinel 的自动发现机制。

那么在什么时候 Sentinel 会向 “Hello 频道” 发送消息呢?Sentinel 通过在定时器 sentinelTimer() 中调用 sentinelSendHello() 来想 “Hello 频道” 发送消息。

在 sentinelHandleRedisInstance() 函数中还有一个重要的处理,就是监控 Redis 是否下线。在 sentinelHandleRedisInstance() 函数中调用了 sentinelCheckSubjectivelyDown() 函数,其作用是检查 Redis 服务器是否主观下线。主观下线的意思是指只有当前 Sentinel 认为监控的 Redis 下线了,此时需要询问其他 Sentinel 服务器是否也认为此 Redis 下线才能确认为主观下线。

检测 Redis 是否主观下线的方法是:通过发送 ping 命令给 Redis 服务器,如果 Redis 服务器在一定时间内还没回复,那么就可以认为是主观下线。在定时器 sentinelTimer() 中会调用 sentinelSendPing() 发送 ping 命令给 Redis 服务器,定时发送 ping 命令称为 “心跳” 机制。

我们来看看 sentinelCheckSubjectivelyDown() 代码实现:

从上面的代码可以知道,如果 Redis 服务器在一定的时间内没有响应 ping 命令,那么就把 Redis 标识为主观下线(添加 SRI_S_DOWN 标志)。

如果被监控的服务器是 Redis 主服务器,那么还需要检测其是否客观下线。这因为主观下线有可能不可靠(如 Sentinel 本身网络不通),所以必须通过检测其为客观下线才能认为是真正下线,Sentinel 通过 sentinelCheckObjectivelyDown() 函数检测 Redis 主服务器是否客观下线。代码如下:

从上面的代码首先判断 Redis 主服务器是否主观下线(也就是判断是否被标志位 SRI_S_DOWN),如果被认为是主观下线,那么就遍历监控这台主服务器的所有 Sentinel,然后判断它们是否也认为是主观下线,如果认为主观下线的 Sentinel 数大于 quorum(我们在配置文件设置的,讲解配置的时候讲过),那么就认为是客观下线。

如果 Redis 主服务器被认为是客观下线,那么就开始故障转移。通过调用 sentinelStartFailover() 函数可以一次故障转移。sentinelStartFailover() 代码如下:

sentinelStartFailover() 函数的代码很简单,只是把故障转移状态设置为 SENTINEL_FAILOVER_STATE_WAIT_START 表示等待开始故障转移,而真正的故障转移在 sentinelFailoverStateMachine() 函数中实现。

我们来看看 故障转移 是怎么实现的

故障转移

监控到有 Redis 主服务器宕机(客观下线),Sentinel 就开始进行故障转移。故障转移的目的是把有问题的主服务器摘掉,然后选择一台从服务器提升为主服务器。因为在主从架构中,主服务器负责处理所有的写操作,所以快速完成故障转移可以减少故障带来的损失。故障转移在 sentinelFailoverStateMachine() 函数中实现,其代码如下:

从代码可以看出,sentinelFailoverStateMachine() 是由一个状态机组成的。我们看到状态机有 5 种状态,分别为:

  1. SENTINEL_FAILOVER_STATE_WAIT_START
  2. SENTINEL_FAILOVER_STATE_SELECT_SLAVE
  3. SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
  4. SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
  5. SENTINEL_FAILOVER_STATE_RECONF_SLAVES

这 5 种状态分别对应不同的处理,如 SENTINEL_FAILOVER_STATE_WAIT_START 状态由 sentinelFailoverWaitStart() 函数处理,下面我们分析一下故障转移的处理过程。

WAIT_START 状态处理
Sentinel 集群中由多台 Sentinel 服务器组成,但故障转移处理只需要一台 Sentinel 就可以完成,所以在故障转移处理之前必须选举一台 Leader 来完成。WAIT_START 状态由 sentinelFailoverWaitStart() 函数处理,代码如下:

代码首先判断当前 Sentinel 是否为 Leader,如果不是,故障转移会被延时到下一个定时器触发时再次检测自己是否是 Leader,如果超过一定的时间,那么就调用 sentinelAbortFailover() 函数停止故障转移操作。如果当前 Sentinel 是 Leader,那么就会进入下一个状态 “SELECT_SLAVE”。

前面说过,选举 Leader 是通过 Raft 协议来完成的,关于 Raft 协议前面也作了简单的介绍,所以这里说说 Sentinel 是怎么完成选举操作。

在 Sentinel 检测到 Redis 主服务器客观下线时会发送 “is-master-down-by-addr” 请求给所有监控此 Redis 服务器的 Sentinel,此请求的作用是向其他 Sentinel 询问投票结果。如果还没有投票,那么就会投给发送 “is-master-down-by-addr” 请求的 Sentinel,请求在 sentinelAskMasterStateToOtherSentinels() 函数中发送,代码如下:

这个投票过程跟 Raft 协议是一致的,当其他 Sentinel 接收到此请求后会调用 sentinelVoteLeader() 函数进行投票。sentinelVoteLeader() 代码如下:

如果发送请求的 Sentinel 的 Epoch(对应 Raft 协议的 Term)比较新,那么就会投票给此 Sentinel。通过一轮的投票之后,Sentinel 接着会统计投票结果,如果当前的 Sentinel 获得一半以上的票数,那么此 Sentinel 便会成为 Leader,统计投票结果在 sentinelGetLeader() 函数中实现。代码如下:

虽然 sentinelGetLeader() 的代码有点长,但逻辑比较简单,就是统计监控 Redis 主服务器的所有 Sentinel 的投票结果,如果某个 Sentinel 获取的票数超过一半以上,那么将会成为 Leader。因为故障转移是由 Leader 完成的,所以如果当前 Sentinel 被选中为 Leader,那么便会进入下一个状态。

SELECT_SLAVE 状态处理
SELECT_SLAVE 状态通过 sentinelFailoverSelectSlave() 函数处理,主要完成的工作是从隶属于宕机的主服务器的所有从服务器中选择一台来作为新的主服务器。
选择哪一台作为新主机呢?Sentinel 会通过以下原则来选择:

  1. 不能有下面三个标记中的一个:SRI_S_DOWN|SRI_O_DOWN|SRI_DISCONNECTED。
  2. ping 心跳不能超时。
  3. 优先级不能为 0(slave->slave_priority)。
  4. 回复 INFO 数据不能超时。
  5. 主从连接断线时间不能超过一段时间。

通过上面的条件筛选后可能会得到很多候选服务器,此时还需要按下面的条件排序:

  1. 优先选择优先级高的从机。
  2. 优先选择主从复制偏移量高的从机,即从机从主机复制的数据越多。
  3. 优先选择有 runid 的从机。
  4. 如果上面条件都一样,那么将 runid 按字典顺序排序。

通过上面的筛选过程后最终会得到一台最合适的从服务器来成为新的主服务器,接着 Sentinel 会进入下一个状态 “SLAVEOF_NOONE”。
SLAVEOF_NOONE 状态处理

这个状态由 sentinelFailoverSendSlaveOfNoOne() 函数处理,会向新主服务器发送 slaveof noone 命令,Redis 从服务器接收到此命令后会升级为主服务器。关于 Redis 从服务器升级为主服务器的过程,这里就不作详细的介绍了,有兴趣可以参考 Redis 的源码实现。

发送完 slaveof noone 命令后,Sentinel 便会进入下一个状态 “WAIT_PROMOTION”。

WAIT_PROMOTION 状态处理
WAIT_PROMOTION 状态处理非常简单,就是等待上一个状态的执行结果。判断上一个状态处理是否完毕,是通过向候选主服务器发送 info 命令来获取信息,如果上一个状态执行完毕,那么就会进入下一个状态 “RECONF_SLAVES”。

RECONF_SLAVES 状态处理
这是故障转移状态机的最后一个状态,这个状态的主要处理向其他从服务器发送 slaveof 命令,即通知它们让候选主服务器成为它们的新主机。至此,故障转移操作完毕。