一.概述
redis使用哨兵系统保证redis的稳定性与可靠性。每个哨兵系统由一个或多个sentinel实例组成,每个哨兵系统都可以监视任意多个主服务器,以及其属下的从服务器,并在主服务器下线时(如由于网络异常等导致主服务器掉线),自动将下线主服务器属下的某个从服务器升级为新的主服务器,以代替下线主服务器处理命令请求。当下线的主服务器重新上线时,会成为新主服务器的从服务器。
二.Sentinel的初始化
初始化一个sentinel(哨兵)大致分为以下几步:1.初始化服务器;2.将普通服务器使用的命令表替换为sentinel专用命令表和sentinel状态;3.根据指定的配置文件,初始化sentinel的监视主服务器列表;4.连接主服务器。下面一 一说明。
Step1:初始化服务器
sentinel本质上是一个运行在特殊模式下redis服务器,因此第一步便是初始化一个普通的Redis服务器。在源码中可以看到,main函数做为redis的入口函数,在启动时之初对于sentinel与普通redis服务器并无区别,都会先调用initServerConfig()初始化服务器,该函数对服务器中的一些变量进行了初始化。部分main函数代码如下:
int main(int argc, char **argv) {
// 检查启动命令是否包含redis-sentinel 或 --sentinel,若包含则说明是哨兵模式
server.sentinel_mode = checkForSentinelMode(argc,argv);
// 初始化服务器
initServerConfig();
/* We need to init sentinel right now as parsing the configuration file
* in sentinel mode will have the effect of populating the sentinel
* data structures with master nodes to monitor. */
/*若是哨兵模式则需进行响应的配置*/
if (server.sentinel_mode) {
initSentinelConfig(); // 将端口号设置为哨兵模式默认端口
initSentinel(); // 初始化哨兵
}
...
// 加载配置文件
loadServerConfig(configfile,options);
}
Step2:设置Sentinel的专用命令表并初始化Sentinel状态
在初始化服务器后,main函数中会判断是否为sentinel模式启动,若是则调用相关函数进行配置。首先由于sentinel与普通使用的端口不同,因此调用函数initSentinelConfig()设置端口,之后调用initSentinel初始化sentinel的状态,每个sentinel的状态都由sentinelState结构体进行描述,该结构保存了服务器中所有与sentinel有关的状态(其它的通用状态仍由redisServer结构保存)。代码如下:
#define REDIS_SENTINEL_PORT 26379
// 设置sentinel端口
void initSentinelConfig(void) {
server.port = REDIS_SENTINEL_PORT;
}
/*初始化哨兵*/
void initSentinel(void) {
unsigned int j;
/* Remove usual Redis commands from the command table, then just add
* the SENTINEL command. */
/*将普通服务器的命令表移出,因为哨兵sentinel使用自己专用的命令表*/
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); // 服务器命令表设置为哨兵专用的命令表sentinelcmds
redisAssert(retval == DICT_OK);
}
/* Initialize various data structures. */
// 初始化哨兵状态结构体(sentinelState)
sentinel.current_epoch = 0; // 当前纪元
sentinel.masters = dictCreate(&instancesDictType,NULL); // 创建哨兵的master字典(被监视的主服务器)
sentinel.tilt = 0;
sentinel.tilt_start_time = 0;
sentinel.previous_time = mstime();
sentinel.running_scripts = 0;
sentinel.scripts_queue = listCreate();
sentinel.announce_ip = NULL;
sentinel.announce_port = 0;
}
// 描述sentinel状态的结构体
struct sentinelState {
uint64_t current_epoch; //当前纪元
dict *masters; /*该字典保存了被这个哨兵监视的所有主服务器,键为主服务器名称,值为一个指向sentinelRedisInstance结构的指针*/
int tilt; // 是否进入TILT模式
int running_scripts; // 目前正在执行的脚本数量
mstime_t tilt_start_time; /* When TITL started. */
mstime_t previous_time; // 最后一次执行时间处理器的时间
list *scripts_queue; // FIFO队列,包含了所有需要执行的用户脚本
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;
Step3:读取配置文件初始化sentinel状态中的master字典
sentinel字典中保存了所有被sentinel监视的主服务器信息,该字典的键值结构如下所示:
- 键:被监视服务的名字。
- 值:字典的值保存的是一个sentinelRedisInstance结构,每一个sentinelRedisInstance结构代表一个被sentinel监视的Redis服务器实例,可以是主服务器,从服务器,或是另一个sentinel。其结构如下所示:
// 每一个sentinelRedisInstance结构代表一个被sentinel监视的Redis服务器实例
typedef struct sentinelRedisInstance {
int flags; // 标识值记录了实例的类型(主服务器为SRI_MASTER,从服务器为SRI_SLAVE)及当前状态(如主观下线SRI_S_DOWN)
char *name; // 实例的名字
char *runid; // 实例的运行ID
uint64_t config_epoch; // 配置纪元,用于事先故障转移(选举回合计数)
sentinelAddr *addr; // 实例的地址
redisAsyncContext *cc; // 该结构体保存了异步命令连接所要使用的上下文(数据与结构)
redisAsyncContext *pc; // 该结构体保存了异步订阅连接所要使用的上下文 (关于异步连接在后面说明)
mstime_t cc_conn_time; // 命令连接的创建时间
mstime_t pc_conn_time; // 订阅连接的创建时间
...
mstime_t down_after_period; // 实例无响应多少秒后判定为主观下线
dict *sentinels; // 若该实例是为主服务器创建的,那么该字典保存了除本sentinels之外的监视该主服务器的哨兵对应的sentinelRedisInstance实例,
// 键为ip:port,值为sentinelRedisInstance
dict *slaves; // 若该实例表示的是一个主服务器,那么该字典存储了其所连接的从服务器(键为从服务器IP:port,值为从服务器所对应的sentinelRedisInstance结构)
unsigned int quorum; // 判断该实例为客观下线所需的支持投票数
int parallel_syncs; /* How many slaves to reconfigure at same time. */
char *auth_pass; /* Password to use for AUTH against master & slaves. */
/* Slave specific. */
mstime_t master_link_down_time; /* Slave replication link down time. */
int slave_priority; /* Slave priority according to its INFO output. */
mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */
struct sentinelRedisInstance *master; /* Master instance if it's slave. */
char *slave_master_host; /* Master host as reported by INFO */
int slave_master_port; /* Master port as reported by INFO */
int slave_master_link_status; /* Master link status as reported by INFO */
unsigned long long slave_repl_offset; /* Slave replication offset. */
/* Failover */
char *leader; /* If this is a master instance, this is the runid of
the Sentinel that should perform the failover. If
this is a Sentinel, this is the runid of the Sentinel
that this Sentinel voted as leader. */
uint64_t leader_epoch; /* Epoch of the 'leader' field. */
uint64_t failover_epoch; /* Epoch of the currently started failover. */
int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */
mstime_t failover_state_change_time;
mstime_t failover_start_time; /* Last failover attempt start time. */
mstime_t failover_timeout; // 刷新故障迁移状态的最大时间
mstime_t failover_delay_logged; /* For what failover_start_time value we
logged the failover delay. */
struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */
/* Scripts executed to notify admin or reconfigure clients: when they
* are set to NULL no script is executed. */
char *notification_script;
char *client_reconfig_script;
} sentinelRedisInstance;
而每个masters字典的初始化是根据被载入的Sentinel配置文件来进行的,该文件包含了要监视主服务器的地址,端口等信息,为每一个主服务器建立一个sentinelRedisInstance。redis会在main函数中调用loadServerConfig(configfile,options);加载配置文件,随后调用loadServerConfigFromString(config)执行具体的加载过程,该函数先加载一般服务器配置属性,然后调用char *sentinelHandleConfiguration(char **argv, int argc) 加载sentinel的配置信息。
Step4:连接主服务器
最后一步是创建连向被监视的主服务器的网路连接,此后sentine将成为该主服务器的客户端,可以向该主服务器发送信息以获取相关信息。sentienl会向主服务器创建两个异步连接(并非异步IO):一个命令连接(用于发送命令和接收命令回复)和一个订阅连接(订阅主服务器的__sentinel__::hello频道)。
这连个异步连接会在serverCron函数中创建,serverCron中每隔一段时间便会检查sentinel与实例的连接,若处于断线状态便会进行连接,redis中每个异步连接用一个redisAsyncContext结构描述。
三.redis中的异步连接
此处所谓的异步连接并非采用的是异步I/O,而是指redis中的一种机制。在调用该机制写数据时并不直接写入操作系统套接字缓存,而是先写入应用层缓存,待可写时(如:套接字缓存中的空闲空间足够大,即高于某一水位线)写入套接字缓存。因此可以推测redis称之为异步连接只是因为发送数据不一定立刻较复socket,而是由redis判断何时可写而后交付。这与muduo的思想一致,其具体过程如下所述:
1.用于描述一个异步连接的数据结构
redis中使用redisAsyncContext结构描述一个异步连接,其中包含了一个异步连接所要使用的数据与结构。
typedef struct redisContext {
int err; // 错误标志,当无错误时为0
char errstr[128]; // 错误字符串,当出现错误时进行填充
int fd; // 文件描述符
int flags; // 连接状态
char *obuf; // 输出缓存,保存要写入fd的数据
redisReader *reader; /* Protocol reader */
} redisContext;
typedef struct redisAsyncContext {
/* Hold the regular context, so it can be realloc'ed. */
// redisContext保存了一个redis连接的基本结构,如fd,输出缓存
redisContext c;
/* Setup error flags so they can be used directly. */
int err;
char *errstr;
/* Not used by hiredis */
void *data;
/* Event library data and hooks */
// 保存了用于事件循环的结构和钩子函数(回调函数)
struct {
/* data指向一个用于该连接与事件循环的中间结构(一个redisAeEvents结构,它保存了注册事件所需的所有数据)
* 回指redisAeEventsevent结构,这样在之后调用addRead等进行注册事件时就可直接将data做为参数传入*/
void *data;
/* Hooks that are called when the library expects to start
* reading/writing. These functions should be idempotent. */
void (*addRead)(void *privdata); // 用于为连接添加读事件
void (*delRead)(void *privdata); // 用于为连接注销读事件
void (*addWrite)(void *privdata);// 用于为连接添加写事件
void (*delWrite)(void *privdata);// 用于为连接注销写事件
void (*cleanup)(void *privdata); // 用于为连接清除所有事件
} ev;
/* Called when either the connection is terminated due to an error or per
* user request. The status is set accordingly (REDIS_OK, REDIS_ERR). */
redisDisconnectCallback *onDisconnect;
// 当第一次可写事件产生时进行回调(因为对于非阻塞套接字调用connect,无论成功失败都会产生可写事件)
redisConnectCallback *onConnect;
/* Regular command callbacks */
redisCallbackList replies;
/* Subscription callbacks */
struct {
redisCallbackList invalid;
struct dict *channels;
struct dict *patterns;
} sub;
} redisAsyncContext;
2.将异步连接与事件循环进行关联
redis中使用redisAeEvents结构将异步连接与事件循环关联起来,一个redisAeEvents包含了为一个异步连接注册事件的全部所需数据,因此它被当成一个参数传入用于注册事件的函数。其结构如下:
// 用于将异步连接与redis事件循环相关联
typedef struct redisAeEvents {
redisAsyncContext *context; // 该事件所关联的异步连接
aeEventLoop *loop; // 关联的事件循环
int fd; // 要注册事件的文件描述符
int reading, writing;
} redisAeEvents;
redis中使用函数redisAeAttach来进行关联操作,其过程如下:
// 将事件循环loop与异步连接ac进行关联
static int redisAeAttach(aeEventLoop *loop, redisAsyncContext *ac) {
redisContext *c = &(ac->c);
redisAeEvents *e;
/* Nothing should be attached when something is already attached */
if (ac->ev.data != NULL)
return REDIS_ERR;
// 创建redisAeEvents结构,将异步连接redisAsyncContext与redis事件循环进行关联
e = (redisAeEvents*)malloc(sizeof(*e));
e->context = ac; // 指向异步连接
e->loop = loop; // 指向要关联的事件循环
e->fd = c->fd; // 异步连接的文件描述符
e->reading = e->writing = 0;
/* Register functions to start/stop listening for events */
ac->ev.addRead = redisAeAddRead; // 关联钩子函数,即设置用于注册读事件的函数
ac->ev.delRead = redisAeDelRead;
ac->ev.addWrite = redisAeAddWrite; // 设置用于注册写事件的函数
ac->ev.delWrite = redisAeDelWrite;
ac->ev.cleanup = redisAeCleanup;
ac->ev.data = e; // 回指redisAeEventsevent结构,这样在之后调用addRead等进行注册时就可直接将data做为参数传入
return REDIS_OK;
}
可以看到那些在redisAeAttach中设置的用于注册事件的函数的参数都是使用某个异步连接中存储的data(指向一个redisAeAttach)做为参数的,比如为异步连接注册写事件的函数如下:
static void redisAeAddRead(void *privdata) {
redisAeEvents *e = (redisAeEvents*)privdata; // 将参数转为redisAeEvents
aeEventLoop *loop = e->loop;
if (!e->reading) {
e->reading = 1;
aeCreateFileEvent(loop,e->fd,AE_READABLE,redisAeReadEvent,e);
}
}
有了上述的redisAsyncContext与redisAeEvents结构,在由redisAeAttach进行关联,那么一个异步连接就可以如下这般设置注册一个事件了。
// 展示如何为一个异步连接注册读事件的伪代码
void showHowToRegiestReadEvent(redisAsyncContext &asynConn) {
if(未关联loop) {
redisAeAttach(server.el, asynConn);
}
asynConn.addRead(asyn.ev,data);
}
3.创建sentinel到实例异步连接
对于一个sentinel而言,在定期事件serverCron中会定期调用sentinelTimer()处理一些事件,其中就包括检测连接与进行连接的工作。其中会辗转调用sentinelReconnectInstance函数进行连接工作,具体如下:
// 设置异步连接的写回调函数为redisAeWriteEvent
static void redisAeAddWrite(void *privdata) {
redisAeEvents *e = (redisAeEvents*)privdata;
aeEventLoop *loop = e->loop;
if (!e->writing) {
e->writing = 1;
aeCreateFileEvent(loop,e->fd,AE_WRITABLE,redisAeWriteEvent,e);
}
}
// 设置可写事件的宏,即调用异步连接中设置好的addWrite
// addWrite = redisAeAddWrite
#define _EL_ADD_WRITE(ctx) do { \
if ((ctx)->ev.addWrite) (ctx)->ev.addWrite((ctx)->ev.data); \
} while(0)
// 设置连接成功后调用的函数为fn,并设置一个写事件回调函数
// fn = sentinelLinkEstablishedCallback
int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn) {
if (ac->onConnect == NULL) {
ac->onConnect = fn;
/* The common way to detect an established connection is to wait for
* the first write event to be fired. This assumes the related event
* library functions are already set. */
_EL_ADD_WRITE(ac);
return REDIS_OK;
}
return REDIS_ERR;
}
/*当实例处于断线状态则创建连向实例的异步连接*/
void sentinelReconnectInstance(sentinelRedisInstance *ri) {
// 若实例未处于断线状态则返回
if (!(ri->flags & SRI_DISCONNECTED)) return;
/* Commands connection. */
// 若未创建连向该实例的命令连接
if (ri->cc == NULL) {
// 创建连向服务器的非阻塞连接,并初始化一个描述异步连接的redisAsyncContext结构体对象
ri->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,REDIS_BIND_ADDR);
if (ri->cc->err) {
sentinelEvent(REDIS_DEBUG,"-cmd-link-reconnection",ri,"%@ #%s",
ri->cc->errstr);
sentinelKillLink(ri,ri->cc);
} else {
// 更新命令连接的创建时间
ri->cc_conn_time = mstime();
ri->cc->data = ri;
// 创建一个redisAeEvents结构将连接与事件循环进行关联
redisAeAttach(server.el,ri->cc);
// 设置连接回调函数,并注册一个写回调函数
redisAsyncSetConnectCallback(ri->cc, sentinelLinkEstablishedCallback);
redisAsyncSetDisconnectCallback(ri->cc, sentinelDisconnectCallback); // 设置断开连接回调函数
sentinelSendAuthIfNeeded(ri,ri->cc); // 必要的话将发送身份验证信息
sentinelSetClientName(ri,ri->cc,"cmd");
/* Send a PING ASAP when reconnecting. */
sentinelSendPing(ri);
}
}
/* Pub / Sub */
// 订阅连接
if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && ri->pc == NULL) {
ri->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,REDIS_BIND_ADDR);
if (ri->pc->err) {
sentinelEvent(REDIS_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",
ri->pc->errstr);
sentinelKillLink(ri,ri->pc);
} else {
int retval;
ri->pc_conn_time = mstime();
ri->pc->data = ri;
redisAeAttach(server.el,ri->pc);
redisAsyncSetConnectCallback(ri->pc,
sentinelLinkEstablishedCallback);
redisAsyncSetDisconnectCallback(ri->pc,
sentinelDisconnectCallback);
sentinelSendAuthIfNeeded(ri,ri->pc);
sentinelSetClientName(ri,ri->pc,"pubsub");
/* Now we subscribe to the Sentinels "Hello" channel. */
retval = redisAsyncCommand(ri->pc,
sentinelReceiveHelloMessages, NULL, "SUBSCRIBE %s",
SENTINEL_HELLO_CHANNEL);
if (retval != REDIS_OK) {
/* If we can't subscribe, the Pub/Sub connection is useless
* and we can simply disconnect it and try again. */
sentinelKillLink(ri,ri->pc);
return;
}
}
}
/* Clear the DISCONNECTED flags only if we have both the connections
* (or just the commands connection if this is a sentinel instance). */
if (ri->cc && (ri->flags & SRI_SENTINEL || ri->pc))
ri->flags &= ~SRI_DISCONNECTED;
}
无论connect成功与否都将产生一个写事件,从而调用设置的可写事件回调函数redisAeWriteEvent,其中会调用一次异步连接中onConnent指向的函数进行一些处理。之后将异步连接的输出缓存obuf中的数据写入套接字,当obuf中的数据被写完时注销写事件,待下次再向obuf添加数据时再重新注册写事件。
四.定期获取主从服务器信息
sentinel默认会以每10秒1次的频率,通过命令连接向被监视的主服务器发送INFO命令,之后主服务器会返回自己的信息即从服务器信息。
当sentinel从主服务器获取到从服务器的信息后,除了会为该从服务器创建实例,还会创建连向该从服务器的命令连接和订阅连接。之后会以每10秒一次的频率通过命令连接向从服务器发送INFO命令,并获回复,其中包括从服务器的运行ID,主服务器的IP+port,主从服务器连接状态,从服务器复制偏移量。sentinel会使用该信息更新从服务器实例。
五.频道信息的发送及接收(用于哨兵间的信息交互)
1.频道的订阅
当sentinel与主服务器或从服务器简历订阅连接后,便会通过订阅连接向服务器发送订阅命令SUBSCRIBE __sentinel__:hello来订阅__sentinel__:hello频道,对其订阅会一致持续到连接断开。
redis中订阅了一个频道就好像我们用收音机收听一个频道一样,但某个频道收到一条消息,那么该条消息就会转发给所有该频道的订阅者。即当某个sentinel向某个服务器的__sentinel__:hello发送了一条消息,那么所有订阅该服务器__sentinel__:hello频道的sentinel也都将收到该条消息。
2.通过订阅连接向主从服务器发送消息
sentinel默认会以两秒一次的频率,通过命令连接(不是通过订阅连接,订阅连接只负责发送订阅命令,和接收订阅消息)向服务器的__sentinel__:hello频道发送命令,其中包含了主服务器和哨兵本身的信息,之后该信息会通过订阅连接发送给订阅了该服务器上__sentinel__:hello频道的所有sentinel。该命令格式如下:
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
/* s_ip:sentinel的IP地址
* s_port: sentinel的端口号
* s_runid: sentinel的运行ID
* s_epoch:sentinel的当前纪元配置
* m_name:主服务器名字
* 【注】:之后可发现为什么要包含这些信息,因为通过这些信息就可找到主服务器在sentinel中的实例,
* 亦可找到s_所描述的sentinel在主服务器实例的sentinels字典中的实例
*/
- 以s_开头的参数保存的是sentinel本身的信息
- 以m_开头的参数保存的是主服务器的信息,即使该条命令是发送给从服务器的,那么m_保存的亦是该从服务器正在复制的主服务器的信息
3.接收来自主从服务器的频道信息
当主从服务器收到发送至自身__sentinel__:hello频道的数据,会转发给所有订阅该频道的sentinel。当sentinel收到从__sentinel__:hello频道收到一条消息时,会解析其中的信息。sentinel能收到该信息说明必是监视着同一主服务器。
1)解析主服务器信息
sentinel会从中解析出主服务器信息,并根据解析出的各个参数跟新本sentinel中该主服务器实例。
2)解析sentinel信息
sentinel会在主服务器实例结构的sentinels字典中保存除自己之外所有同样监视该主服务器的sentinel。sentinels字典的键值对如下所述:
- 键为为sentinel设置的名字,格式为ip:port
- 值为sentinel对应的sentinelRedisInstance实例结构。
sentinel会根据解析出的主服务器信息查询masters字典找到主服务器实例,随后通过提取出的源sentinel(即发送该条消息的sentinel)信息在主服务器实例的sentinels字典中查找到对应的实例结构并对其进行更新
4.创建sentinel间的命令连接
如前所述,sentienl会提取信息并更新sentinels字典,除此之外,还会创建一个连向新sentinel的命令连接,该新sentinel也会创建一条连向该sentinel的命令连接。
【注】:sentinel间不需要创建订阅连接,这是由订阅连接在redis中起到的作用决定的,之所以要创建sentinel连向主从服务器的订阅连接是为了通过订阅频道发现未知的新sentinel以便建立连接。
六.检测主观下线状态
所谓主观下线,即某个sentinel自己主观的认位某个实例(可能是主服务器,从服务器或其它sentinel)已经下线,这仅仅是其自身的判断。
sentinel判断主观下线时通过定期发送PING命令至所有与其建立了命令连接的实例来实现的,默认的发送频率为每秒1次。并依据实例返回的回复来判断是否在线。若在down-after-milliseconds毫秒内连续向sentinel返回除+PONG、-LOADING、-MASTERDOWN之外的其它回复,则sentinel判断该实例为主观下线,将该实例结构的flags属性设置SRI_S_DOWN标志。
【注】:不同的sentinel可以设置不同的主观下线时间
七.检查客观下线状态
当sentinel将一个主服务器(从服务器并不判断客观下线)判断为主观下线时,为了确定该主服务器是否真的已经下线,它会像其它同样监视该主服务器的sentinel发送询问命令,若有足够多的sentinel认为该主服务器下线,则sentinel就会将主服务器判定为客观下线,并对主服务器执行故障转移。
1.发送SENTINEL is-master-down-by-addr命令询问下线状态
sentinel发送以下命令至其它sentinel询问是否同意某个主服务器已下线,命令格式如下:
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
/* 【注】:
* - ip:被sentinel判断为主观下线的主服务器IP
* - port:被sentinel判断为主观下线的主服务器端口
* - current_epoch:本sentinel的当前纪元,用于故障转移时选出领头sentinel
* - runid: 当为*时,表示该条命令仅仅检查主服务器的客观线性状态
当为sentinel的运行ID时则表示该命令用于选举领头sentinel
*/
2.接收并处理SENTINEL is-master-down-by-addr命令
当sentinel收到一个SENTINEL is-master-down-by-addr命令后,会根据命令中的主服务器IP和端口号检查该主服务器是否下线,之后回复一条包含三个参数的应答。这三个参数如下所示:
参数 | 意义 |
down_state | 返回sentinel对主服务器的检查结果,1表示主服务器已下线,0表示主服务器未下线 |
leader_runid | 该值可以为*或sentinel的局部领头sentinel的运行ID - 当返回*时,表示该回复仅仅用于回复主服务器的下线状态 - 当返回局部领头sentinel的运行ID时,表示该回复用于选举领头sentinel |
leader_epoch | 记录了sentinel的局部领头sentinel的配置纪元 |
3.接收SENTINEL is-master-down-by-addr命令的回复
sentinel将根据SENTINEL is-master-down-by-addr命令的回复统计有多少其它sentinel同意主服务器下线,当这一数量达到配置的判断客观下线所需的数量时,sentinel将主服务器实例结构的flag属性的SRI_O_DOWN标志置位,表示主服务器已客观下线。主服务器实例的quorum属性记录了认为sentinel进入下线状态所需的sentinel的数量。
八.选举领头Sentinel
当一个主服务器被判定为客观下线,那么监视该主服务器的sentinel会进行协商,选举出一个领头sentinel,并由领头sentinel对下线的主服务器进行故障转移。其具体步骤如下:
step1:当某个sentinel(源sentinel)将主服务器判定为客观下线,那么便会向其它sentinel(目标sentinel)发送SENTINEL is-master-down-by-addr命令,命令中current_epoch参数为自己的配置纪元,且runid参数不为*,而是自己的运行id,这表示希望其它sentinel在本纪元(回合)内投票给自己(即选自己为领头sentinel)。
【注】:在每个配置纪元内都有一次投票机会来推选某个sentinel成为领头sentinel,不论此次选举是否成功,所有sentinel的配置纪元的值都会自增一次(不需要每个sentinel都有相同的配置纪元,只需一起增一即可,可以想想为什么,可参考后面的步骤)。
step2:当某个sentinel收到投票请求后会进行回复,回复中的leader_runid和leader_epoch参数将被设置为所投(推选)的sentinel的运行ID和配置纪元。
step3:当源sentinel收到来自目标sentinel的回复后,会检查回复中的leader_epoch与自己的配置纪元是否相同(若不相同,表示要么不是投自己的,要么就是过期投票)。若相同则继续检查leader_runid是否等于自己的运行ID,即判断是否将票投递给了自己。
step4:当某个sentinel被半数以上的sentinel推举时,那么该sentinel将成为领头sentinel。若在给定时限内没有一个sentinel被选举为领头sentinel,那么将在一段时间后重新选举,直至出现领头sentinel。
【注】:由于领头sentinel需获得半数以上的sentinel的支持,且一个配置纪元内只有一次投票机会,所以在一个配置纪元内只会出现一个领头sentinel。
九.故障转移
当选举出领头sentinel后,领头sentinel会对已下线主服务器执行故障转移操作,该操作包括三个步骤:1.在下线主服务器的从服务器中挑选一个从服务器做为新的主服务器。2.让其它从服务器复制新的主服务器。3.将下线主服务器设置为新的主服务器的从服务器。
step1:选出新的主服务器
在下线主服务器的从服务器中选择一个状态良好,数据较完整地从服务器提升为主服务器。其选择过程如下:
S1)将已下线主服务器地所有从服务器保存到一个列表中,然后逐步过滤。
S2)删除列表中处于下线或断线状态地从服务器
S3)删除与主服务器过早断开地从服务器,以down-after-millseconds * 10毫秒来判断。其中down-after-millseconds时sentinel判断主服务器主观下线所需地时间。
S4)从剩余从服务器中选择优先级较高地从服务器,若有多个优先级相同地,那么选择复制偏移量较大地(数据更完整)。
【注】:由上述步骤可知,在进行故障转移时可能存在数据丢失,因此redis不能保存那些“敏感数据”。
当选择好要提升地从服务器后,sentienl会像其发送SLAVEOF no one命令,将该从服务器转化为主服务器。并每秒一次的向该从服务器发送INFO命令,当升级的从服务器的回复从slave变为master时,领头sentinel就知道从服务器已升级为主服务器。
step2:修改其它从服务的复制目标
sentinel会像其它从服务器发送SLAVEOF命令,使其复制新的主服务器。
step3:将旧的主服务器设置为从服务器
sentinel会将SLAVEOF命令保存在旧主服务对应的实例中,待其重新上线时发送,从而将旧主服务器变为从服务器。