六:定时发送消息

哨兵每隔一段时间,会向其所监控的所有实例发送一些命令,用于获取这些实例的状态。这些命令包括:”PING”、”INFO”和”PUBLISH”。

“PING”命令,主要用于哨兵探测实例是否活着。如果对方超过一段时间,还没有回复”PING”命令,则认为其是主观下线了。

“INFO”命令,主要用于哨兵获取实例当前的状态和信息,比如该实例当前是主节点还是从节点;该实例反馈的IP地址和PORT信息,是否与我记录的一样;该实例如果是主节点的话,那它都有哪些从节点;该实例如果是从节点的话,它与主节点是否连通,它的优先级是多少,它的复制偏移量是多少等等,这些信息在故障转移流程中,是判断实例状态的重要信息;

“PUBLISH”命令,主要用于哨兵向实例的HELLO频道发布有关自己以及主节点的信息,也就是所谓的HELLO消息。因为所有哨兵都会订阅主节点和从节点的HELLO频道,因此,每个哨兵都会收到其他哨兵发布的信息。

因此,通过这些命令,尽管在配置文件中只配置了主节点的信息,但是哨兵可以通过主节点的”INFO”回复,得到所有从节点的信息;又可以通过订阅实例的HELLO频道,接收其他哨兵通过”PUBLISH”命令发布的信息,从而得到监控同一主节点的所有其他哨兵的信息。

在“主函数”sentinelHandleRedisInstance中,是通过调用sentinelSendPeriodicCommands来发送这些命令的。注意,以上的命令都有自己的发送周期,在sentinelSendPeriodicCommands函数中,并不是一并发送三个命令,而是发送那些,按照发送周期应该发送的命令。该函数的代码如下:

  1. void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
  2. mstime_t now = mstime();
  3. mstime_t info_period, ping_period;
  4. int retval;
  5.  
  6. /* Return ASAP if we have already a PING or INFO already pending, or
  7. * in the case the instance is not properly connected. */
  8. if (ri->flags & SRI_DISCONNECTED) return;
  9.  
  10. /* For INFO, PING, PUBLISH that are not critical commands to send we
  11. * also have a limit of SENTINEL_MAX_PENDING_COMMANDS. We don't
  12. * want to use a lot of memory just because a link is not working
  13. * properly (note that anyway there is a redundant protection about this,
  14. * that is, the link will be disconnected and reconnected if a long
  15. * timeout condition is detected. */
  16. if (ri->pending_commands >= SENTINEL_MAX_PENDING_COMMANDS) return;
  17.  
  18. /* If this is a slave of a master in O_DOWN condition we start sending
  19. * it INFO every second, instead of the usual SENTINEL_INFO_PERIOD
  20. * period. In this state we want to closely monitor slaves in case they
  21. * are turned into masters by another Sentinel, or by the sysadmin. */
  22. if ((ri->flags & SRI_SLAVE) &&
  23. (ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS))) {
  24. info_period = 1000;
  25. } else {
  26. info_period = SENTINEL_INFO_PERIOD;
  27. }
  28.  
  29. /* We ping instances every time the last received pong is older than
  30. * the configured 'down-after-milliseconds' time, but every second
  31. * anyway if 'down-after-milliseconds' is greater than 1 second. */
  32. ping_period = ri->down_after_period;
  33. if (ping_period > SENTINEL_PING_PERIOD) ping_period = SENTINEL_PING_PERIOD;
  34.  
  35. if ((ri->flags & SRI_SENTINEL) == 0 &&
  36. (ri->info_refresh == 0 ||
  37. (now - ri->info_refresh) > info_period))
  38. {
  39. /* Send INFO to masters and slaves, not sentinels. */
  40. retval = redisAsyncCommand(ri->cc,
  41. sentinelInfoReplyCallback, NULL, "INFO");
  42. if (retval == REDIS_OK) ri->pending_commands++;
  43. } else if ((now - ri->last_pong_time) > ping_period) {
  44. /* Send PING to all the three kinds of instances. */
  45. sentinelSendPing(ri);
  46. } else if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
  47. /* PUBLISH hello messages to all the three kinds of instances. */
  48. sentinelSendHello(ri);
  49. }
  50. }

如果实例标志位中设置了SRI_DISCONNECTED标记,说明当前实例的异步上下文还没有创建好,因此直接返回;

实例的pending_commands属性,表示已经向该实例发送的命令中,尚有pending_commands个命令还没有收到回复。每次调用redisAsyncCommand函数,向实例异步发送一条命令之后,就会增加该属性的值,而每当收到命令回复之后,就会减少该属性的值;

因此,如果该属性的值大于SENTINEL_MAX_PENDING_COMMANDS(100),说明该实例尚有超过100条命令的回复信息没有收到。这种情况下,说明与实例的连接已经不正常了,为了节约内存,因此直接返回;

接下来计算info_period和ping_period,这俩值表示发送"INFO"和"PING"命令的时间周期。如果当前时间距离上次收到"INFO"或"PING"回复的时间已经超过了info_period或ping_period,则向实例发送"INFO"或"PING"命令;

如果当前实例为从节点,并且该从节点对应的主节点已经客观下线了,则置info_period为1000,否则的话置为SENTINEL_INFO_PERIOD(10000)。之所以在主节点客观下线后更频繁的向从节点发送"INFO"命令,是因为从节点可能会被置为新的主节点,因此需要更加实时的获取其状态;

将ping_period置为ri->down_after_period的值,该属性的值是根据配置文件中down-after-milliseconds选项得到的,如果该属性值大于SENTINEL_PING_PERIOD(1000),则将ping_period置为SENTINEL_PING_PERIOD;

