上一篇 我们模拟了单机器下哨兵模式的搭建,那么接下来我们看下哨兵模式的实现与工作。
为什么又分成两篇呢?因为篇幅太长(偷懒),再一个这篇主要说的是Sentinel的初始化以及信息交换,下一篇着重说下状态检查、Sentinel头领选举与故障转移 。
启动并初始化Sentinel
当一个Sentinel启动时,需要执行以下步骤:
(1)初始化服务器。
因为Sentinel本事上是一个运行在特殊模式下的Redis服务器,所以启动时的第一步也就是初始化一个普通的Redis服务器。不同点是在初始化的时候不会载入RDB或AOF文件。
(2)将普通的Redis服务器使用的代码替换成Sentinel专用代码。
端口替换Redis服务器使用redis.h/REDIS_SERVERPORT(6379)常量作为服务器端口,而Sentinel则使用sentinel.c/REDIS_SENTINEL_PORT(26379)常量作为服务器端口。
另外Redis服务器使用redisCommandTable服务器作为命令表,而Sentinel则使用sentinelcmds作为服务器的命令表,并且其中的info命令会使用Sentinel模式下专用的sentinelInfoCommand函数,而不是普通redis服务器使用的redis.c/infoCommand函数。
(3)初始化Sentinel状态。
服务器会初始化一个sentinel.c/sentinelState结构('Sentinel状态'),用于保存服务器中所有和Sentinel功能有关的状态。
struct sentinelState{ //当前纪元,用于实现故障转移 uint64_t current_epoch; //保存了所有被这个sentinel监视的主服务器 //字典的键是主服务器的名字 //字典中的值是一个指向sentinelRedisInstance结构的指针 dict *masters; //是否进入TILT模式 int tilt; //目前正在执行的脚本数量 int running_scripts; //进图TILT模式的时间 mstime_t tilt_start_time; //一个FIFO队列,包含了所有需要执行的用户脚本 list *scripts_queue; } sentinel;
(4)根据给定的配置文件,初始化Sentinel的监视主服务器列表。
sentinelRedisInstance结构(实例结构)
typedef struct sentinelRedisInstance{ //标识值,记录了实例的类型以及该实例的当前状态 int flag; //实例的名称(主服务器由用户配置,从服务器自动设置ip:port) char *name; //实例的运行id char *runid; //配置纪元,用户实现故障转移 uint64_t config_epoch; //实例的地址 //sentinelAddr *addr; //实例无相应多少毫秒之后才会被判断主观下线 mstime_t down_after_period‘; //判断这个实例为客观下线所需要支持的投票数量 int quorun; //在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量 int parallel_syncs; //刷新故障迁移状态的最大时间限制 mstime_t fallover_timeout; } sentinelRedisInstance;
*addr 指向sentinelAddr结构:
typedef struct sentinelAddr{ char *ip; int port; } sentinelAddr;
根据用户的配置初始化实例结构:
#名称 ip port
sentinel monitor mymaster 127.0.0.1 6379 2
#实例在相应多少毫秒之后才会被判断主观下线
sentinel down-after-milliseconds mymaster 3000
sentinel failover-timeout mymaster 10000
#在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
sentinel parallel-syncs mymaster 1
(5)创建连向主服务器的网络连接。
Sentinel将称为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息:对于被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接,一个是命令连接这个连接专门用于向主服务器发送命令,并接收命令回复;另一个是订阅连接这个连接专门用于订阅主服务器的_sentinel_:hollo频道。
获取主服务器信息
sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。
通过分析主服务器返回的INFO命令回复,Sentinel可以获取以下两个方面的信息:
一方面是关于主服务器本身的信息,包括run_id域记录的服务器运行ID以及role域记录的服务器角色;另一方面是关于主服务器属下的所有从服务器信息,每个从服务器由一个“slave”字符串开头的行记录,每行的ip=域记录了从服务器的IP地址,而port=域则记录了从服务器的端口号。根据这些IP地址和端口号,Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器。
主服务器 实力结构的flags属性的值为SRI_MASTER,而从服务器实例结构的flags属性的值为SRI_SLAVE。
当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。
在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令,PUBLISH _sentinel_:hello "< s_ip > < s_port >< s_runid >< s_epoch > < m_name > < m_ip >< m_ip ><m_epoch>"。
接收来自主服务器和从服务器的频道信息
当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令:SUBSCRIBE __semtomel__:hello
当一个Sentinel从__sentinel__:hello频道收到一条信息时,Sentinel会对这条信息进行分析,提取出信息中的Sentinel IP地址,Sentinel端口号,Sentinel运行ID等八个参数,并进行检查:如果信息中记录的Sentinel运行ID和基尔兽信息的Sentinel的运行ID相同,那么说明这条信息时Sentinel自己发送的,Sentinel将丢弃这条信息,不做处理,反之如果记录的Sentinel运行ID和接收消息的Sentinel的运行ID不相同,那么说明这条信息时监视同一个服务器的其他Sentinel发来的,接收信息的Sentinel将根据这些信息中的参数,对相应主服务器的实例结构进行更新。
Sentinel为主服务器创建的实例结构中的sentinels字典中除了保存本身以外,还会保存同样监视这个主服务器的其他Sentinel的资料,其中字典键为 ip:port 值为Sentinel实例结构。
如果目标Sentinel(接收消息的Sentinel)接收到源Sentinel(发送消息的Sentinel)的消息,提取其中的参数检查主服务器实例结构的sentinels字典中源Sentinel实例结构是否存在,如果存在则更新,不存在则创建实例结构并存储到主服务器的sentinels字典中。并且,目标Sentinel还会创建连接向源Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成互相连接的网络。
但是Sentinel之间不对创建订阅连接,这是因为Sentinel需要通过接收主服务器或者从服务器发来的频道信息发现未知的新Sentinel,所以才需要建立订阅连接,而相互已知的Sentinel只要使用命令连接来进行通信就足够了。
每天学一点,总会有收获。
下篇我们看下Redis的Sentinel(哨兵)的状态检查、Sentinel头领选举与故障转移。
说明:尊重作者知识产权,文中内容参考《Redis设计与实现》,仅在此做学习与大家分享。