[입 개발] 아는 사람은 알지만 모르는 사람은 모르는 memcached expire 이슈...

Memcached는 아주 유명한 오픈소스 인메모리 캐시 솔루션입니다. 많은 사람들이 사용하기에 이미 그 사용법이 굉장히 많이 알려져있습니다. 그런데, 자주 사용하던 사람들이 아니면 잘 모르는 이상한 동작이 memcached 에는 하나 있습니다. 그것이 바로 expire 입니다.

보통 expire를 Memcached 에 셋팅할때는 second 단위로, expire 되어야 할 상대 시간을 넣습니다. 즉 대부분 예상은 아래와 같이 생각하게 됩니다.

expected expire time = current time + set expire time

그리고 이것은 아주 잘 동작합니다. 우리가 30일 이상 expire time 을 설정하기 전까지는...

넵... 주변에서 expire time 을 30일 이상, 즉 60 * 60 * 24 * 30 이상으로 설정하기 전까지는 잘 쓰다가, 왜 30일 이상으로 설정하면 데이터가 바로 사라져서 정상 동작하지 않는다라고 말씀하시는 분들이 속출합니다. 물론 이게 30일인지 아는것도 시간이 걸린다는... 미안해... 에단...

사실 이 문제는 memcached를 오래 다뤄부신 분들은 대부분 한번씩 겪어보는 장애(?) 또는 현상입니다. 왜냐하면 신기하게도 memcached 는 30일이 넘어가는 값에 대해서는 해당 값이 절대 시간이라고 처리를 해버립니다. 왜냐고 묻지 말아주세요. 제가 만든건 아니라서...

먼저 expire 를 처리하게 되는 items.c 안의 do_item_get 을 살펴보도록 하겠습니다. item이 expire 가 되는 경우는 주로 item 에 접근하게 될 때, 시간을 체크합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
item *do_item_get(const char *key, const size_t nkey, const uint32_t hv, conn *c) {
    ......
    if (it != NULL) {
        was_found = 1;
        if (item_is_flushed(it)) {
            do_item_unlink(it, hv);
            do_item_remove(it);
            it = NULL;
            pthread_mutex_lock(&c->thread->stats.mutex);
            c->thread->stats.get_flushed++;
            pthread_mutex_unlock(&c->thread->stats.mutex);
            if (settings.verbose > 2) {
                fprintf(stderr, " -nuked by flush");
            }
            was_found = 2;
        } else if (it->exptime != 0 && it->exptime <= current_time) { do_item_unlink(it, hv); do_item_remove(it); it = NULL; pthread_mutex_lock(&c->thread->stats.mutex);
            c->thread->stats.get_expired++;
            pthread_mutex_unlock(&c->thread->stats.mutex);
            if (settings.verbose > 2) {
                fprintf(stderr, " -nuked by expire");
            }
            was_found = 3;
        } else {
            it->it_flags |= ITEM_FETCHED|ITEM_ACTIVE;
            DEBUG_REFCNT(it, '+');
        }
    }
    ......
}

코드를 보면 it->exptime 이 0이 아니고 it->exptime <= current_time 으면 item을 expire 처리하게 됩니다. 명확하죠. 그럼 이제 exptime을 설정하는 코드를 살펴보도록 하겠습니다. exptime 은 realtime 이라는 함수를 이용해서 처리하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
#define REALTIME_MAXDELTA 60*60*24*30
static rel_time_t realtime(const time_t exptime) {
    if (exptime == 0) return 0;
    if (exptime > REALTIME_MAXDELTA) {
        if (exptime <= process_started)
            return (rel_time_t)1;
        return (rel_time_t)(exptime - process_started);
    } else {
        return (rel_time_t)(exptime + current_time);
    }
}

위의 코드를 보면 exptime 이 0일때는 값을 0으로 리턴합니다. exptime이 0일 때는 expire time이 설정되지 않은 아이템입니다. 이제 그 다음 코드를 보시면 exptime > REALTIME_MAXDELTA 라는 조건이 있습니다. 그리고 그 값은 위에 60*60*24*30 으로 정의되어 있습니다. 즉 30일이죠.

그래서 설정한 exptime이 30일이 넘어가면, extpime <= process_started 조건에 만족하면 그냥 1로 셋팅합니다. 즉 1초 뒤에 다 지워지는 겁니다. process_started 는 프로세스가 처음에 시작한 시간입니다. 그리고 REALTIME_MAXDELTA 보다 적으면, current_time 에 exptime을 저장합니다. 즉 상대시간으로 인식이 되는 거죠.

그리고 마지막으로 current_time은 clock_handler 함수에서 설정되어집니다. 아래와 같이 current_time 에서는 이미 process_started 가 빠져 있습니다.

1
2
3
4
5
6
7
static void clock_handler(const int fd, const short which, void *arg) {
        ......
        struct timeval tv;
        gettimeofday(&tv, NULL);
        current_time = (rel_time_t) (tv.tv_sec - process_started);
        ......
}

그럼 30일 이상의 값을 셋팅하고 싶을 때는 어떻게 해야 할까요? 넵 바로 그날짜에 맞는 시간값을 넣어주시면 됩니다. 예를 들어 지금부터 한달 뒤인 2016/11/22 에 맞는 unixtimestamp 로 설정하시면 됩니다. 즉 오늘 날짜를 구해서 거기에 원하는 날짜 만큼 더한 unixtimestamp로 expire time을 설정하시면 제대로 된 expire를 설정할 수 있습니다.

자 이제, memcached의 expire time을 설정할때는 항상 주의하셔야 합니다. 반대로 Redis 는 그냥 -_- 정한 값이 expire time 입니다. Redis 에서는 이런 이슈는 없습니다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.