接下来开始发送命令:如果当前实例不是哨兵实例,并且距离上次收到"INFO"命令回复已经超过了info_period,则向该实例异步发送"INFO"命令。

否则,如果距离上次收到"PING"命令回复已经超过了ping_period,则调用函数sentinelSendPing向该实例异步发送"PING"命令;

否则,如果距离上次收到"PUBLISH"命令的回复已经超过了SENTINEL_PUBLISH_PERIOD(2000),则调用函数sentinelSendHello向该实例异步发送"PUBLISH"命令;

因此,"PING"用于探测实例是否活着,可以发送给所有类型的实例;而"INFO"命令用于获取实例的信息,只需发送给主节点和从节点实例;而"PUBLISH"用于向HELLO频道发布哨兵本身和主节点的信息,除了发送给主节点和从节点之外,哨兵本身也实现了"PUBLISH"命令的处理函数,因此该命令也会发送给哨兵实例。

1:PING消息

函数sentinelSendPing用于向实例发送”PING”命令,因为该命令用于探测实例是否主观下线,因此等到后面讲解主观下线是在分析。

2:HELLO消息

函数sentinelSendHello用于发布HELLO消息,它的代码如下:

  1. int sentinelSendHello(sentinelRedisInstance *ri) {
  2. char ip[REDIS_IP_STR_LEN];
  3. char payload[REDIS_IP_STR_LEN+1024];
  4. int retval;
  5. char *announce_ip;
  6. int announce_port;
  7. sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ? ri : ri->master;
  8. sentinelAddr *master_addr = sentinelGetCurrentMasterAddress(master);
  9.  
  10. if (ri->flags & SRI_DISCONNECTED) return REDIS_ERR;
  11.  
  12. /* Use the specified announce address if specified, otherwise try to
  13. * obtain our own IP address. */
  14. if (sentinel.announce_ip) {
  15. announce_ip = sentinel.announce_ip;
  16. } else {
  17. if (anetSockName(ri->cc->c.fd,ip,sizeof(ip),NULL) == -1)
  18. return REDIS_ERR;
  19. announce_ip = ip;
  20. }
  21. announce_port = sentinel.announce_port ?
  22. sentinel.announce_port : server.port;
  23.  
  24. /* Format and send the Hello message. */
  25. snprintf(payload,sizeof(payload),
  26. "%s,%d,%s,%llu," /* Info about this sentinel. */
  27. "%s,%s,%d,%llu", /* Info about current master. */
  28. announce_ip, announce_port, server.runid,
  29. (unsigned long long) sentinel.current_epoch,
  30. /* --- */
  31. master->name,master_addr->ip,master_addr->port,
  32. (unsigned long long) master->config_epoch);
  33. retval = redisAsyncCommand(ri->cc,
  34. sentinelPublishReplyCallback, NULL, "PUBLISH %s %s",
  35. SENTINEL_HELLO_CHANNEL,payload);
  36. if (retval != REDIS_OK) return REDIS_ERR;
  37. ri->pending_commands++;
  38. return REDIS_OK;
  39. }

首先得到实例ri所属的主节点实例master;然后调用sentinelGetCurrentMasterAddress函数得到master的地址信息;

如果实例ri的标志位中具有SRI_DISCONNECTED标记的话,直接返回;

如果当前哨兵配置了sentinel.announce_ip的话,则使用该ip信息作为自己的ip地址,否则,调用anetSockName函数,根据socket描述符得到当前哨兵的ip地址;

如果当前哨兵配置了sentinel.announce_port的话,则使用该port信息作为自己的端口信息,否则,使用server.port作为当前哨兵的端口信息;

接下来组装要发布的HELLO信息,HELLO信息的格式是:"sentinel_ip,sentinel_port,sentinel_runid,current_epoch,master_name,master_ip,master_port,master_config_epoch"

接下来,向ri异步发送"PUBLISH__sentinel__:hello <HELLO>"命令,设置命令回调函数为sentinelPublishReplyCallback;

当哨兵收到实例对于该”PUBLISH”命令的回复之后,会调用回调函数sentinelPublishReplyCallback,该函数只用于更新属性ri->last_pub_time,对回复内容无需关心:

  1. void sentinelPublishReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
  2. sentinelRedisInstance *ri = c->data;
  3. redisReply *r;
  4. REDIS_NOTUSED(privdata);
  5.  
  6. if (ri) ri->pending_commands--;
  7. if (!reply || !ri) return;
  8. r = reply;
  9.  
  10. /* Only update pub_time if we actually published our message. Otherwise
  11. * we'll retry again in 100 milliseconds. */
  12. if (r->type != REDIS_REPLY_ERROR)
  13. ri->last_pub_time = mstime();
  14. }

之前在介绍sentinelReconnectInstance函数时讲过,当哨兵向主节点或从节点实例建立订阅连接时,向实例发送” SUBSCRIBE __sentinel__:hello"命令,订阅HELLO频道时,设置该命令的回调函数为sentinelReceiveHelloMessages。因此,当收到该频道上发布的消息时,就会调用函数sentinelReceiveHelloMessages。

