[분산 캐시]Memcached 의 flush_all의 주의 사항을 읽고!!!

주의사항: 제 글에 잘못된 내용이 있어서 이를 수정합니다. 꼭 다음 글을 같이 읽어주시기 바랍니다.

http://geekdani.wordpress.com/2012/05/19/memcached-flush_all-%EB%91%90%EB%B2%88%EC%A7%B8/

(2012/05/19 11:13)

지난번  포스트”Redis와Memcache의flush는왜다를까?”

(https://charsyam.wordpress.com/2012/05/17/%EB%B6%84%EC%82%B0%EC%BA%90%EC%8B%9C-redis-%EC%99%80-memcache%EC%9D%98-flush%EB%8A%94-%EC%99%9C-%EB%8B%A4%EB%A5%BC%EA%B9%8C/)

라는 글을 올린 후, memcached에 공헌도 많이 하시고, 실제로 memcached 커미터 수준인 분께서 다음과 같은 댓글을 올려주셨습니다. 실제로 flush_all 의 사용법에 대해서 주의사항이 있다는 것입니다. 그리고 또 다른 고수님께서도 해당 문제에 대한 퀘스천 마크를 남기셨습니다.(아, 실력 부족이 여실히 들어나네요.  그래도 제 주변에는 고수분들이 많으셔서 다행입니다. 부러우시죠? 이분들이 제 자랑입니다.)

그런데 문제는 아무리 소스를 봐도 알 수가 없다는 것이었습니다.(아, 실력 부족) 다만 코드를 유심히 보면서 고민했던 것은, current_time 이나 oldest_live 가 바뀔 경우에는 해당 가능성이 있다는 것만을 발견했습니다. 그런데 current_time은 보면, clock_hander 에서 값을 현재시간으로 바뀌어주는 코드만 있고, 나머지 부분에서는 바뀌는 부분이 없습니다. 점점 더 미궁으로 빠지는 거죠.

그래서 다시 한번 질문을 올렸습니다. “케이스를 알려주세요” 라구요.

그러자 또 다른 고수 @GeekDani 님께서 다음과 같은 답변을 들어주시고, 감사히 해당 내용에 대해서 블로그 포스팅까지 해주셨습니다.

http://geekdani.wordpress.com/2012/05/19/memcached-flush-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%A3%BC%EC%9D%98%EC%A0%90/

꼭 읽어보시길 바랍니다.

사실 이것만 봐도 대부분의 분들이 이해하실 수 있으실 것 입니다. 불안하신가요? 다만 다행인 것은 대부분의 사람들은 flush_all 다음에 옵션이 있다는 것을 모릅니다. 그리고 그냥 flush_all 만 했을 경우에는, 제가 말했듯이 계속 oldest_live 가 current_time 보다 항상 작기 때문에 문제가 바생하지 않습니다.

예전에 저도 이와 비슷하게 flush_all 을 여러 번 한적이 있는데, 문제가 없었습니다. 캐시 내용이 거의 몇 테라급인데도 말이지요. 아마도 다른 분들도 exptime을 추가로 주는 부분을 안 쓰실 가능성이 높습니다.

그럼 이 글을 왜 적었느냐? 그냥 넘어가기는 아쉬우니, 실제로 flush_all 에 delay 시간을 적어주면 어떻게 되는 것인가에 대한 코드의 조건을 잠시 살펴보도록 하겠습니다.

먼저 flush_all 은 이해를 하고 있으니, flush_all에 옵션을 주면 어떤 동작이 일어나는지 먼저 알고 있어야 합니다. 일단 정확하게 설명하면, flush_all 뒤의 옵션은 “특정 시간에 데이터를 모두 삭제하라” 입니다. .이거 관련해서 뒤에서 다시 한번 설명하겠습니다.

아래의 코드에서 exptime > 0 의 코드가 우리가 flush_all 에 옵션을 추가로 줄 경우 동작하게 되는 것입니다. 다시 살아나는 것을 보니, 분명히, oldest_live 값이 current_time 보다 커질 것이다라고 볼 수 있을껍니다.


if (exptime > 0)

settings.oldest_live = realtime(exptime) - 1;

else /* exptime == 0 */

settings.oldest_live = current_time - 1;

item_flush_expired();

옵션으로 시간이 지정되어있지 않으면 그냥 oldest_live 가 현재 시간으로 지정되지만, 시간이 지정되어 있으면 realtime 이라는 함수를 통해서 변경됩니다. 이게 왜 필요한가? 라고 물어볼 수 있는데, 여기서 memcached 만의 재미난 특징이 하나 있습니다. 아마 expire time 설정해 보신 분들은 한번씩 실수하게 되는 것인데 일단 코드 먼저 보시죠. Memcached의 119라인을 보시면 됩니다.


#define REALTIME_MAXDELTA 60*60*24*30

 

static rel_time_t realtime(const time_t exptime) {

/* no. of seconds in 30 days - largest possible delta exptime */

 

if (exptime == 0) return 0; /* 0 means never expire */

 

if (exptime > REALTIME_MAXDELTA) {

/* if item expiration is at/before the server started, give it an

expiration time of 1 second after the server started.

(because 0 means don't expire).  without this, we'd

underflow and wrap around to some large value way in the

future, effectively making items expiring in the past

really expiring never */

if (exptime <= process_started)

return (rel_time_t)1;

return (rel_time_t)(exptime - process_started);

} else {

return (rel_time_t)(exptime + current_time);

}

}

REALTIME_MAXDELTA 라는 값이 지정되어 있는데 이 값보다 옵션으로 입력된 값이 크면 exptime – process_started 값을 던져줍니다. REALTIME_MAXDELTA 보다 작으면 현재 시간에 해당 값을 더해서 줍니다. 혹시나 process_started 값이 궁금하신 분들도 있으실텐데 그냥 프로세스 시작 값이 저장되어 있고 실제 current_time의 경우 이미 process_started 값이 빠져 있으니 신경안쓰셔서도 됩니다. 다만, exptime이 REALTIME_MAXDELTA 보다는 큰데 현재시간보다 적으면, 그냥 값을 1로 설정합니다. 이러면 그냥 지워졌다고 보시면 됩니다. 즉 원칙은 REALTIME_MAXDELTA 보다 적은 값은 상대 값이고, 그 이상의 값은 절대 값이라는 겁니다(겨우, 이걸 설명하려고 이렇게나 지면을!!! 퍽퍽퍽)

그런데 여기서 Expire time 관련한 아이템은 딱 두 가지 종류가 있습니다.

1)     Expire time 을 지정하지 않은 아이템

