[입 개발] RedisSentinel 은 어떻게 새로운 마스터를 선택할까?

해당 블로그는 KT UCloud의 지원을 받고 있습니다.

저는 항상 제가 사용하는 솔루션들의 내부구조를 알고 싶은 욕망을 가지고 있는데, 게을러서 거의 하지못합니다. 그런데, 오늘 제 블로그에 paulina 님이 RedisSentinel 에서 어떻게 다음 마스터를 선택하는가에 대한 질문을 올리셔서, 해당 블로그로 답을 대신할려고 합니다.

 

과연 어떤 서버가 새로운 마스터로 선출될까요? 원칙적으로 이벤트를 캐치해서 새로운 마스터를 찾아야 하는데, 어떤 녀석인지 알 수 있다면, 좀 더 쉽지 않을까요 라는 질문으로 해당 글을 시작합니다. 여기에 답을 하기 전에 먼저 전체 흐름을 따라가 보겠습니다.

 

Redis에서는 정기적으로 수행되어야 하는 작업을 위한 serverCron 이라는 함수 redis.c 에 있습니다. 그리고 그 하단에  다음과 같이 sentinelTimer 라는 함수를 호출하게 됩니다.


/* Run the sentinel timer if we are in sentinel mode. */
 run_with_period(100) {
 if (server.sentinel_mode) sentinelTimer();
 }

즉 모든 작업은 이 sentinelTimer 함수에서 시작하게 됩니다. sentinelTimer 함수는 sentinel.c 파일에 있습니다. 해당 함수는 다음과 같은 두 줄로 되어있습니다.


void sentinelTimer(void) {
 sentinelCheckTiltCondition();
 sentinelHandleDictOfRedisInstances(sentinel.masters);
}

여기서 sentinelCheckTiltCondition 는 해당 Timer 가 다른 작업으로 인해서 늦게 호출될 때, 현재 서비스 상태가 이상하다고 인식을 하는 상태입니다. 이것은 다른 장애와는 다르게 현재 자신의 시스템이 느리다는 것만 인식하는 상태가 됩니다.

그리고 이제 실제로 sentinelHandleDictOfRedisInstances 함수를 통해서 실제 작업이 시작됩니다.


/* Perform scheduled operations for all the instances in the dictionary.
 * Recursively call the function against dictionaries of slaves. */
void sentinelHandleDictOfRedisInstances(dict *instances) {
 dictIterator *di;
 dictEntry *de;
 sentinelRedisInstance *switch_to_promoted = NULL;

/* There are a number of things we need to perform against every master. */
 di = dictGetIterator(instances);
 while((de = dictNext(di)) != NULL) {
 sentinelRedisInstance *ri = dictGetVal(de);

sentinelHandleRedisInstance(ri);
 if (ri->flags & SRI_MASTER) {
 sentinelHandleDictOfRedisInstances(ri->slaves);
 sentinelHandleDictOfRedisInstances(ri->sentinels);
 if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
 switch_to_promoted = ri;
 }
 }
 }
 if (switch_to_promoted)
 sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
 dictReleaseIterator(di);
}

그리고 안의 sentinelHandleRedisInstance 함수가 실제로 redis-sentinel 의 main 함수라고 할 수 있습니다.


/* Perform scheduled operations for the specified Redis instance. */
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
 /* ========== MONITORING HALF ============ */
 /* Every kind of instance */
 sentinelReconnectInstance(ri);
 sentinelPingInstance(ri);

/* Masters and slaves */
 if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
 /* Nothing so far. */
 }

/* Only masters */
 if (ri->flags & SRI_MASTER) {
 sentinelAskMasterStateToOtherSentinels(ri);
 }

/* ============== ACTING HALF ============= */
 /* We don't proceed with the acting half if we are in TILT mode.
 * TILT happens when we find something odd with the time, like a
 * sudden change in the clock. */
 if (sentinel.tilt) {
 if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
 sentinel.tilt = 0;
 sentinelEvent(REDIS_WARNING,"-tilt",NULL,"#tilt mode exited");
 }

/* Every kind of instance */
 sentinelCheckSubjectivelyDown(ri);

/* Masters and slaves */
 if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
 /* Nothing so far. */
 }

/* Only masters */
 if (ri->flags & SRI_MASTER) {
 sentinelCheckObjectivelyDown(ri);
 sentinelStartFailover(ri);
 sentinelFailoverStateMachine(ri);
 sentinelAbortFailoverIfNeeded(ri);
 }
}

sentinelCheckSubjectivelyDown 함수에서 노드의 SDOWN 이 발생했는지 또는 해당 SDOWN에서 복구되었는지를 체크합니다. 이전 포스트서 얘기했듯이, SDOWN은 한대의 서버에서 해당 서버가 장애가 난 것을 확인하는 것이고, 실제 FailOver는 ODOWN이 발생해야 시작됩니다. 이것을 체크하는 것이 sentinelCheckObjectivelyDown 함수입니다.

</pre>
/* Is this instance down accordingly to the configured quorum? */
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
 dictIterator *di;
 dictEntry *de;
 int quorum = 0, odown = 0;

if (master->flags & SRI_S_DOWN) {
 /* Is down for enough sentinels? */
 quorum = 1; /* the current sentinel. */
 /* Count all the other sentinels. */
 di = dictGetIterator(master->sentinels);
 while((de = dictNext(di)) != NULL) {
 sentinelRedisInstance *ri = dictGetVal(de);

if (ri->flags & SRI_MASTER_DOWN) quorum++;
 }
 dictReleaseIterator(di);
 if (quorum >= master->quorum) odown = 1;
 }