该频道上的消息,是监控同一实例的其他哨兵节点发来的HELLO消息,当前哨兵通过HELLO消息,来发现其他哨兵,并且相互之间交互最新的主节点信息。sentinelReceiveHelloMessages函数的代码如下:

  1. void sentinelReceiveHelloMessages(redisAsyncContext *c, void *reply, void *privdata) {
  2. sentinelRedisInstance *ri = c->data;
  3. redisReply *r;
  4. REDIS_NOTUSED(privdata);
  5.  
  6. if (!reply || !ri) return;
  7. r = reply;
  8.  
  9. /* Update the last activity in the pubsub channel. Note that since we
  10. * receive our messages as well this timestamp can be used to detect
  11. * if the link is probably disconnected even if it seems otherwise. */
  12. ri->pc_last_activity = mstime();
  13.  
  14. /* Sanity check in the reply we expect, so that the code that follows
  15. * can avoid to check for details. */
  16. if (r->type != REDIS_REPLY_ARRAY ||
  17. r->elements != 3 ||
  18. r->element[0]->type != REDIS_REPLY_STRING ||
  19. r->element[1]->type != REDIS_REPLY_STRING ||
  20. r->element[2]->type != REDIS_REPLY_STRING ||
  21. strcmp(r->element[0]->str,"message") != 0) return;
  22.  
  23. /* We are not interested in meeting ourselves */
  24. if (strstr(r->element[2]->str,server.runid) != NULL) return;
  25.  
  26. sentinelProcessHelloMessage(r->element[2]->str, r->element[2]->len);
  27. }

该函数中,首先更新ri->pc_last_activity为当前时间;

然后判断是否处理接收到的消息,注意,只处理"message"消息,也就是说不会处理"subscribe"消息;

注意,如果收到的"message"消息中,包含了自身的runid,说明这是本哨兵自己发送的消息,因此无需处理,直接返回;

最后,调用sentinelProcessHelloMessage函数处理收到的HELLO消息;

注意:在测试时发现会收到从节点重复的HELLO消息,也就是同一时间,同一个哨兵发布的两条一模一样的消息。这是因为哨兵向主节点发送的”PUBLISH”命令,会因为主从复制的原因,而同步到从节点;而同时该哨兵也向从节点发送”PUBLISH”命令,因此,从节点就会在同一时间,收到两条一模一样的HELLO消息,并将它们发布到频道上。

另外,一旦哨兵发现了其他哨兵之后,可以直接向其发送"PUBLISH __sentinel__:hello <HELLO>"命令。哨兵自己实现了”PUBLISH”的处理函数sentinelPublishCommand,当收到其他哨兵直接发来的HELLO消息时,就会调用该函数处理。该函数的代码如下:

  1. void sentinelPublishCommand(redisClient *c) {
  2. if (strcmp(c->argv[1]->ptr,SENTINEL_HELLO_CHANNEL)) {
  3. addReplyError(c, "Only HELLO messages are accepted by Sentinel instances.");
  4. return;
  5. }
  6. sentinelProcessHelloMessage(c->argv[2]->ptr,sdslen(c->argv[2]->ptr));
  7. addReplyLongLong(c,1);
  8. }

因此,不管是从真正的订阅频道中收到HELLO消息,还是直接收到其他哨兵发来的”PUBLISH”命令,最终都是通过sentinelProcessHelloMessage函数对HELLO消息进行处理的。该函数的代码如下:

  1. void sentinelProcessHelloMessage(char *hello, int hello_len) {
  2. /* Format is composed of 8 tokens:
  3. * 0=ip,1=port,2=runid,3=current_epoch,4=master_name,
  4. * 5=master_ip,6=master_port,7=master_config_epoch. */
  5. int numtokens, port, removed, master_port;
  6. uint64_t current_epoch, master_config_epoch;
  7. char **token = sdssplitlen(hello, hello_len, ",", 1, &numtokens);
  8. sentinelRedisInstance *si, *master;
  9.  
  10. if (numtokens == 8) {
  11. /* Obtain a reference to the master this hello message is about */
  12. master = sentinelGetMasterByName(token[4]);
  13. if (!master) goto cleanup; /* Unknown master, skip the message. */
  14.  
  15. /* First, try to see if we already have this sentinel. */
  16. port = atoi(token[1]);
  17. master_port = atoi(token[6]);
  18. si = getSentinelRedisInstanceByAddrAndRunID(
  19. master->sentinels,token[0],port,token[2]);
  20. current_epoch = strtoull(token[3],NULL,10);
  21. master_config_epoch = strtoull(token[7],NULL,10);
  22.  
  23. if (!si) {
  24. /* If not, remove all the sentinels that have the same runid
  25. * OR the same ip/port, because it's either a restart or a
  26. * network topology change. */
  27. removed = removeMatchingSentinelsFromMaster(master,token[0],port,
  28. token[2]);
  29. if (removed) {
  30. sentinelEvent(REDIS_NOTICE,"-dup-sentinel",master,
  31. "%@ #duplicate of %s:%d or %s",
  32. token[0],port,token[2]);
  33. }
  34.  
  35. /* Add the new sentinel. */
  36. si = createSentinelRedisInstance(NULL,SRI_SENTINEL,
  37. token[0],port,master->quorum,master);
  38. if (si) {
  39. sentinelEvent(REDIS_NOTICE,"+sentinel",si,"%@");
  40. /* The runid is NULL after a new instance creation and
  41. * for Sentinels we don't have a later chance to fill it,
  42. * so do it now. */
  43. si->runid = sdsnew(token[2]);
  44. sentinelFlushConfig();
  45. }
  46. }
  47.  
  48. /* Update local current_epoch if received current_epoch is greater.*/
  49. if (current_epoch > sentinel.current_epoch) {
  50. sentinel.current_epoch = current_epoch;
  51. sentinelFlushConfig();
  52. sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu",
  53. (unsigned long long) sentinel.current_epoch);
  54. }
  55.  
  56. /* Update master info if received configuration is newer. */
  57. if (master->config_epoch < master_config_epoch) {
  58. master->config_epoch = master_config_epoch;
  59. if (master_port != master->addr->port ||
  60. strcmp(master->addr->ip, token[5]))
  61. {
  62. sentinelAddr *old_addr;
  63.  
  64. sentinelEvent(REDIS_WARNING,"+config-update-from",si,"%@");
  65. sentinelEvent(REDIS_WARNING,"+switch-master",
  66. master,"%s %s %d %s %d",
  67. master->name,
  68. master->addr->ip, master->addr->port,
  69. token[5], master_port);
  70.  
  71. old_addr = dupSentinelAddr(master->addr);
  72. sentinelResetMasterAndChangeAddress(master, token[5], master_port);
  73. sentinelCallClientReconfScript(master,
  74. SENTINEL_OBSERVER,"start",
  75. old_addr,master->addr);
  76. releaseSentinelAddr(old_addr);
  77. }
  78. }
  79.  
  80. /* Update the state of the Sentinel. */
  81. if (si) si->last_hello_time = mstime();
  82. }
  83.  
  84. cleanup:
  85. sdsfreesplitres(token,numtokens);
  86. }

