哨兵的用途(Sentinel)
哨兵是redis 高可用的解决方案,由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器以及这些主服务器属下的所有从服务器,它能够在被监视的主服务器下线时,自动将该主服务器属下的某个优秀的从服务器升级为新的主服务器,由这个新的主服务器代替已下线的主服务器继续处理命令请求
举个例子:
可以将哨兵系统理解为一个大公司的人事监管系统,人事部门负责监管整个公司各部门领导(主服务器)以及各领导属下的员工(从服务器)
当某个部门的领导离职后,人事部门就会从该离职领导的下属中挑选出一个优秀的人才来当选新的领导,新领导当选的过程是需要负责管理该部门的多个人事之间通过一定的方式推选出来,不能某一个人事说了算
下面将详细说明整个过程中的细节部分
何为哨兵
哨兵本质上是一个运行在特殊模式下的redis服务器
这里需要注意的是哨兵和普通redis服务器所执行的工作是不同的,因此一个哨兵的启动过程也会和普通redis服务器有所区别
比如普通redis服务器在初始化时会通过载入RBD或AOF文件来还原数据库状态,而哨兵并不使用数据库,所以哨兵在初始化阶段不需要载入以上文件
再比如哨兵服务器不需要处理普通redis服务器的命令(SET,GET,SETNX...)等,而是有自己专用的命令,如(PING,INFO, SENTINEL...),还有哨兵服务器的服务器端口默认是26379等等,故哨兵在启动的第二步骤是将一部分普通redis服务器所使用的代码替换为哨兵专用代码
这也是为什么为什么在哨兵模式下,服务器执行不了诸如SET,EVAL等命令的原因,因为哨兵服务器并没有载入这些命令表
哨兵是如何监视redis服务器的
哨兵服务器初始化完成后,服务器会初始化一个叫做Sentinel状态的结构即sentinelState,该结构保存了哨兵服务器中所有和哨兵功能有关的状态记录
struct sentinelState {
// 保存了所有被这个sentinel监视的服务器
// 字典的键是服务器的名字
// 字典的值则是一个指向sentinelRedisInstance结构的指针
dict *masters
// 一个INFO队列,包含了所有需要执行的用户脚本
list *scripts_queue;
// ...
}
sentinelRedisInstance结构
typedef struct sentinelRedisInstance {
// 标识值,记录了实例的类型,以及该实例当前的状态
int flags;
// 当flags=SRI_MASTER(主服务器)时,会有一个从服务字典,用于记录该主服务器的所有从服务器信息
dict slaves
// 实例的名字
char *name;
// 实例运行的ID
char *runid;
// 实例的地址信息
sentinelAddr *addr;
// 实例无响应多少毫秒之后才会被判断主观下线
mstime_t down_after_period;
// 判断该实例客观下线所需的票数
int quorum;
// ...
}
sentinelAddr 结构
typedef struct sentinelAddr {
// IP地址
char *ip;
// 端口号
int port;
}
有了以上结构作为基础,哨兵状态实例就可以监视所有的服务器了
初始化哨兵服务器的最后一步是创建连向被监视主服务器的网络连接,也即哨兵服务器会成为它监视的主服务器的客户端,哨兵服务器可以向主服务器发送命令,并接收主服务器返回的信息
这里需要注意的是,对于每一个被哨兵监视的主服务器来说,哨兵会创建两个连向主服务器的一步网络连接:
-
命令连接
专门用于向主服务器发送命令,并接收回复 -
订阅连接
专门用于订阅主服务器的sentinel:hello频道,该连接的作用后面会介绍,这里大家可以先猜想一下
哨兵如何获取主服务器信息
哨兵默认会以十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并解析INFO命令返回的主服务器当前的信息
我们简单看一下INFO命令回复的信息有哪些
# Server
...
run_id: xxxxxxxxxxxxxx
...
# Replication
role: master
...
slave0: ip=127.0.0.1,port=2379,state=online,offset=43,lag=0
slave0: ip=127.0.0.1,port=2380,state=online,offset=43,lag=0
slave0: ip=127.0.0.1,port=2381,state=online,offset=43,lag=0
...
# Other
...
哨兵通过分析以上主服务器返回的INFO回复,就可以知道两个方面的信息
-
被监视主服务自身信息,运行id,服务器角色等
-
被监视主服务器属下所有从服务器信息,包括ip地址,端口号等,通过这些信息,哨兵无需用户提供从服务器的信息,就可以实现自动发现从服务器
哨兵会根据以上两点信息更新主服务的运行id(可能主服务器重启了),和该主服务器的所有从服务器信息(可能该主服务器的从服务器数量发生变动)
并会将所有从服务器信息保存在该主服务器实例结构的slaves字典中,字典的key为哨兵以从服务器的ip:port形式的字符串,而值也是一个sentinelRedisInstance实例,只是该实例的flags为SRI_SLAVE
哨兵如何处理主服务器的下属-从服务器
当哨兵发现主服务器又新的从服务器时,哨兵除了会为这个新的从服务器创建对应的实例结构外,还会创建连接到从服务器的命令连接以及订阅连接
同理,哨兵也会以默认十秒一次向从服务器发送INFO命令,下面我们看一下从服务器返回的INFO信息和主服务器返回的有何不同之处
# Server
...
run_id: xxxxxxxxxxxxxx
...
# Replication
role: slave
master_host: 127.0.0.1
master_port: 6379
master_link_statue: up
slave_repl_offset: 11878
slave_priority: 100
# Other
...
可以看到从服务器的INFO返回信息和主服务器返回的还是又很大差别的,通过从服务器返回的信息,哨兵可以提取出以下信息:
-
从服务器的运行id
-
从服务器的运行角色
-
从服务器所复制的主服务器的ip地址,以及端口号
-
主从服务器的连接状态
-
从服务器的复制偏移量
-
从服务器的优先级
根据以上信息,哨兵会以每十秒一次的频率更新从服务的实例状态信息
那么哨兵除了每十秒向主,从服务器发送一次INFO命令之外,还会做什么其他的事情呢?
集体宣示
在默认情况下,哨兵会以每两秒一次的频率,通过命令连接向所有被监视的主,从服务器的 sentinel:hello频道 发送 "主权宣示"命令,告诉所有人我是谁(我的ip+port+runid...)以及我所监视的主服务器是哪个(主服务器的name+ip+port...)
另外之前说过,哨兵会和它监视的每一个主/从服务器建立订阅sentinel:hello频道的连接,也就是说:
对于每一个和sentinel连接的服务器,哨兵既通过命令连接以默认每两秒的频率向所有服务器的sentinel:hello频道发送主权宣示,还会通过订阅该服务器的sentinel:hello频道以接收该频道的其他哨兵发来的宣示主权消息
因此:
对于监视同一个服务器的多个哨兵来说,一个哨兵发送的宣示主权消息会被其他的哨兵接收到,这些信息会被用于哨兵之间的相互存在认知,即哨兵于哨兵之间可以通过订阅相同频道而实现相互之间的存在感知,这正是上面遗留关于订阅连接问题的答案
哨兵为主服务器创建的实例结构中,有一个sentinels字典,该字典用于保存除了自己之外,所有同样监视该主服务器的其他哨兵信息,而通过订阅连接,就可以实现该字典的每两秒更新一次
同源哨兵互联
通过以上分析,我们知道当一个哨兵通过频道信息发现另一个同源哨兵(监视同一个主服务器的哨兵)时,不仅会在该主服务的实例结构中为其创建相应的实例结构,还会创建一个连接该同源哨兵的命令连接,最终形成一个相互连接的哨兵网络,这也为下文中的检测客观下线以及选举领头哨兵做了铺垫
哨兵作用来了
默认情况下,哨兵会以每一秒一次的频率向所有与它建立了命令连接的实例(主,从,其他哨兵)发送PING命令,并通过实例返回的回复来判断实例是否在线
检测主观下线
哨兵配置文件中的down-after-milliseconds选项指定了该哨兵判断某监视实例进入主观下线所需的时间长度,举个例子
如果该值设置为了5000毫秒,那么在5000毫秒之内,若一个实例连续向哨兵返回无效PING回复,那么哨兵就会将该sentinelRedisInstance的flags标识置为SRI_S_DOWN
检测客端下线
当某个哨兵判断一个主服务器主观下线之后,为了确认该主服务器是否时真的下线,它自己一个人是决定不了的,它需要向同样监视这一主服务器的其他哨兵询问,看它们是否也认为该主服务器已经下线,当该哨兵从其他的哨兵那里得到了足够数量的已下线判断之后,该哨兵就会将该主服务器判断为客观下线
询问的命令格式如下:
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
其中runid为或者自身运行id,若为表示这仅仅是一次询问客观下线,若为运行id则表示这是一次选举领头哨兵操作
被询问哨兵会根据ip,port检查主服务器是否已下线,并返回以下格式:
-
down_state :1标识已下线,0标识未下线
-
leader_runid :* 或者 局部领头哨兵的运行id,* 代表这仅仅是一次检测主服务器是否下线,而运行id则表示这是一次领头哨兵的选举操作
-
leader_epoch:若leader_runid为* ,则该值无效(为0),若leader_runid不为*,则该值表示该哨兵认为的局部领头哨兵的配置纪元
根据同源其他哨兵的返回信息,发起询问的哨兵会统计出所有返回信息中已同意下线数量,若这一数量达到配置指定的判断客观下线所需的数量时,该哨兵会将主服务器实例结构的flags属性的SRI_O_DOWN标识打开,代表该主服务器已经进入客观下线了(gg了)
选举领头哨兵
当一个主服务器被某个哨兵判断为客观下线时,所有监视该下线的服务器的哨兵们会进行一场协商选举,选举出领头哨兵,将由该领头哨兵全权负责对下线主服务器进行故障转移操作
如何选举?
- 所有监视该下线主服务器的在线哨兵都有资格成为领头哨兵
- 如果在给定时限内,没有一个哨兵被选举成功,那么将在一段时间后重新选举,直到选出领头哨兵为止
- 每次选举之后,不论是否选举成功,所有参与选举哨兵的配置纪元值都会自增一次,配置纪元实际上就是一个计数器,没有实际特别之处
- 每个发现主服务器进入客观下线的哨兵都会要求其他哨兵将自己设置为局部领头哨兵
- 每一个哨兵设置其局部领头哨兵的原则时先到先得,之后的请求都会被拒绝
- 若某个哨兵被半数以上的哨兵设置成了局部领头哨兵,那么该哨兵就会成为领头哨兵
总结一句话就是谁先成功通知其他半数以上的哨兵将自己设置为局部领头哨兵,谁就行成为真正的领头哨兵
故障转移
所谓故障转移就是:
-
领头哨兵在已下线的主服务器的所有从服务器中,挑选出一个状态良好,数据完整的从服务器
-
向该从服务器发送SLAVEOF no one 命令,将该从服务器转为主服务器
-
发送完命令后,领头哨兵会以默认每秒一次的频率向该从服务器发送INFO命令,并判断返回信息中的role信息,当role信息变为master时,领头哨兵就知道被选中的从服务器已经顺利升级为主服务器了
-
让已下线主服务器所属的所有从服务器去复制新的主服务器,即通过向所有从服务器发送SLAVEOF 命令实现
-
最后,领头哨兵会将已下线的主服务器设置为新的主服务器的从服务器
以上就是整个故障转移的步骤
最后简单说一下选举某个状态良好,数据完整的从服务器的规则
1:剔除所有已下线或断线状态的从服务器
2:剔除最近5秒没有回复过领头哨兵INFO命令的从服务器
3:领头哨兵根据从服务器的优先级,对列表中剩余的从服务器进行排序,选出优先级最高的从服务器,若有多个从服务器的优先级相同,则领头哨兵会根据从服务的复制偏移量大小进行排序,选出最大的复制偏移量的从服务器
4:若有多个优先级最高,复制偏移量最大的从服务器,那么领头哨兵会按照运行ID排序,选出运行ID最小的从服务器作为被选中的下一任主服务器
总结
以上就是整个哨兵系统在redis中的原理,其中选举领头哨兵的算法叫做Raft算法,感兴趣的同学可以继续研究研究
网友评论