2)     Expire time 을 지정한 아이템( 2012/05/19 11:13 이 부분의 내용이 잘못되었습니다. 위에 지정한 부분을 같이 읽어주세요. 다만 잘못된 생각의 흐름을 남기기 위해서 글을 수정하지 않고 부분부분 표시만 해둡니다. 감사합니다.  )

Expire Time을 지정하지 않은 아이템의 경우는 크게 상관이 없습니다. 그냥 그 시간이 되면 사라지는 겁니다.(lazy delete이긴 하지만) 문제는 2)의 케이스입니다. 먼저 Expire time이 flush_all 에 지정한 옵션 값보다 작다면, 아무 문제가 없습니다. 그런데 초반에 Expire Time을 한 20년 뒤로 잡아두었다면 어떻게 될까요? 사실 이건 flush_all에 delay 시간을 주느냐 안주느냐와는 상관이 없이 문제가 발생합니다. 그래서 사용되는 코드가 아까 슬쩍 지나간 item_flush_expired(); 입니다. Thread.c 의 505라인에 있는데, 단순히 lock을 호출하고 do_item_flush_expired() 을 호출합니다 이런 패턴은 외부에서 호출하는 함수는 Lock을 타고, 내부에서 사용하는 함수는 그냥 호출하게 할 수 있으므로 편리합니다. 이런 패턴을 뭐라고 하는데 까먹었네요. ㅋㅋㅋ

do_item_flush_expired() 는 thread.c 의 548 Line에 있습니다. 소스코드는 다음과 같습니다.