首先,根据消息中的master_name,调用函数sentinelGetMasterByName,在字典sentinel.masters中寻找相应的主节点实例master,如果找不到,则直接退出;

然后,调用getSentinelRedisInstanceByAddrAndRunID函数,根据消息中的sentinel_ip,sentinel_port和sentinel_runid信息,在字典master->sentinels中,找到runid,ip和port都匹配的哨兵实例。

如果没有找到匹配的哨兵实例,要么这是一个新发现的哨兵,要么是某个哨兵的信息发生了变化(比如有可能某个哨兵实例重启了,导致runid发生了变化;或者网络拓扑发生了变化,导致ip或port发生了变化)。

这种情况下,首先调用函数removeMatchingSentinelsFromMaster,删除字典master->sentinels中,具有相同runid,或者具有相同ip和port的哨兵实例;然后根据HELLO消息中的ip和port信息,重新创建一个新的哨兵实例,添加到字典master->sentinels中,这样下次调用sentinelReconnectInstance时,就会向该哨兵实例进行建链了。;

如果找到了匹配的哨兵实例,并且HELLO消息中的sentinel_current_epoch,大于本实例当前的current_epoch,则更新本实例的current_epoch属性;

如果HELLO消息中的master_config_epoch,大于本实例记录的master的config_epoch,则更新本实例记录的master的config_epoch。并且如果HELLO消息中的master_ip或master_port,与本实例记录的主节点的ip或port信息不匹配的话,则说明可能发生了故障转移,某个从节点升级成为了新的主节点,因此调用sentinelResetMasterAndChangeAddress函数,重置主节点,及其从节点实例的信息;

最后,更新si->last_hello_time属性为当前时间;

3:”INFO”命令

“INFO”命令,主要用于哨兵获取主从节点实例当前的状态和信息,比如该实例当前是主节点还是从节点;该实例反馈的IP地址和PORT信息,是否与本哨兵记录的一样;该实例如果是主节点的话,那它都有哪些从节点;该实例如果是从节点的话,它与主节点是否连通,它的优先级是多少,它的复制偏移量是多少等等,这些信息在故障转移流程中,是判断实例状态的重要信息;

