哨兵作用
哨兵(sentinel) 是一个分布式系统,是程序高可用性的一个保障。用于监视任意多个主服务器,以及这些主服务器属下的所有从服务器,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master。
监控
不断地检查master和slave是否正常运行 master存活检测、master与slave运行情况检测。
通知
当被监控地服务器出现问题时,向其他(哨兵间,客户端)发送通知。
自动故障转移
断开master与slave连接,选取一个slave作为master,将其他slave连接到新的master,并告知客户端新的服务器地址。
注意
哨兵也是一台redis服务器,只是不提供数据服务,通常哨兵配置数量为单数
启动哨兵
配置文件
哨兵默认的配置文件 sentinel.conf
一般的以 sentinel_port.conf
命名 哨兵的配置文件
配置信息
port 26379 (端口号)
dir /tmp (哨兵运行信息存储)
monitor mymaster 127.0.0.1 6379 2
# mymaster (master 名字 随意)
# 127.0.0.1 6379 (IP + 端口号)
# 2 (哨兵个数 //2 + 1 当有 2 个哨兵认为 master 挂了 就挂了)
down-after-milliseconds mymaster 30000 (单位 毫秒 )
parallel-syncs mymaster 1 ( 新的master 一次有多少个 slave 同步,设置的越小,完成数据同步的时间越长,响应的服务器压力越小。)
failover-timeout mymaster 180000( 3 分钟 如果没有同步完成 就判定为同步超时)
启动
配置主从结构,以 1master 2 slave为例。
1 先启动 master 和 slave
主从配置 参看 主从篇博客主从
redis-server config_6379.conf
redis-server config_6380.conf
redis-server config_6381.conf
2 启动哨兵
redis-sentinel sentinel_26379.conf
redis-sentinel sentinel_26380.conf
redis-sentinel sentinel_26381.conf
Sentinel 命令
PING:PONG
SENTINEL masters :列出所有被监视的主服务器,以及这些主服务器的当前状态。
SENTINEL slaves :列出给定主服务器的所有从服务器,以及这些从服务器的当前状态。
SENTINEL get-master-addr-by-name : 返回给定名字的主服务器的 IP 地址和端口号。 如果这个主服务器正在执行故障转移操作, 或者针对这个主服务器的故障转移操作已经完成, 那么这个命令返回新的主服务器的 IP 地址和端口号。
SENTINEL reset : 重置所有名字和给定模式 pattern 相匹配的主服务器。 pattern 参数是一个 Glob 风格的模式。 重置操作清楚主服务器目前的所有状态, 包括正在执行中的故障转移, 并移除目前已经发现和关联的, 主服务器的所有从服务器和 Sentinel 。
SENTINEL failover : 当主服务器失效时, 在不询问其他 Sentinel 意见的情况下, 强制开始一次自动故障迁移 (不过发起故障转移的 Sentinel 会向其他 Sentinel 发送一个新的配置,其他 Sentinel 会根据这个配置进行相应的更新)。
初始化Sentinel
初始化服务器
从下面启动代码可以看出启动方式由函数 checkForSentinelMode
来决定,是否使用 sentinel
的模式进行一个启动, 添加的指令也是用的 sentinelcmds
的命令表
int checkForSentinelMode(int argc, char **argv) {
int j;
if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
for (j = 1; j < argc; j++)
if (!strcmp(argv[j],"--sentinel")) return 1;
return 0;
}
// 检查服务器是否以 Sentinel 模式启动
server.sentinel_mode = checkForSentinelMode(argc,argv);
// 初始化服务器
initServerConfig(); // 在第二步介绍该函数
// 如果服务器以 Sentinel 模式启动,那么进行 Sentinel 功能相关的初始化
// 并为要监视的主服务器创建一些相应的数据结构
if (server.sentinel_mode) {
initSentinelConfig();
initSentinel();
}
从源码我们可以看出哨兵的启动有两种方式
redis-sentinel sentinel_xxx.conf
redis-server sentinel_xxx.conf --sentinel
无论哪种方式启动redis
,都会执行 initServerConfig
,不同的是 Sentinel
还会 执行initSentinelConfig
、initSentinel
两个初始化函数。接下来看看这两个函数都干了什么~ 。
替换 Sentinel 的专用代码
initSentinelConfig()
这个函数会用 Sentinel
配置的属性覆盖服务器默认的属性。
void initSentinelConfig(void) {
server.port = REDIS_SENTINEL_PORT;//26379
}
initSentinel()
会进行一个命令表的加载。一个主要的查询命令 INFO
也不同于普通服务器,而是使用一个特殊的版本。
// 初始化服务器 Sentinel 服务器
void initSentinel(void) {
int j;
// 删除 普通 Redis 服务器的命令表(该表用于普通模式)
dictEmpty(server.commands,NULL);
// 添加 sentinel 模式专用的命令。
for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
int retval;
struct redisCommand *cmd = sentinelcmds+j;
retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
redisAssert(retval == DICT_OK);
}
/* 初始化 Sentinel 的状态 这是为了故障转移阶段选取 切换执行者 记录的状态 */
sentinel.current_epoch = 0;
// 保存 主服务器 信息的字典 (这里记录了监测的主服务器的信息)
sentinel.masters = dictCreate(&instancesDictType,NULL);
// 初始化 TILT 模式的相关选项
sentinel.tilt = 0;
sentinel.tilt_start_time = 0;
sentinel.previous_time = mstime();
// 初始化脚本相关选项
sentinel.running_scripts = 0;
sentinel.scripts_queue = listCreate();
}
// sentinel 的指令集合
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
};
初始化 Sentinel 状态
在完成命令表加载之后,紧接着会进行 sentinelState
和 sentinelRedisInstance
结构的一个初始化。
Sentinel
状态中的 masters
字典记录了所有被监视的主服务器信息,键为服务器名字,值为被监视主服务器对应的sentinel.c/sentinelRedisInstance
结构。每个sentinelRedisInstance
实例结构代表监视一个Redis
服务器实例,这个实例可以是主服务器,也可以是从服务器,或者另外一个sentinel服务器。
对于sentinelState
的初始化将引发对masters
字典的初始化,而masters字典的初始化是根据被该入的sentinel
配置文件(sentinel_26379.conf
)来进行的。主要为被监控 master
的ip
和 port
。
注意 这些都是有 sentinel 来维护和使用的。
sentinelState
struct sentinelState {
// 当前纪元 用做故障转移
uint64_t current_epoch; /* Current epoch. */
// 保存了所有被这个 sentinel 监视的主服务器
// 字典的键是主服务器的名字
// 字典的值则是一个指向 sentinelRedisInstance 结构的指针,可以是主服务器,从服务器或者其他sentinel节点
dict *masters; /* Dictionary of master sentinelRedisInstances.
Key is the instance name, value is the
sentinelRedisInstance structure pointer. */
// 是否进入了 TILT 模式?
int tilt; /* Are we in TILT mode? */
// 目前正在执行的脚本的数量
int running_scripts; /* Number of scripts in execution right now. */
// 进入 TILT 模式的时间
mstime_t tilt_start_time; /* When TITL started. */
// 最后一次执行时间处理器的时间
mstime_t previous_time; /* Last time we ran the time handler. */
// 一个 FIFO 队列,包含了所有需要执行的用户脚本
list *scripts_queue; /* Queue of user scripts to execute. */
} sentinel;
sentinelRedisInstance
name
实例的名字
主服务器的名字由用户在配置文件中设置
从服务器以及 Sentinel 的名字由 Sentinel 自动设置
格式为 ip:port ,例如 "127.0.0.1:26379"
runid
实例的运行 ID
sentinelAddr
实例的地址
主服务器实例特有的属性
sentinels
其他同样监控这个主服务器的所有 sentinel
slaves
如果这个实例代表的是一个主服务器
那么这个字典保存着主服务器属下的从服务器
字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构
quorum
判断这个实例为客观下线(objectively down)所需的支持投票数量
parallel_syncs
SENTINEL parallel-syncs
在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
auth_pass
连接主服务器和从服务器所需的密码
从服务器实例特有的属性
master_link_down_time
主从服务器连接断开的时间
slave_priority
从服务器优先级
slave_reconf_sent_time
执行故障转移操作时,从服务器发送 SLAVEOF
master
主服务器的实例(在本实例为从服务器时使用)
slave_master_host
INFO 命令的回复中记录的主服务器 IP
slave_master_port
INFO 命令的回复中记录的主服务器端口号
slave_master_link_status
INFO 命令的回复中记录的主从服务器连接状态
slave_repl_offset
从服务器的复制偏移量
结构中的 sentinelAddr
保存着对象的 地址和端口。
/* Address object, used to describe an ip:port pair. */
/* 地址对象,用于保存 IP 地址和端口 */
typedef struct sentinelAddr {
char *ip;
int port;
} sentinelAddr;
建立连接
sentinel 会先去连接 sentinel
中 masters
中的每一个 master
,并在每一个 master
和 sentinel
之间创建两个异步连接 一个 命令连接
一个 订阅链接
。此时 sentinel将成为 master 的客户端它可以向主服务器发送命令,并从命令回复中获取相关信息。
命令连接
专门用于向主服务器发送命令,并接收命令回复。比如sentinel向主服务器发送INFO
命令。
订阅连接
专门用于订阅主服务器的 _sentinel_:hello
频道。 比如 sentinel
向主,从,其它sentinel
发送sentinel
本身和主库信息。
redis在发布与订阅功能中,被发送的信息都不会保存在redis服务器中,若消息到来时,需要接收的客户端不在线或者断线,那么这个客户端就会丢失这条信息。为了不丢失_sentinel_:hello
频道的任何信息,sentinel
必须专门的用一个订阅连接来接收该频道的信息。
获取主服务器信息
Sentinel
默认会以每10
秒一次的频率向主服务器发送INFO
命令,通过分析命令回复来获取主服务器的当前信息。Sentinel可以获取以下两方面的信息:
1主服务器本身的信息,包括服务器run_id,role的服务器角色。
2 主服务器对应的所有从服务器
的信息(从服务器IP和端口)。
获取从服务器信息
当Sentinel
发现有新的从服务器出现时,Sentinel
除了会为这个新的从服务器创建相应的实例结构(sentinelRedisInstance
)之外,还会创建到从服务器的命令连接
和订阅连接
。
Sentinel
依然会像对待主服务器那样,每10s 发送一个INFO
命令来获取从服务器的当前信息。
run_id、role、ip、port 、master_link_status(主从服务器的连接状态)、slave_priority(从服务器的优先级)等信息。
向主从服务器发送信息
在默认情况下, Sentinel
会以每2秒一次的频率,通过命令连接向,所有被监视的主服务器和从服务器发送以下格式的命令:
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
这条命令向服务器的_sentinel_:hello
频道发送了一条信息,信息的内容由多个参数组成:
(1) s_
开头的参数记录的是sentinel
本身的信息。
(2) m_
开头的参数记录的则是主服务器的信息,如果sentinel正在监视的是主服务器,那么这些参数就是主服务器的信息,如果sentinel正在监视的是从服务器,那么这些参数记录就是从服务器正在复制的主服务器的信息。
参数 | 描述 |
---|---|
S_ip | Sentinel的ip地址 |
S_port | Sentinel的端口号 |
S_runid | Sentinel的运行ID |
S_epoch | Sentinel 的当前配置纪元 |
m_name | 主服务器的名字 |
M_ip | 主服务器的IP地址 |
M_port | 主服务器的端口号 |
M_epoch | 主服务器的当前配置纪元 |
例如
"127.0.0.1,26379,e955b4c77398ef6b5f055bc7ebfd5e828dbed4fc,0,mymaster,127.0.0.1,6379,0"
# --------------------------------解释------------------------------------------
127.0.0.1 # sentinel ip 地址
26379 # sentinel 端口号
e955b4c77398ef6b5f055bc7ebfd5e828dbed4fc # sentinel的运行 id
0 # sentinel 当前配置纪元
mymaster # sentinel 监控的 master name
127.0.0.1 # master ip 地址
6379 # master 端口号
0 # master 当前配置纪元
接收来自主从服务器的频道信息
当Sentinel
与一个主服务器或者从服务器建立起订阅连接之后,Sentinel
就会通过订阅连接
向服务器发送 subscribe_sentinel_:hello
。
对于每个与 Sentinel
连接的服务器,Sentinel
既通过命令连向服务器的_sentinel_:hello
频道发送信息,又通过订阅连接从服务器的_sentinel_:hello
频道接收信息。
因此当有新的Sentinel
连接进来时, 向订阅连接中发送的 subscribe_sentinel_:hello
被已有的Sentinel
接收(同时自己也会接受到来自自己的这条消息)。
// 发送 PUBLISH 命令的间隔
#define SENTINEL_PUBLISH_PERIOD 2000
if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
/* PUBLISH hello messages to all the three kinds of instances. */
sentinelSendHello(ri);
}
/* 接收来自主服务器和从服务器的频道信息
当 sentinel 与一个主服务器或者从服务器建立起订阅连接之后, sentinel 就会通过订阅连接,向服务器发送以下命令:
*/
SUBSCRIBE __sentinel__:hello
/* Now we subscribe to the Sentinels "Hello" channel. */
// 发送 SUBSCRIBE __sentinel__:hello 命令,订阅频道
retval = redisAsyncCommand(ri->pc,
sentinelReceiveHelloMessages, NULL, "SUBSCRIBE %s",
SENTINEL_HELLO_CHANNEL);
当一个Sentinel
从_sentinel_:hello
频道收到一条信息时,Sentinel
会对这条信息进行分析,提取出信息中 ip 、port、run_id 等8个参数,并进行以下检查:如果这条消息是自己发的,就直接忽略。如果是新进来的Sentinel
, 此时Sentinel 会对 对应的主服务器实例结构进行更新,即将新加进来的 Sentinel
添加到 sentinels
字典中。
每个Sentinel
都有自己的一个sentinels
字典,Sentinels
字典信息保存了除自己之外的所有Sentinel
信息。
下线状态
对于Redis的Sentinel中关于下线有两个不同的概念:(1)主观下线(Subjectively Down, 简称 Sdown) 指的是单个 Sentinel 实例对服务器做出的下线判断,此时不会进行故障转移。(2) 客观下线(Objectively Down, 简称 Odown)指的是多个 Sentinel 实例在对同一个服务器做出 Sdown 判断,此时目标sentinel会对主服务器进行故障转移。本篇具体详细介绍。
主观下线状态
默认的Sentinel
会以每秒一次的频率向所有与它创建命令连接的实例(包括主、从、其他sentinel在内)发送PING
命令,并通过实例回复来判断实例是否在线。
合法的回复
+pong
、 -loading
、 -masterdown
。
无效回复
除此之外的所有回复或者无回复都被视作无效回复。无回复指在指定的时间内没有回复就认为是无回复。
down-after-milliseconds # 指定的时间 未收到回复 视为无效
用户设置down-after-milliseconds选项的值,不仅会被sentinel用来判断主服务器的主观下线状态,还会被用于判断主服务器下的所有从服务器,以及同样监视主服务器的其他sentinel的主观下线状态。
-- 例如用户向sentinel设置以了下配置:
sentinel monitor master 127.0.0.1 6379 2
sentinel down-after-milliseconds master 50000
这里的master
是主服务器的名称, 端口默认6379
,2
代表sentinel
集群中有2
个sentinel
认为master
状态下线时,才能真正认为该master已经不可用了(也就是客观下线
)。
这50000毫秒不仅会成为sentinel
判断master
进入主观下线的标准,还会判断所有从库
、其它sentinel
进入主观下线的标准。
当多个sentinel设置的主观下线时长可能不同
对于多个sentinel
共同监视同一个主服务器时,这些sentinel
在配置文件sentinle.conf
中所设置的down-after-milliseconds
值也可能不同,因此当一个sentinel
将主服务器判断为主观下线时,其它sentinel
可能仍然会认为主服务器处于在线状态。只有全部的sentine
都判断进入了主观下线状态时,才会认为主master
进入了主观下线状态。
客观下线状态
当Sentinel
将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,会向同样监视这一主服务器的其它Sentinel
进行询问,当有半数以上(看具体配置, 一般的是半数以上 例如sentinel monitor mymaster 127.0.0.1 6379 2
中 就为当 2
个判定下线时,就认为时客观下线了)
当master
, 被确定客观下线之后sentinel
们 会选出一个 决策者 去执行故障转移操作。客观下线条件只适用于主服务器
。
is-master-down-by-addr
命令用来判断是否客观下线
sentinel is-master-down-by-addr ip port current_epoch run_id
sentinel
当前的配置纪元 current_epoch
用于选举 决策者 sentinel, run_id
可以是*或者sentinel的 运行id。
决策者选取
假设现在有4个sentinel
这四个sentinel
既是投票者,也是候选者(这四个必须时健康的)。
1 不能有下面三个标记中的一个:SRI_S_DOWN|SRI_O_DOWN|SRI_DISCONNECTED
2 ping 心跳正常
3 优先级不能为 0(slave->slave_priority)
4 INFO 数据不能超时
5 主从连接断线会时间不能超时
投票的过程很简单,每个sentinel
都将自己的ip
、 port
、current_epoch
、run_id
由 is-master-down
发送到 hello
频道。
sentinel
第一个获取到谁的 is-master-down
信息, 就将自己的票投给对应的sentinel
。
一次过后 current_epoch
最大的,且超过了半数以上。则被选为决策者 否则再来一轮,每增加一轮 current_epoch + 1
, 直到选出为止。
故障转移
选取候选Slave
1 在线的
2 响应速度快的
3 与原 master 断开连接最短的
4 优先原则
优先级>offset>runid
最终选取出 新的 master
之后向新的 master
发送
slaveof no one # 断开主从
然后声明新的master
slaveof ip port # 发送新的IP 和 新的port
最后将原来的 master 作为从机。当重新上线时,sentinel
会发送 salveof
命令使其成为从机。
总结
-
sentinel
只是一个运行在特殊模式下的redis
服务器,它使用了和普通模式不同的命令表,以及区别与普通模式下使用的命令不同。 -
sentinel
向主服务器发送INFO
命令来获得主服务器属下所有从服务器的地址信息,并为这些从服务器创建相应的实例结构,以及连向这些从服务器的命令连接和订阅连接。 -
一般情况下,
sentinel
以每10秒一次的频率向被监视的主服务器和从服务器发送INFO
命令,当主服务器处于下线状态,或者sentinel
正在对主服务器进行故障转移操作时,sentinel
向从服务器发送INFO
命令的频率会改为1秒一次。 -
对于监视同一个主服务器和从服务器的多个
sentinel
来说,它们会以每2秒一次的频率,通过向被监视的_sentinel_:hello
频道发送消息来向其他sentinel
宣告自己的存在。 -
每个
sentinel
也会从_sentinel_:hello
中频道中接收其他sentinel
发来的信息,并根据这些信息为其他sentinel
创建相应的实例结构,以及命令连接。 -
sentinel
只会与主服务器和从服务器创建命令连接和订阅连接,sentinel
与sentinel
之间则只创建命令连接。 -
sentinel
以每秒一次的频率向实例(包括主,从,其它sentinel
)发送PING
命令,并根据实例的回复来判断实例是否在线,当一个实例在指定的时长中连续向sentinel
发送无效回复时,sentinel
会将这个实例判断为主观下线。 -
当
sentinel
将一个主服务器判断为主观下线时,它会向同样的监视这个主服务器的其他sentinel
进行询问,看它们是否同意这个主服务器已经进入主观下线状态。 -
当
sentinel
收集到足够多的主观下线投票之后,它会将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作。