void do_item_flush_expired(void) {

int i;

item *iter, *next;

if (settings.oldest_live == 0)

return;

for (i = 0; i < LARGEST_ID; i++) {

/* The LRU is sorted in decreasing time order, and an item's timestamp

* is never newer than its last access time, so we only need to walk

* back until we hit an item older than the oldest_live time.

* The oldest_live checking will auto-expire the remaining items.

*/

for (iter = heads[i]; iter != NULL; iter = next) {

if (iter->time >= settings.oldest_live) {

next = iter->next;

if ((iter->it_flags & ITEM_SLABBED) == 0) {

do_item_unlink_nolock(iter, hash(ITEM_key(iter), iter->nkey, 0));

}

} else {

/* We've hit the first old item. Continue to the next queue. */

break;

}

}

}

}

(2012/05/19 11:13 코드를 보면 지금까지 사용하던 exptime 이 아니라 time입니다. 이것은 서로 다른 동작입니다. 꼭 상단의 @GeekDani 님 글을 참고하시기 바랍니다.)

코드를 보면 현재 설정된 oldest_live 보다 더 이후의 expire_time을 가지고 있는 것은 미리 삭제해버립니다. 즉 flush_all [딜레이 시간] 이 들어가더라도, 몇몇 아이템은 그 순간 사라지게 됩니다. 여기서 재미난 추측을 하나 해볼 수 있습니다. Expire Time을 굉장히 길게 설정한 아이템이 많다면? Redis 처럼 operation이 블록되는 현상이 벌어질 수 있을 것 같습니다.(뭐, 테스트는 다음에 해보겠습니다.)

그래서 flush_all 을 하게 되면 oldest_live 가 현재 시간으로 설정되었다가, 다시 flush_all 100을 하게 되면, 일부데이터는 바로 사라지지만, 다시 데이터가 복구된 거 같은 효과가 발생합니다. 이 때 oldest_live 값이 current_time 보다 커지기 때문에 get 시에 영향을 안 받는 거죠. 다음 코드 생각나시죠?


if (settings.oldest_live != 0 && settings.oldest_live <= current_time &&

it->time <= settings.oldest_live) {

do_item_unlink(it, hv);

do_item_remove(it);

it = NULL;

if (was_found) {

fprintf(stderr, " -nuked by flush");

}

}

그런데 기억하셔야 할 것은 flush_all [expire time] 이 된 것은 delay가 된 것일 뿐입니다. 즉 해당 시간이 되면 다시 flush_all과 같아집니다. 주의하시기 바랍니다.

결국, 최초에 문제가 발생할 수 있을 만한 부분에서 결국 문제가 발생한 것입니다.

다시 정리하자면.

1)     그냥 flush_all 은 문제 없다. flush_all [expire_time] 을 아는 사람도 거의 없다.

2)     Flush_all 하고 나서 다시 flush_all [expire_time] 하면 해당 시간 이후로 expire_time 이 지정된 아이템과, 이미 get에서 제거된 아이템들을 제외하고는 전부 다시 보이게 된다.

3)     그러나 다시 해당 delay 시간이 지나가면 flush_all이 된다.

4)     그리고 flush_all에 옵션을 줄 수 있는 라이브러리가 별로 없는듯 하다.(php와 python 라이브러에는 그냥 flush_all 만 있네요.. 크게 주의하지 않으셔도 될 듯 합니다.)

5)     당연한 얘기지만, flush_all [expire_time] 주고 나서 그 사이에 set한 데이터들은 모두 사라진다. 까먹지 말자.

 

결론(2012/05/19 11:13 @GeekDani 님의 글을 참고로 수정합니다.)

  • flush_all 후 flush_all [exptime] 하면 exptime까지는 보여집니다. (물론 flush_all [exptime] 전에 GET 했던 것은 살아나는 효과는 일어나지 않습니다.)
  • flush_all [exptime] 후 exptime 안에서 SET은 지나면 사라진다.
  • flush되는 것과 expire되는 것은 처리가 다르다.
  • 기타 등등