在sentinelSendPeriodicCommands函数中,设置的”INFO”命令的回调函数是sentinelInfoReplyCallback。该函数的代码很简单,主要是调用sentinelRefreshInstanceInfo函数对回复进行处理。因此,主要看一下sentinelRefreshInstanceInfo函数的代码:

  1. void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) {
  2. sds *lines;
  3. int numlines, j;
  4. int role = 0;
  5.  
  6. /* The following fields must be reset to a given value in the case they
  7. * are not found at all in the INFO output. */
  8. ri->master_link_down_time = 0;
  9.  
  10. /* Process line by line. */
  11. lines = sdssplitlen(info,strlen(info),"\r\n",2,&numlines);
  12. for (j = 0; j < numlines; j++) {
  13. sentinelRedisInstance *slave;
  14. sds l = lines[j];
  15.  
  16. /* run_id:<40 hex chars>*/
  17. if (sdslen(l) >= 47 && !memcmp(l,"run_id:",7)) {
  18. if (ri->runid == NULL) {
  19. ri->runid = sdsnewlen(l+7,40);
  20. } else {
  21. if (strncmp(ri->runid,l+7,40) != 0) {
  22. sentinelEvent(REDIS_NOTICE,"+reboot",ri,"%@");
  23. sdsfree(ri->runid);
  24. ri->runid = sdsnewlen(l+7,40);
  25. }
  26. }
  27. }
  28.  
  29. /* old versions: slave0:<ip>,<port>,<state>
  30. * new versions: slave0:ip=127.0.0.1,port=9999,... */
  31. if ((ri->flags & SRI_MASTER) &&
  32. sdslen(l) >= 7 &&
  33. !memcmp(l,"slave",5) && isdigit(l[5]))
  34. {
  35. char *ip, *port, *end;
  36.  
  37. if (strstr(l,"ip=") == NULL) {
  38. /* Old format. */
  39. ip = strchr(l,':'); if (!ip) continue;
  40. ip++; /* Now ip points to start of ip address. */
  41. port = strchr(ip,','); if (!port) continue;
  42. *port = '\0'; /* nul term for easy access. */
  43. port++; /* Now port points to start of port number. */
  44. end = strchr(port,','); if (!end) continue;
  45. *end = '\0'; /* nul term for easy access. */
  46. } else {
  47. /* New format. */
  48. ip = strstr(l,"ip="); if (!ip) continue;
  49. ip += 3; /* Now ip points to start of ip address. */
  50. port = strstr(l,"port="); if (!port) continue;
  51. port += 5; /* Now port points to start of port number. */
  52. /* Nul term both fields for easy access. */
  53. end = strchr(ip,','); if (end) *end = '\0';
  54. end = strchr(port,','); if (end) *end = '\0';
  55. }
  56.  
  57. /* Check if we already have this slave into our table,
  58. * otherwise add it. */
  59. if (sentinelRedisInstanceLookupSlave(ri,ip,atoi(port)) == NULL) {
  60. if ((slave = createSentinelRedisInstance(NULL,SRI_SLAVE,ip,
  61. atoi(port), ri->quorum, ri)) != NULL)
  62. {
  63. sentinelEvent(REDIS_NOTICE,"+slave",slave,"%@");
  64. sentinelFlushConfig();
  65. }
  66. }
  67. }
  68.  
  69. /* master_link_down_since_seconds:<seconds> */
  70. if (sdslen(l) >= 32 &&
  71. !memcmp(l,"master_link_down_since_seconds",30))
  72. {
  73. ri->master_link_down_time = strtoll(l+31,NULL,10)*1000;
  74. }
  75.  
  76. /* role:<role> */
  77. if (!memcmp(l,"role:master",11)) role = SRI_MASTER;
  78. else if (!memcmp(l,"role:slave",10)) role = SRI_SLAVE;
  79.  
  80. if (role == SRI_SLAVE) {
  81. /* master_host:<host> */
  82. if (sdslen(l) >= 12 && !memcmp(l,"master_host:",12)) {
  83. if (ri->slave_master_host == NULL ||
  84. strcasecmp(l+12,ri->slave_master_host))
  85. {
  86. sdsfree(ri->slave_master_host);
  87. ri->slave_master_host = sdsnew(l+12);
  88. ri->slave_conf_change_time = mstime();
  89. }
  90. }
  91.  
  92. /* master_port:<port> */
  93. if (sdslen(l) >= 12 && !memcmp(l,"master_port:",12)) {
  94. int slave_master_port = atoi(l+12);
  95.  
  96. if (ri->slave_master_port != slave_master_port) {
  97. ri->slave_master_port = slave_master_port;
  98. ri->slave_conf_change_time = mstime();
  99. }
  100. }
  101.  
  102. /* master_link_status:<status> */
  103. if (sdslen(l) >= 19 && !memcmp(l,"master_link_status:",19)) {
  104. ri->slave_master_link_status =
  105. (strcasecmp(l+19,"up") == 0) ?
  106. SENTINEL_MASTER_LINK_STATUS_UP :
  107. SENTINEL_MASTER_LINK_STATUS_DOWN;
  108. }
  109.  
  110. /* slave_priority:<priority> */
  111. if (sdslen(l) >= 15 && !memcmp(l,"slave_priority:",15))
  112. ri->slave_priority = atoi(l+15);
  113.  
  114. /* slave_repl_offset:<offset> */
  115. if (sdslen(l) >= 18 && !memcmp(l,"slave_repl_offset:",18))
  116. ri->slave_repl_offset = strtoull(l+18,NULL,10);
  117. }
  118. }
  119. ri->info_refresh = mstime();
  120. sdsfreesplitres(lines,numlines);
  121.  
  122. /* ---------------------------- Acting half -----------------------------
  123. * Some things will not happen if sentinel.tilt is true, but some will
  124. * still be processed. */
  125.  
  126. /* Remember when the role changed. */
  127. if (role != ri->role_reported) {
  128. ri->role_reported_time = mstime();
  129. ri->role_reported = role;
  130. if (role == SRI_SLAVE) ri->slave_conf_change_time = mstime();
  131. /* Log the event with +role-change if the new role is coherent or
  132. * with -role-change if there is a mismatch with the current config. */
  133. sentinelEvent(REDIS_VERBOSE,
  134. ((ri->flags & (SRI_MASTER|SRI_SLAVE)) == role) ?
  135. "+role-change" : "-role-change",
  136. ri, "%@ new reported role is %s",
  137. role == SRI_MASTER ? "master" : "slave",
  138. ri->flags & SRI_MASTER ? "master" : "slave");
  139. }
  140.  
  141. /* None of the following conditions are processed when in tilt mode, so
  142. * return asap. */
  143. if (sentinel.tilt) return;
  144.  
  145. /* Handle master -> slave role switch. */
  146. if ((ri->flags & SRI_MASTER) && role == SRI_SLAVE) {
  147. /* Nothing to do, but masters claiming to be slaves are
  148. * considered to be unreachable by Sentinel, so eventually
  149. * a failover will be triggered. */
  150. }
  151. ...
  152. }

该函数首先在for循环中解析"INFO"回复信息:

首先解析出"run_id"之后的信息,保存在ri->runid中。如果该实例的runid发生了变化,还需要记录日志,向"+reboot"频道发布消息;

如果实例为主节点,则解析"slave"后的从节点信息,取出其中的ip和port信息,然后根据ip和port,调用sentinelRedisInstanceLookupSlave函数,在字典ri->slaves中寻找是否已经保存了该从节点的信息。如果没有,则调用createSentinelRedisInstance创建从节点实例,并插入到ri->slaves中,也就是发现了主节点属下的从节点,下次调用函数sentinelReconnectInstance时,就会向该从节点建链了;

