哨兵模式是提高 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";
}
}
}
...
}