[입 개발] 왜 Redis 2.4.12 까지 keys 명령에 문제가 발생했을까?

음, 오래간만에 입개발을 올리네요. 얼마전에 아시는 분의 Redis 서버에 장애가 나서 이런 저런 추측을 하다가, Redis 2.4.12 or Redis 2.6.0-RC1 까지 keys 에 문제가 있었다는 걸 알게 되었습니다. http://redis.io/topics/problems 를 참고하시면 됩니다. 평소에는 문제가 없다가 expire 되는 키가 있을 때만 발생하는 이슈였는데요. 왜 그런 이슈가 있었는지 그냥, 소스를 보다가 -_- 내부구조 이해도 할겸 해서 코드를 살짝 리딩했습니다.

 

일단 2.4.12 에서 2.4.13 으로 바뀌면서 keys 명령의 코드는 딱 한줄이 바뀌었습니다.


di = dictGetIterator(c->db->dict);

 

위의 라인이 2.4.13에서는


di = dictGetSafeIterator(c->db->dict);

 

뭐가 바뀌었는지 눈치채셨나요? 그냥 iterator 함수명이 바뀌었습니다. 그리고 그 내부를 까보면 다음과 같습니다.


dictIterator *dictGetSafeIterator(dict *d) {
 dictIterator *i = dictGetIterator(d);

 i->safe = 1;
 return i;
}

 

보시면 원래의 함수를 부르고 iterator의 safe를 1로 셋팅하는 것 밖에 없습니다.  그런데 버그가 고쳐졌다라는 부분이 의심스러워집니다. 왜, 도대체 무슨 이유로!!!

 

그래서 위의 safe 를 찾아서 코드를 뒤져보았습니다. 다행히 2~3군데 밖에 안쓰더군요(다행히!!!) 그 중에 보시면 되는 것이 dict.c 의 dictNext 함수입니다.


dictEntry *dictNext(dictIterator *iter)
{
 while (1) {
 if (iter->entry == NULL) {
dictht *ht = &iter->d->ht[iter->table];


// 아랫부분의 safe를 주목하세요.

if (iter->safe && iter->index == -1 && iter->table == 0)
 iter->d->iterators++;
 iter->index++;
 if (iter->index >= (signed) ht->size) {
 if (dictIsRehashing(iter->d) && iter->table == 0) {
 iter->table++;
 iter->index = 0;
 ht = &iter->d->ht[1];
 } else {
 break;
 }
 }
 iter->entry = ht->table[iter->index];
 } else {
 iter->entry = iter->nextEntry;
 }
 if (iter->entry) {
 /* We need to save the 'next' here, the iterator user
 * may delete the entry we are returning. */
 iter->nextEntry = iter->entry->next;
 return iter->entry;
 }
 }
 return NULL;
}

 

위의 소스를 보면 iter->d->iterators 의 값을 증가시키는게 있습니다.

그리고 이 iterators를 살펴보면 keysCommand 에서 무시무시한 일이 발생합니다. keysCommand를 보시면 expire 된 키를 찾기 위해서 expireIfNeeded 를 호출하게 됩니다. 이 코드는 다시 안에 getExpire 라는 함수를 가져옵니다. 그리고 그 안에서는 다시 dictFind를 호출합니다. 아 힘들다. 그럼 최종적으로 dictFind 에서 _dictRehashStep 라는 함수를 호출할수 있습니다.(조건에 따라서 다릅니다만.) 이 때 안에 Key의 리해싱이 일어나게 됩니다. 그럼 이걸 따라가던 iterator들은 무슨 일이 벌어지게 될까요?  그래서 이전 버전에는 keys가 expire 키가 있을 때 문제가 발생했던 것입니다. 뭐, 지금은 이 safe 변수를 통해서 고쳐져있구요. 그런데 safe 변수 값은 이 패치 이전부터 있던것으로 보아 이런 경우를 미리 예상을 하고 있었는데, 크게 신경을 못쓰고 있었던거 같습니다. 재미난 Redis Internal은 이걸로 마치고 다음번에 또 찾아뵙죠 ㅎㅎㅎ