解析"master_link_down_since_seconds"信息,该信息表示从节点与主节点的断链时间。将其转换成整数后,记录到ri->master_link_down_time中;

解析"role"信息,如果包含"role:master",则置role为SRI_MASTER,说明该实例报告自己为主节点;如果包含"role:slave",则置role为SRI_SLAVE,说明该实例报告自己为从节点;

如果role为SRI_SLAVE,找到回复信息中的"master_host:"信息,记录到ri->slave_master_host中;找到回复信息中的"master_port:"信息,记录到ri->slave_master_port中;找到回复信息中的"master_link_status:"信息,根据其值是否为"up",记录到ri->slave_master_link_status中;找到回复信息中的"slave_priority:"信息,记录到ri->slave_priority中;找到回复信息中的"slave_repl_offset:"信息,记录到ri->slave_repl_offset中;

解析完所有"INFO"回复信息之后,更新ri->info_refresh为当前时间;

接下来根据实例的角色信息执行一些动作:

ri->role_reported的初始值是根据ri->flags得到的,如果收到"INFO"回复后,解析得到的role与ri->role_reported不同,说明该实例的角色发生了变化,比如从主节点变成了从节点,或者相反。只要role与ri->role_reported不同,就首先更新ri->role_reported_time为当前时间,并且将ri->role_reported置为role;如果role为SRI_SLAVE,还需要更新ri->slave_conf_change_time的值为当前时间;最后,还根据ri->flags中的角色是否与role,来记录日志,发布信息;

如果当前哨兵已经进入了TILT模式,则直接返回;

如果ri->flags中为主节点,但是role为从节点,这种情况无需采取动作,因为这种情况会被视为主节点不可达,最终会引发故障迁移流程;

本函数剩下的动作,与故障转移流程有关,后续在介绍。

七:判断实例是否主观下线

         首先解释一下主观下线和客观下线的区别。

所谓主观下线,就是从“我”(当前实例)的角度来看,某个实例已经下线了。但是单个哨兵的视角可能是盲目的,仅从“我”的角度,就决定一个实例下线是武断的。因此,“我”还会通过命令询问其他哨兵节点,看它们是否也认为该实例已经下线了,如果超过quorum个(包括“我”)哨兵反馈认为该实例已经下线了,则“我”就会认为该实例确实已经下线了,也就是所谓的客观下线了。

判断某个实例主观下线,主要是根据其是否能及时回复”PING”命令决定的。因此,首先看一下发送”PING”命令的函数sentinelSendPing的实现:

  1. int sentinelSendPing(sentinelRedisInstance *ri) {
  2. int retval = redisAsyncCommand(ri->cc,
  3. sentinelPingReplyCallback, NULL, "PING");
  4. if (retval == REDIS_OK) {
  5. ri->pending_commands++;
  6. /* We update the ping time only if we received the pong for
  7. * the previous ping, otherwise we are technically waiting
  8. * since the first ping that did not received a reply. */
  9. if (ri->last_ping_time == 0) ri->last_ping_time = mstime();
  10. return 1;
  11. } else {
  12. return 0;
  13. }
  14. }

在该函数中,设置收到”PING”命令回复后的回调函数为sentinelPingReplyCallback。

需要注意的是,如果ri->last_ping_time值为0,则更新ri->last_ping_time为当前时间。而只有在收到"PING"命令的正常回复之后,ri->last_ping_time的值才会被置为0。

下面是回调函数sentinelPingReplyCallback的代码:

  1. void sentinelPingReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
  2. sentinelRedisInstance *ri = c->data;
  3. redisReply *r;
  4. REDIS_NOTUSED(privdata);
  5.  
  6. if (ri) ri->pending_commands--;
  7. if (!reply || !ri) return;
  8. r = reply;
  9.  
  10. if (r->type == REDIS_REPLY_STATUS ||
  11. r->type == REDIS_REPLY_ERROR) {
  12. /* Update the "instance available" field only if this is an
  13. * acceptable reply. */
  14. if (strncmp(r->str,"PONG",4) == 0 ||
  15. strncmp(r->str,"LOADING",7) == 0 ||
  16. strncmp(r->str,"MASTERDOWN",10) == 0)
  17. {
  18. ri->last_avail_time = mstime();
  19. ri->last_ping_time = 0; /* Flag the pong as received. */
  20. } else {
  21. /* Send a SCRIPT KILL command if the instance appears to be
  22. * down because of a busy script. */
  23. if (strncmp(r->str,"BUSY",4) == 0 &&
  24. (ri->flags & SRI_S_DOWN) &&
  25. !(ri->flags & SRI_SCRIPT_KILL_SENT))
  26. {
  27. if (redisAsyncCommand(ri->cc,
  28. sentinelDiscardReplyCallback, NULL,
  29. "SCRIPT KILL") == REDIS_OK)
  30. ri->pending_commands++;
  31. ri->flags |= SRI_SCRIPT_KILL_SENT;
  32. }
  33. }
  34. }
  35. ri->last_pong_time = mstime();
  36. }

如果回复信息为"PONG","LOADING"或"MASTERDOWN",表示正常回复,因此置该实例的属性ri->last_avail_time为当前时间,并且置ri->last_ping_time为0,这样下次发送"PING"命令时就会更新ri->last_ping_time的值了;

