详解 Redis 哨兵模式(一)
哨兵模式是提高 Redis 可用性的一种方式,本文是哨兵模式系列的第一篇文章,主要介绍哨兵模式的启动过程。
哨兵其实是 Redis Sever
的一种特殊模式。哨兵实例的初始化入口也是 mian
。下面我们来分析哨兵模式下的启动过程。
哨兵实例初始化
Redis
首先会调用 checkForSentinelMode
函数来判断当前是否以哨兵模式来运行,并把标识赋值到 server.sentinel_mode
。
server.sentinel_mode = checkForSentinelMode(argc,argv);
我们来看一下 checkForSentinelMode
是如何判断当前是否以哨兵模式来运行:
int checkForSentinelMode(int argc, char **argv) {
int j;
// 判断第一个参数是不是 redis-sentinel
if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
// 判断其他的参数是不是 --sentinel
for (j = 1; j < argc; j++)
if (!strcmp(argv[j],"--sentinel")) return 1;
return 0;
}
可以看出它是通过两个条件来判断的:
- 执行的命令是否为
redis-sentinel
。 - 命令参数中是否含有
--sentinel
。
这就对应了我们在命令行中启动哨兵实例的两种方式,一是直接运行 redis-sentinel
命令,另一种是运行 redis-server
命令并且参数中含有 --sentinel
参数。如下所示:
redis-sentinel sentinel.conf⽂件路径
redis-server sentinel.conf⽂件路径 --sentinel
当这两个条件满足一个时就会被认为是以哨兵模式运行当前实例,这时会将 server.sentinel_mode
的值设置为 1,这个值会被用来判断当前实例是否以是否以哨兵模式运行。
初始化配置项
根据上一步中 server.sentinel_mode
的值判断当前是否以哨兵模式初始化。
// 是否是哨兵模式
if (server.sentinel_mode) {
initSentinelConfig();
initSentinel();
}
我们看到当为哨兵模式时会调用 initSentinelConfig
和 initSentinel
函数来完成哨兵的初始化。
initSentinelConfig
函数主要把将 server
的端口号改为 REDIS_SENTINEL_PORT
,这个宏定义的值为 26379;然后把 server.protected_mode
改为 0,表示允许外部连接哨兵实例。
void initSentinelConfig(void) {
server.port = REDIS_SENTINEL_PORT;
server.protected_mode = 0; /* Sentinel must be exposed. */
}
initSentinel
函数主要做了两部分工作。
首先是替换 server
执⾏的命令表。因为哨兵是 Redis
的一种特殊模式,它所能执行的命令和 Redis
实例是有所区别的,哨兵实例执行的⼀些命令,其名称虽然和 Redis
实例命令表中的命令名称⼀样,但它们的实现函数是针对哨兵实例专门实现的。
void initSentinel(void) {
...
// 清空当前命令表
dictEmpty(server.commands,NULL);
// 将哨兵模式命令表复制到当前实例可执行的命令表中
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);
serverAssert(retval == DICT_OK);
}
...
}
// 哨兵模式命令表
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},
{"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
{"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
{"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0}
};
其次 initSentinel
函数会初始化哨兵实例的各种属性信息。主要是初始化 sentinelState
结构体里面的数据。我们来看一下 sentinelState
有哪些数据:
struct sentinelState {
// 哨兵实例 ID
char myid[CONFIG_RUN_ID_SIZE+1]; /* This sentinel ID. */
// 当前纪元
uint64_t current_epoch; /* Current epoch. */
// 监听的所有主节点的哈希表
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. */
// 保存脚本的队列
list *scripts_queue; /* Queue of user scripts to execute. */
// 向其他哨兵实例发送的 IP 地址
char *announce_ip; /* IP addr that is gossiped to other sentinels if
not NULL. */
// /向其他哨兵实例发送的端口号
int announce_port; /* Port that is gossiped to other sentinels if
non zero. */
...
} sentinel;
启动哨兵实例
哨兵初始化之后就会调用 sentinelIsRunning
函数来启动哨兵实例。
if (!server.sentinel_mode) {
...
} else {
InitServerLast();
sentinelIsRunning();
}
sentinelIsRunning
函数主要的逻辑是首先校验哨兵的配置文件是否存在且可以正常写入;其次会校验哨兵实例是否设置了 ID,如果没有设置则会随机生成一个 ID;最后会调用 sentinelGenerateInitialMonitorEvents
函数给每个监听的主节点发送事件消息。
void sentinelIsRunning(void) {
int j;
// 校验配置文件
if (server.configfile == NULL) {
serverLog(LL_WARNING,
"Sentinel started without a config file. Exiting...");
exit(1);
} else if (access(server.configfile,W_OK) == -1) {
serverLog(LL_WARNING,
"Sentinel config file %s is not writable: %s. Exiting...",
server.configfile,strerror(errno));
exit(1);
}
// 如果没有 ID 则会随机生成一个
for (j = 0; j < CONFIG_RUN_ID_SIZE; j++)
if (sentinel.myid[j] != 0) break;
if (j == CONFIG_RUN_ID_SIZE) {
/* Pick ID and persist the config. */
getRandomHexChars(sentinel.myid,CONFIG_RUN_ID_SIZE);
sentinelFlushConfig();
}
...
// 向 +monitor 频道发送事件
sentinelGenerateInitialMonitorEvents();
}
我们来看一下 sentinelGenerateInitialMonitorEvents
是怎样发送事件的:
void sentinelGenerateInitialMonitorEvents(void) {
dictIterator *di;
dictEntry *de;
// 获取监听主节点迭代器
di = dictGetIterator(sentinel.masters);
while((de = dictNext(di)) != NULL) {
// 获取主节点
sentinelRedisInstance *ri = dictGetVal(de);
// 发送事件
sentinelEvent(LL_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
}
dictReleaseIterator(di);
}
通过上面的代码,我们看到 sentinelGenerateInitialMonitorEvents
会获取获取主节点的列表,然后调用 sentinelEvent
对每个主节点发送 +monitor
事件。我们来看一下 sentinelEvent
函数的定义:
void sentinelEvent(int level, char *type, sentinelRedisInstance *ri, const char *fmt, ...);
level
表示当前的日志级别,type
表示发送事件信息所用的订阅频道,ri
表示交互的主节点,fmt
表示发送的消息内容。我们来看一下 sentinelEvent
的逻辑:
void sentinelEvent(int level, char *type, sentinelRedisInstance *ri,
const char *fmt, ...) {
// 判断是否以 %@ 开头
if (fmt[0] == '%' && fmt[1] == '@') {
sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ?
NULL : ri->master;
// 如果是和主节点交互则根据实例的名称、IP地址、端⼝号等信息生成传递的消息
if (master) {
snprintf(msg, sizeof(msg), "%s %s %s %d @ %s %s %d",
sentinelRedisInstanceTypeStr(ri),
ri->name, ri->addr->ip, ri->addr->port,
master->name, master->addr->ip, master->addr->port);
} else {
snprintf(msg, sizeof(msg), "%s %s %s %d",
sentinelRedisInstanceTypeStr(ri),
ri->name, ri->addr->ip, ri->addr->port);
}
fmt += 2;
} else {
msg[0] = '\0';
}
...
if (level != LL_DEBUG) {
channel = createStringObject(type,strlen(type));
payload = createStringObject(msg,strlen(msg));
// 发送消息
pubsubPublishMessage(channel,payload);
decrRefCount(channel);
decrRefCount(payload);
}
...
}
sentinelEvent
函数会先判断传⼊的消息内容开头是否为 %@
,如果是的话它就会判断监听实例的类型是否为主节点。如果是主节点 sentinelEvent
函数会把监听实例的名称、IP 和端⼝号加⼊到待发送的消息中。如果当前的日志级别不是 LL_DEBUG
则会调用 pubsubPublishMessage
函数来真正的发送消息。
加载配置项
大家可能有个疑问,sentinel.masters
即哨兵的主节点是什么时候赋值的呢,其实就是在加载配置文件的时候。哨兵的加载配置文件也是通过 loadServerConfig
函数,loadServerConfig
调用的 loadServerConfigFromString
函数中有一个分支,它会判断当前是否为哨兵的配置项,如果是则会调用 sentinelHandleConfiguration
函数来解析当前配置。
if (!strcasecmp(argv[0],"sentinel")) {
/* argc == 1 is handled by main() as we need to enter the sentinel
* mode ASAP. */
if (argc != 1) {
if (!server.sentinel_mode) {
err = "sentinel directive while not in sentinel mode";
goto loaderr;
}
// 解析哨兵配置
err = sentinelHandleConfiguration(argv+1,argc-1);
if (err) goto loaderr;
}
}
sentinelHandleConfiguration
解析 sentinel monitor <name> <host> <port> <quorum>
配置时,会根据这些配置信息调用 createSentinelRedisInstance
函数来创建主节点的实例信息。
char *sentinelHandleConfiguration(char **argv, int argc) {
...
if (!strcasecmp(argv[0],"monitor") && argc == 5) {
/* monitor <name> <host> <port> <quorum> */
int quorum = atoi(argv[4]);
if (quorum <= 0) return "Quorum must be 1 or greater.";
if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2],
atoi(argv[3]),quorum,NULL) == NULL)
{
switch(errno) {
case EBUSY: return "Duplicated master name.";
case ENOENT: return "Can't resolve master instance hostname.";
case EINVAL: return "Invalid port number";
}
}
}
...
}
转载自:https://juejin.cn/post/7176470596975345723