/* Set the flag accordingly to the outcome. */
 if (odown) {
 if ((master->flags & SRI_O_DOWN) == 0) {
 sentinelEvent(REDIS_WARNING,"+odown",master,"%@ #quorum %d/%d",
 quorum, master->quorum);
 master->flags |= SRI_O_DOWN;
 master->o_down_since_time = mstime();
 }
 } else {
 if (master->flags & SRI_O_DOWN) {
 sentinelEvent(REDIS_WARNING,"-odown",master,"%@");
 master->flags &= ~SRI_O_DOWN;
 }
 }
}
<pre>

과반 수 이상이 해당 서버가 장애라는 결과가 있으면 odown 으로 설정합니다. 비동기 모드라 마스터 상태를 계속 확인하다가 응답이 설정되어 있으면 해당 값을 읽고 quorum 값을 증가시키고, 특정 시점에서 동작한다고 보시면 될것 같습니다. 그리고 실제로 sentinelFailoverStateMachine 함수에서 Failover 동작을 하게 됩니다.

sentinelFailoverStateMachine 안에서도 실제로 어떤 슬레이브를 마스터로 선출하는지는 sentinelFailoverSelectSlave 함수에 달려 있습니다.

</pre>
void sentinelFailoverSelectSlave(sentinelRedisInstance *ri) {
 sentinelRedisInstance *slave = sentinelSelectSlave(ri);

if (slave == NULL) {
 sentinelEvent(REDIS_WARNING,"-no-good-slave",ri,
 "%@ #retrying in %d seconds",
 (SENTINEL_FAILOVER_FIXED_DELAY+
 SENTINEL_FAILOVER_MAX_RANDOM_DELAY)/1000);
 ri->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
 ri->failover_start_time = mstime() + SENTINEL_FAILOVER_FIXED_DELAY +
 SENTINEL_FAILOVER_MAX_RANDOM_DELAY;
 } else {
 sentinelEvent(REDIS_WARNING,"+selected-slave",slave,"%@");
 slave->flags |= SRI_PROMOTED;
 ri->promoted_slave = slave;
 ri->failover_state = SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE;
 ri->failover_state_change_time = mstime();
 sentinelEvent(REDIS_NOTICE,"+failover-state-send-slaveof-noone",
 slave, "%@");
 }
}

 

그 안에서도 실제 선택은 sentinelSelectSlave 함수가 수행합니다.


/* Select a suitable slave to promote. The current algorithm only uses
 * the following parameters:
 *
 * 1) None of the following conditions: S_DOWN, O_DOWN, DISCONNECTED.
 * 2) last_avail_time more recent than SENTINEL_INFO_VALIDITY_TIME.
 * 3) info_refresh more recent than SENTINEL_INFO_VALIDITY_TIME.
 * 4) master_link_down_time no more than:
 * (now - master->s_down_since_time) + (master->down_after_period * 10).
 *
 * Among all the slaves matching the above conditions we select the slave
 * with lower slave_priority. If priority is the same we select the slave
 * with lexicographically smaller runid.
 *
 * The function returns the pointer to the selected slave, otherwise
 * NULL if no suitable slave was found.
 */

int compareSlavesForPromotion(const void *a, const void *b) {
 sentinelRedisInstance **sa = (sentinelRedisInstance **)a,
 **sb = (sentinelRedisInstance **)b;
 if ((*sa)->slave_priority != (*sb)->slave_priority)
 return (*sa)->slave_priority - (*sb)->slave_priority;
 return strcasecmp((*sa)->runid,(*sb)->runid);
}




sentinelRedisInstance *sentinelSelectSlave(sentinelRedisInstance *master) {
 sentinelRedisInstance **instance =
 zmalloc(sizeof(instance[0])*dictSize(master->slaves));
 sentinelRedisInstance *selected = NULL;
 int instances = 0;
 dictIterator *di;
 dictEntry *de;
 mstime_t max_master_down_time;

max_master_down_time = (mstime() - master->s_down_since_time) +
 (master->down_after_period * 10);

di = dictGetIterator(master->slaves);
 while((de = dictNext(di)) != NULL) {
 sentinelRedisInstance *slave = dictGetVal(de);
 mstime_t info_validity_time = mstime()-SENTINEL_INFO_VALIDITY_TIME;

if (slave->flags & (SRI_S_DOWN|SRI_O_DOWN|SRI_DISCONNECTED)) continue;
 if (slave->last_avail_time < info_validity_time) continue;
 if (slave->info_refresh < info_validity_time) continue;
 if (slave->master_link_down_time > max_master_down_time) continue;
 instance[instances++] = slave;
 }
 dictReleaseIterator(di);
 if (instances) {
 qsort(instance,instances,sizeof(sentinelRedisInstance*),
 compareSlavesForPromotion);
 selected = instance[0];
 }
 zfree(instance);
 return selected;
}

 

결론적으로 새로운 마스터 후보는 다음과 같은 순서로 선택됩니다.

  1. SDOWN, ODOWN, DISCONNECT 된 상태인 슬레이브는 후보에서 제외
  2. last_avail_time 이 info_validity_time 보다 적으면 후보에서 제외
  3. info_refresh 값이 info_validaity_time 보다 적으면 후보에서 제외
  4. master_link_down_time 이 max_master_down_time 보다 크면 후보에서 제외
  5. 남은 후보들 중에서 slave_priority가 높은 녀석이 우선, 아니면 다 같으면 runid로 비교해서 큰 녀석이 선택됨

 

한마디로 요약하면, 그냥 이벤트 받으면 switch-master 이벤트를 체크해서 마스터 변경을 인식하거나, 아니면 SENTINEL get-master-addr-by-name <master name>를 이용하셔야 합니다. 왜냐하면, 위의 값들을 계산해서 처리하기에는 너무 힘들듯 합니다. 그럼 고운하루되시길…