如果回复信息以"BUSY"开头,并且该实例已经被置为主观下线,并且还没有向该实例发送过"SCRIPT KILL"命令,则向该实例发送"SCRIPTKILL"命令;

最后,不管回复信息是什么,更新ri->last_pong_time为当前时间。

因此,有关”PING”命令的时间属性总结如下:

ri->last_ping_time:上一次正常发送”PING”命令的时间。需要注意的是,只有当收到"PING"命令的正常回复后,下次发送"PING"命令时才会更新该属性为当时时间戳。如果发送”PING”命令后,没有收到任何回复,或者没有收到正常回复,则下次发送”PING”命令时,就不会更新该属性。如果该属性值为0,说明已经收到了上一个"PING"命令的正常回复,但是还没有开始发送下一个"PING"命令。检测实例是否主观下线,主要就是根据该属性判断的。

ri->last_pong_time:每当收到"PING"命令的回复后,不管是否是正常恢复,都会更新该属性为当时时间戳;

在哨兵的“主函数”sentinelHandleRedisInstance中,调用sentinelCheckSubjectivelyDown函数检测实例是否主观下线,该函数同时还会检测TCP连接是否正常。该函数的代码如下:

  1. void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
  2. mstime_t elapsed = 0;
  3.  
  4. if (ri->last_ping_time)
  5. elapsed = mstime() - ri->last_ping_time;
  6.  
  7. /* Check if we are in need for a reconnection of one of the
  8. * links, because we are detecting low activity.
  9. *
  10. * 1) Check if the command link seems connected, was connected not less
  11. * than SENTINEL_MIN_LINK_RECONNECT_PERIOD, but still we have a
  12. * pending ping for more than half the timeout. */
  13. if (ri->cc &&
  14. (mstime() - ri->cc_conn_time) > SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
  15. ri->last_ping_time != 0 && /* Ther is a pending ping... */
  16. /* The pending ping is delayed, and we did not received
  17. * error replies as well. */
  18. (mstime() - ri->last_ping_time) > (ri->down_after_period/2) &&
  19. (mstime() - ri->last_pong_time) > (ri->down_after_period/2))
  20. {
  21. sentinelKillLink(ri,ri->cc);
  22. }
  23.  
  24. /* 2) Check if the pubsub link seems connected, was connected not less
  25. * than SENTINEL_MIN_LINK_RECONNECT_PERIOD, but still we have no
  26. * activity in the Pub/Sub channel for more than
  27. * SENTINEL_PUBLISH_PERIOD * 3.
  28. */
  29. if (ri->pc &&
  30. (mstime() - ri->pc_conn_time) > SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
  31. (mstime() - ri->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3))
  32. {
  33. sentinelKillLink(ri,ri->pc);
  34. }
  35.  
  36. /* Update the SDOWN flag. We believe the instance is SDOWN if:
  37. *
  38. * 1) It is not replying.
  39. * 2) We believe it is a master, it reports to be a slave for enough time
  40. * to meet the down_after_period, plus enough time to get two times
  41. * INFO report from the instance. */
  42. if (elapsed > ri->down_after_period ||
  43. (ri->flags & SRI_MASTER &&
  44. ri->role_reported == SRI_SLAVE &&
  45. mstime() - ri->role_reported_time >
  46. (ri->down_after_period+SENTINEL_INFO_PERIOD*2)))
  47. {
  48. /* Is subjectively down */
  49. if ((ri->flags & SRI_S_DOWN) == 0) {
  50. sentinelEvent(REDIS_WARNING,"+sdown",ri,"%@");
  51. ri->s_down_since_time = mstime();
  52. ri->flags |= SRI_S_DOWN;
  53. }
  54. } else {
  55. /* Is subjectively up */
  56. if (ri->flags & SRI_S_DOWN) {
  57. sentinelEvent(REDIS_WARNING,"-sdown",ri,"%@");
  58. ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
  59. }
  60. }
  61. }

ri->cc_conn_time属性表示上一次向该实例发起命令类型的TCP建链的时间;ri->pc_conn_time属性表示上一次向该实例发起订阅类型的TCP建链的时间;

首先计算elapsed的值,该值表示是当前时间与ri->last_ping_time之间的时间差;

然后判断命令类型的TCP连接是否正常,不正常的条件是:距离上次建链时已经超过了SENTINEL_MIN_LINK_RECONNECT_PERIOD,并且上次发送"PING"后还没有收到正常回复,且当前时间与ri->last_ping_time之间的时间差已经超过了ri->down_after_period/2,并且距离上次收到任何"PING"回复的时间,已经超过了ri->down_after_period/2;

如果命令类型的连接不正常了,则直接调用sentinelKillLink断开连接,释放异步上下文;

然后判断订阅类型的TCP连接是否正常,不正常的条件是:距离上次建链时已经超过了SENTINEL_MIN_LINK_RECONNECT_PERIOD,并且距离上次收到订阅频道发来的任何消息的时间,已经超过了SENTINEL_PUBLISH_PERIOD*3;

如果订阅类型的连接不正常了,则直接调用sentinelKillLink断开连接,释放异步上下文;

如果elapsed的值大于ri->down_after_period,或者:当前实例我认为它是主节点,但是它的"INFO"回复中却报告自己是从节点,并且距离上次收到它在"INFO"回复中报告自己是从节点的时间,已经超过了ri->down_after_period+SENTINEL_INFO_PERIOD*2;

满足以上任意一个条件,都认为该实例是主观下线了。因此:只要该实例还没有标志为主观下线,则将SRI_S_DOWN标记增加到实例标志位中,表示该实例主观下线;

如果不满足以上条件,但是该实例之前已经被标记为主观下线了,则认为该实例主观上线了,去掉其标志位中的SRI_S_DOWN和SRI_SCRIPT_KILL_SENT标记;

Redis源码解析:21sentinel(二)定期发送消息、检测主观下线的更多相关文章

  1. Redis源码阅读(二)高可用设计——复制

    Redis源码阅读(二)高可用设计-复制 复制的概念:Redis的复制简单理解就是一个Redis服务器从另一台Redis服务器复制所有的Redis数据库数据,能保持两台Redis服务器的数据库数据一致 ...

  2. # Volley源码解析(二) 没有缓存的情况下直接走网络请求源码分析#

    Volley源码解析(二) 没有缓存的情况下直接走网络请求源码分析 Volley源码一共40多个类和接口.除去一些工具类的实现,核心代码只有20多个类.所以相对来说分析起来没有那么吃力.但是要想分析透 ...

  3. Cwinux源码解析(二)

    我在我的个人博客上发表了第二篇解析文章.欢迎各位读者批评指正. Cwinux源码解析(二)

  4. Spring事务源码解析(二)获取增强

    在上一篇文章@EnableTransactionManagement注解解析中,我们搭建了源码阅读的环境,以及解析了开启Spring事务功能的注解@EnableTransactionManagemen ...

  5. Netty 源码解析(二):Netty 的 Channel

    本文首发于微信公众号[猿灯塔],转载引用请说明出处 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty源码解析(一):开始 当前:Netty 源码解析(二): Netty 的 Channel ...

  6. AOP源码解析之二-创建AOP代理前传,获取AOP信息

    AOP源码解析之二-创建AOP代理前传,获取AOP信息. 上篇文章对AOP的基本概念说清楚了,那么接下来的AOP还剩下两个大的步骤获取定义的AOP信息,生成代理对象扔到beanFactory中. 本篇 ...

  7. 老生常谈系列之Aop--Spring Aop源码解析(二)

    老生常谈系列之Aop--Spring Aop源码解析(二) 前言 上一篇文章老生常谈系列之Aop--Spring Aop源码解析(一)已经介绍完Spring Aop获取advice切面增强方法的逻辑, ...

  8. Redis源码漂流记(二)-搭建Redis调试环境

    Redis源码漂流记(二)-搭建Redis调试环境 一.目标 搭建Redis调试环境 简要理解Redis命令运转流程 二.前提 1.有一些c知识简单基础(变量命名.常用数据类型.指针等) 可以参考这篇 ...

  9. .Net Core缓存组件(Redis)源码解析

    上一篇文章已经介绍了MemoryCache,MemoryCache存储的数据类型是Object,也说了Redis支持五中数据类型的存储,但是微软的Redis缓存组件只实现了Hash类型的存储.在分析源 ...

随机推荐

  1. <day001>存储到Mysql、mongoDB数据库+简单的Ajax请求+os模块+进程池+MD5

    任务1:记住如何存储到Mysql.mongoDB数据库 ''' 存储到Mysql ''' import pymysql.cursors class QuotePipeline(object): def ...

  2. 数据库DQL、DML、DDL及DCL详解

    目录 1. 数据查询语言(DQL,Data Query Language) 2. 数据操纵语言(DML,Data Manipulation Language) 3. 数据定义语言(DDL,Data D ...

  3. Error:Execution failed for task ':app:validateSigningDebug'.

    今天出差回来 第一天   把项目重新移植到新的电脑上 一运行 给我报错了 ! 这个是签名的路径错误  我们需要重新导一下路径就可以了 点击左上角 File ->  project structu ...

  4. 廖雪峰Java14Java操作XML和JSON-1XML-2DOM

    XML是一种数据表示形式. 可以描述非常复杂的数据数据结构 用于传输和传输数据 DOM:Document Object Model DOM模型就是把XML文档作为一个树形结构,从根结点开始,每个节点都 ...

  5. swiper 插件里面嵌套可滚动内容

    在移动端使用swiper的整屏滚动,如果slide里面有滚动内容的话,滚动的时候会整个页面一起滚动,如果想里面的滚动区域单独滚动的话,可以在初始化swiper的时候添加上 noSwipingClass ...

  6. echarts的使用——vue

    在vue的项目开发中,数据的可视化可以用echarts来实现,具体用法如下: (1)安装echarts,进入项目目录,执行如下命令,安装echarts: npm install echarts (2) ...

  7. PAT甲级——A1067 Sort with Swap(0, i)

    Given any permutation of the numbers {0, 1, 2,..., N−1}, it is easy to sort them in increasing order ...

  8. 记一次log4j日志文件小事故

    最近散仙在做公司的一个跟搜索有关的数据分析项目,主要就是统计搜索的转化率,目的主要有以下几个: (1)通过数据分析挖掘,找出搜索业务在整个平台系统里的GMV里所占份额 (2)给公司的搜索算法调优,提供 ...

  9. Leetcode944. Delete Columns to Make Sorted删列造序

    给定由 N 个小写字母字符串组成的数组 A,其中每个字符串长度相等. 选取一个删除索引序列,对于 A 中的每个字符串,删除对应每个索引处的字符. 所余下的字符串行从上往下读形成列. 比如,有 A = ...

  10. [转]Windows钩子

    Windows钩子 Windows应用程序的运行模式是基于消息驱动的,任何线程只要注册了窗口类就会有一个消息队列来接收用户的输入消息和系统消息.为了取得特定线程接收或发送的消息,就要 Windows提 ...