[입 개발] Memcached 의 delete 에 대한 변천사…

최근에 twemproxy 에서 delete 가 제대로 안된다는 제보를 듣고, 코드를 살펴보길 시작했었는데요. 여기서 재미난 것을 발견해서 포스팅을 할려고 합니다.(오래간만의 포스팅이네요.)

먼저 최근의 memcached 소스의 delete 를 보면 다음과 같습니다. 코드를 보면 ntokens 가 3보다 클 경우, 뒤에 0이 나오거나 noreply 가 있을때만 허용이 되고 있습니다.

즉 다음과 같은 경우가 허용이 됩니다. 다만 number는 0만 허용이 됩니다.

delete <key>
delete <key> <number>
delete <key> <noreply>
delete <key> <number> <noreply>

다음 코드를 보면… 쉽게 이해가 되실껍니다.

static void process_delete_command(conn *c, token_t *tokens, const size_t ntokens) {
    char *key;
    size_t nkey;
    item *it;

    assert(c != NULL);

    if (ntokens > 3) {
        bool hold_is_zero = strcmp(tokens[KEY_TOKEN+1].value, "0") == 0;
        bool sets_noreply = set_noreply_maybe(c, tokens, ntokens);
        bool valid = (ntokens == 4 && (hold_is_zero || sets_noreply))
            || (ntokens == 5 && hold_is_zero && sets_noreply);
        if (!valid) {
            out_string(c, "CLIENT_ERROR bad command line format.  "
                       "Usage: delete <key> [noreply]");
            return;
        }
    }

    key = tokens[KEY_TOKEN].value;
    nkey = tokens[KEY_TOKEN].length;

    if(nkey > KEY_MAX_LENGTH) {
        out_string(c, "CLIENT_ERROR bad command line format");
        return;
    }

    if (settings.detail_enabled) {
        stats_prefix_record_delete(key, nkey);
    }

    it = item_get(key, nkey);
    if (it) {
        MEMCACHED_COMMAND_DELETE(c->sfd, ITEM_key(it), it->nkey);

        pthread_mutex_lock(&c->thread->stats.mutex);
        c->thread->stats.slab_stats[it->slabs_clsid].delete_hits++;
        pthread_mutex_unlock(&c->thread->stats.mutex);

        item_unlink(it);
        item_remove(it);      /* release our reference */
        out_string(c, "DELETED");
    } else {
        pthread_mutex_lock(&c->thread->stats.mutex);
        c->thread->stats.delete_misses++;
        pthread_mutex_unlock(&c->thread->stats.mutex);

        out_string(c, "NOT_FOUND");
    }
}

그런데 java의 spymemcached 나 ruby 라이브러리를 보면 실제 위의 파트를 보내지
않습니다. 그런데 python의 경우는 time을 지정할 경우는 값이 전달되면서 실제로
memcached에서 delete가 실패하게 됩니다. 디폴트로는 0이 전달되게 되므로 사실 큰 문제는 없습니다.

    def delete_multi(self, keys, time=0, key_prefix='', noreply=False):
        """Delete multiple keys in the memcache doing just one query.
        >>> notset_keys = mc.set_multi({'a1' : 'val1', 'a2' : 'val2'})
        >>> mc.get_multi(['a1', 'a2']) == {'a1' : 'val1','a2' : 'val2'}
        1
        >>> mc.delete_multi(['key1', 'key2'])
        1
        >>> mc.get_multi(['key1', 'key2']) == {}
        1
        This method is recommended over iterated regular L{delete}s as
        it reduces total latency, since your app doesn't have to wait
        for each round-trip of L{delete} before sending the next one.
        @param keys: An iterable of keys to clear
        @param time: number of seconds any subsequent set / update
        commands should fail. Defaults to 0 for no delay.
        @param key_prefix: Optional string to prepend to each key when
            sending to memcache.  See docs for L{get_multi} and
            L{set_multi}.
        @param noreply: optional parameter instructs the server to not send the
            reply.
        @return: 1 if no failure in communication with any memcacheds.
        @rtype: int
        """

        self._statlog('delete_multi')

        server_keys, prefixed_to_orig_key = self._map_and_prefix_keys(
            keys, key_prefix)

        # send out all requests on each server before reading anything
        dead_servers = []

        rc = 1
        for server in six.iterkeys(server_keys):
            bigcmd = []
            write = bigcmd.append
            extra = ' noreply' if noreply else ''
            if time is not None:
                for key in server_keys[server]:  # These are mangled keys
                    write("delete %s %d%s\r\n" % (key, time, extra))
            else:
                for key in server_keys[server]:  # These are mangled keys
                    write("delete %s%s\r\n" % (key, extra))
            try:
                server.send_cmds(''.join(bigcmd))
            except socket.error as msg:
                rc = 0
                if isinstance(msg, tuple):
                    msg = msg[1]
                server.mark_dead(msg)
                dead_servers.append(server)

        # if noreply, just return
        if noreply:
            return rc

        # if any servers died on the way, don't expect them to respond.
        for server in dead_servers:
            del server_keys[server]

        for server, keys in six.iteritems(server_keys):
            try:
                for key in keys:
                    server.expect("DELETED")
            except socket.error as msg:
                if isinstance(msg, tuple):
                    msg = msg[1]
                server.mark_dead(msg)
                rc = 0
        return rc

그런데 이게 twemproxy 에서는 문제를 일으킵니다. 현재 twemproxy는 다음과 같은 룰만 허용합니다.

delete <key>

즉 뒤에 0이 붙으면… 그냥 잘라버립니다. T.T 그래서 실제로 python-memcache를 쓸 경우, delete_multi를 하면 실제 delete가 안되는 거죠. 흑흑흑…

그런데… 여기서 이유를 찾은게 문제가 아니라… 왜 이 코드가 있을까요? 이 의문을 가진건 위의 memcached 코드에 0이 아니면 에러가 나게 된 코드가… github을 보면… 이 코드가 2009년 11월 25일에 커밋된 코드라는 겁니다.(지금이 2014년인데!!!)

그래서 memcached 코드를 1.4.0 부터 현재 버전까지 뒤져봤습니다. 해당 코드는 그대로입니다. -_- 약간 변경이 있긴한데… 위의 time을 쓰는 코드는 아니었습니다. 그럼 이건 뭐지 할 수 있습니다.

실제로 1.2.7 소스를 보니 defer_delete 라고 해서 시간을 주면 해당 시간 뒤에 삭제되는 명령이 있었습니다. 정확히는 to_delete라는 곳에 넣어두고, item의 expire time을 지워져야할 시간으로 셋팅해주는 겁니다.

그러니 그 관련 코드가 아직까지도 python-memcache에 생존해 있던 것이죠 T.T 흑흑흑, 아마 현재는 거의 아무도 안쓰지 않을가 싶은… 다른 언어도 만들어서 쓸 수 있지만… 최신 버전의 memcached에서는 불가능 하다는…

그냥 이런 이슈가 있다는 걸… 알고 넘어가시면 좋을듯 합니다. 실제로 delete key time noreply 로 현재의 memcached에 호출하면, 실제로는 내부적으로 에러로 처리되서 실제로 안지워지지만, 응답은 먹히는 버그가 있습니다만… 이건 noreply 명령이 여러개일때 실제로 어는것이 에러가 난지를 알 수가 없기 때문에 그냥 known issue로 하고… 못 고친다는…

Advertisements

[입 개발] memcached slabclass 에 대해서…

오래간 만에 memcached 소슬 보니, 너무 잘못 이해하고 있거나 한것들이 많아서 처음부터 새로 보면서 이번에는 기록을 좀 남겨둘려고 합니다. 흔히들 memcached가 내부적으로 메모리를 어떻게 관리하는지 잘 아시지만, 코드 레벨로는 잘 모르실 수도 있기 때문에 그냥 정리합니다.

먼저 간단히 용어를 정리하자면…

  • chunk_size : key + value + flag 정보를 저장하기 위한 최소 크기: 기본 48
  • factor : item size 크기를 얼마만큼씩 증가시킬지 결정하는 값: 기본 1.25
  • Chunk_align_bytes : chunk 할당시에 사용하는 align : 8
  • item_size_max: 최대 item의 크기: 기본 1MB

그리고 사이즌 chunk_size * 1.25^(n-1) 형태로 증가하게 됨.

이제 slab.c를 보면 slabclass_t 를 볼 수 있습니다.

typedef struct {
    unsigned int size;      /* sizes of items */
    unsigned int perslab;   /* how many items per slab */

    void *slots;           /* list of item ptrs */
    unsigned int sl_curr;   /* total free items in list */

    unsigned int slabs;     /* how many slabs were allocated for this class */

    void **slab_list;       /* array of slab pointers */
    unsigned int list_size; /* size of prev array */

    unsigned int killing;  /* index+1 of dying slab, or zero if none */
    size_t requested; /* The number of requested bytes */
} slabclass_t;

static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

MAX_NUMBER_OF_SLAB_CLASSES 는 201로 정의되어 있습니다. 즉 최대 201개의 slabclass 가 만들어지는데, 실제로는 chunk_size 와 factor 값에 따라서 최대 item_size_max를 넘어가 버리면, slabclass는 거기까지만 사용됩니다.(slab_init 를 보면 쉽게 알 수 있습니다.)

/**
 * Determines the chunk sizes and initializes the slab class descriptors
 * accordingly.
 */
void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1;
    unsigned int size = sizeof(item) + settings.chunk_size;

    ......
    ......

    while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
        /* Make sure items are always n-byte aligned */
        if (size % CHUNK_ALIGN_BYTES)
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);

        slabclass[i].size = size;
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size;
        size *= factor;
        if (settings.verbose > 1) {
            fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
                    i, slabclass[i].size, slabclass[i].perslab);
        }
    }

    power_largest = i;
    slabclass[power_largest].size = settings.item_size_max;
    slabclass[power_largest].perslab = 1;
    if (settings.verbose > 1) {
        fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
                i, slabclass[i].size, slabclass[i].perslab);
    }

    ......
    ......

위의 소스에 나오는 size는 slab별로 할당되는 기본 사이즈의 크기이고 위의 slabclass 구조체에는 item_size_max(기본 1MB) 를 넣어주고, perslab 에는 item_size_max / size로 몇개의 아이템이 들어갈 수 있는지 들어가게됩니다.

그리고 이 slab 안에 array로 item 들이 할당되게 됩니다. 기본적으로 array의 크기는 16으로 설정되고 그 뒤로는 2배씩 증가하게 됩니다. 관련 함수는 grow_slab_list를 보시면 됩니다. 그리고 slab에서 사용하는 chunk가 항상 item_size_max 인것은 아니고, size * perslab으로 될 때도 있습니다.(do_slabs_newslab 에서 확인 가능, memory_allocate 를 이용함)

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int len = settings.slab_reassign ? settings.item_size_max
        : p->size * p->perslab;
    char *ptr;

    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
        (grow_slab_list(id) == 0) ||
        ((ptr = memory_allocate((size_t)len)) == 0)) {

        MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
        return 0;
    }

    memset(ptr, 0, (size_t)len);
    split_slab_page_into_freelist(ptr, id);

    p->slab_list[p->slabs++] = ptr;
    mem_malloced += len;
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);

    return 1;
}

slabclass 는 여기까지 하고, 다음번에는 실제 item 의 추가 삭제시에 어떻게 되는가에 대해서 정리해보도록 하겠습니다.

[입 개발] Memcache 와 Redis 에서 incr/decr 은 어떻게 동작할까?

최근에 “memcache에서 incr/decr 의 경우에 int/long 에 따라서 특별한 이슈가 발생하지 않는가?” 에 대한 질문을 받았습니다. 먼저 결론부터 말하면 memcache/redis 에서 이로 인해서 발생하는 특별한 이슈는 없습니다. 기본적으로 memcache/redis 가 모든 값을 스트링 형태로 저장하기 때문입니다.(다만 redis는 좀 복잡합니다.) 그리고 이 값을 strtoll 같은 함수를 이용해서 integer 형태로 바꾼다음에 delta 값을 저장하는 형태기 때문에 그렇습니다. 그리고 memcache/redis 모두 incr/decr이 실제로는 하나의 코드로 동작합니다.( +/- 일뿐이니깐요)

memecache 쪽 소스를 살펴보면 process_arithmetic_command() 함수에서 incr/decr을 처리하게 됩니다.

static void process_arithmetic_command(conn *c, token_t *tokens, const size_t ntokens, const bool incr) {
    char temp[INCR_MAX_STORAGE_LEN];
    uint64_t delta;
    char *key;
    size_t nkey;

    assert(c != NULL);

    set_noreply_maybe(c, tokens, ntokens);

    if (tokens[KEY_TOKEN].length > KEY_MAX_LENGTH) {
        out_string(c, "CLIENT_ERROR bad command line format");
        return;
    }    

    key = tokens[KEY_TOKEN].value;
    nkey = tokens[KEY_TOKEN].length;

    if (!safe_strtoull(tokens[2].value, &delta)) {
        out_string(c, "CLIENT_ERROR invalid numeric delta argument");
        return;
    }

    switch(add_delta(c, key, nkey, incr, delta, temp, NULL)) {
    case OK:
        out_string(c, temp);
        break;
    case NON_NUMERIC:
        out_string(c, "CLIENT_ERROR cannot increment or decrement non-numeric value");
        break;
    case EOM:
        out_string(c, "SERVER_ERROR out of memory");
        break;

    case DELTA_ITEM_NOT_FOUND:
        pthread_mutex_lock(&c->thread->stats.mutex);
        if (incr) {
            c->thread->stats.incr_misses++;
        } else {
            c->thread->stats.decr_misses++;
        }
        pthread_mutex_unlock(&c->thread->stats.mutex);

        out_string(c, "NOT_FOUND");
        break;
    case DELTA_ITEM_CAS_MISMATCH:
        break; /* Should never get here */
    }
}

위의 소스를 보면 strtoull() 을 래핑한 safe_strtoull() 함수가 있는데, 처음에는 먼저 incr/decr 다음의 delta 값을 integer 형태로 바꾸는 부분입니다. 실제 value 값은 add_delta 함수에서 이루어집니다. 실제로 add_delta()는 do_add_delta() 를 호출하고 거기서 다시 safe_strtoull() 함수를 이용해서 value를 integer로 변환합니다. 그런데 여기서 중요한 점은 이런 연산과정이 아니라 strtoull() 함수입니다. 숫자형이 아니면 예를 들어 value 가 “123” 형태가 아닌 “abc” 이런 값이 있다면, 실패하고 이에 대해서 실패로 리턴하게 됩니다. 즉 숫자형태로 변환되지 않는 데이터를 넣으면 incr/decr 연산은 실패하게 됩니다. 즉 memcache에 바이너리 형태로 숫자값을 넣는다면 incr/decr은 쓸 수 없다가 되어버리는 것입니다. 결국 안에서 사용하는 형태는 항상 64bit이니 문제가 될 것이 없습니다.

그럼 redis는 어떨가요? redis 도 전체적으로 memcache 와 유사합니다. 그런데!!!, redis에는 string 형태에 encoding 형태가 REDIS_ENCODING_INT 인것이 있습니다. 그럼 왜 REDIS_ENCODING_INT 를 사용하는가 하면, 메모리를 최대한 줄이기 위해서 입니다. 실제 문자열의 경우 32bit long의 경우 4글자에 값이 들어가는데, 스트링형태로 저장하면 많은 메모리를 차지하게 됩니다. 이게 재미있는게, 최초의 set 에는 스트링으로 그대로 저장이 되고, incr/decr 이 발생하면 그 때, 이 결과값이 LONG 범위안에 들어가면 이 때 createStringObjectFromLongLong() 함수를 이용해서 REDIS_ENCODING_INT 형태로 저장하게 됩니다. 정정합니다. 오산돌구님 말씀대로 setGenericCommand를 호출하기 전에 파라매터에 대해서 tryObjectEncoding을 호출합니다. 여기서 long 범위에 들어가는 것 중에서 10000이하는 공유하는 shared.integers를 사용하고, 그 이외에는 실제로 REDIS_ENCODING_INT로 저장합니다. 소스는 다음과 같습니다. 결론적으로 최초에 들어갈 때 long 범위면 바로 변경되고, 그게 아니더라도 뒤에 변경할 수
있으면 변경된다고 보시면 될것 같습니다.(오산돌구님께 감사를 ㅎㅎㅎ)

robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;

    if (o->encoding != REDIS_ENCODING_RAW)
        return o; /* Already encoded */

    /* It's not safe to encode shared objects: shared objects can be shared
     * everywhere in the "object space" of Redis. Encoded objects can only
     * appear as "values" (and not, for instance, as keys) */
     if (o->refcount > 1) return o;

    /* Currently we try to encode only strings */
    redisAssertWithInfo(NULL,o,o->type == REDIS_STRING);

    /* Check if we can represent this string as a long integer */
    if (!string2l(s,sdslen(s),&value)) return o;

    /* Ok, this object can be encoded...
     *
     * Can I use a shared object? Only if the object is inside a given range
     *
     * Note that we also avoid using shared integers when maxmemory is used
     * because every object needs to have a private LRU field for the LRU
     * algorithm to work well. */
    if (server.maxmemory == 0 && value >= 0 && value < REDIS_SHARED_INTEGERS) {
        decrRefCount(o);
        incrRefCount(shared.integers[value]);
        return shared.integers[value];
    } else {
        o->encoding = REDIS_ENCODING_INT;
        sdsfree(o->ptr);
        o->ptr = (void*) value;
        return o;
    }
}

void incrDecrCommand(redisClient *c, long long incr) {
    long long value, oldvalue;
    robj *o, *new;

    o = lookupKeyWrite(c->db,c->argv[1]);
    if (o != NULL && checkType(c,o,REDIS_STRING)) return;
    if (getLongLongFromObjectOrReply(c,o,&value,NULL) != REDIS_OK) return;

    oldvalue = value;
    if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||
        (incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {
        addReplyError(c,"increment or decrement would overflow");
        return;
    }
    value += incr;
    new = createStringObjectFromLongLong(value);
    if (o)
        dbOverwrite(c->db,c->argv[1],new);
    else
        dbAdd(c->db,c->argv[1],new);
    signalModifiedKey(c->db,c->argv[1]);
    notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
    server.dirty++;
    addReply(c,shared.colon);
    addReply(c,new);
    addReply(c,shared.crlf);
}

소스를 보시면 longlong 형태로 변환하는 getLongLongFromObjectOrReply() 함수가 실패하면 역시 에러를 리턴하는 것을 알 수 있습니다. 이 때 역시, 사용자가 바이너리 형태로 데이터를 넣으면 incr/decr 류의 명령어는 사용할 수 없다는 것을 기억하시면 될 것 같습니다.

[입 개발] memcached 는 predictable 하고 Redis는 unpredictable 하다.

가끔씩 Redis와 memcached 둘 중에 어떤 것이 좋은가에 대한 질문이 들어옵니다. 이 질문에 대한 답변은 그때 그때 달라요입니다.  예를 들어, 캐시로만 사용할 것인지? 아니면 데이터 스토어로 사용할 것인지? 데이터 스트럭처의 사용이 필요한가 여부 등 여러가지 판단을 할 수 있는 기준이 있습니다. 여기서는 오직 캐시의 목적으로만 볼 때에 대한 그래서 AOF, RDB는 제외하고 이에 대해서 간단하게 얘기해볼려고 합니다.

보통의 제 답변은 생산성<성능 이면 Memcached, 생산성>성능 이면 Redis를 추천합니다. 데이터 스트럭처를 제공한다는 것은 개발자에게 엄청나게 편의성을 제공해주니깐요. 그럼 이제 위의 제목에 관심을 가지게 될 것 같습니다. 왜 memcached는 predictable 하고 Redis는 unpredictable 한가?  먼저 이 과정은, 많은 데이터가 set/get/del 될 때라는 가정입니다. 그리고 이는 내부의 메모리 할당 구조의 차이 때문입니다. 모두들 아시다시피, Redis는 필요한 크기 만큼에 대한 메모리를 할당해서 이를 사용합니다. 그런데 memcached는 slab 할당을 이용해서, redis 보다 이런 부분에서 조금 더 나은 성능을 보여줍니다. 이로 인해서 redis의 메모리는 좀 더 fragmentation 이 발생하기 쉽고,  이로 인해 memcached의 성능이 좀 더 predictable 하다고 할 수 있는 것입니다.

 

그런데, 이게 사실 그렇게 단순한 것 만은 아닙니다. memcached의 경우는 4G 메모리를 할당하면, 데이터 영역만 4G이므로, 이를 관리하기 위해서 메모리를 더 많이 사용하게 됩니다. 이로 인해서 의도한 것 이상으로 메모리를 사용하게 됩니다. 그런데 Redis는 딱 전체 메모리를 설정한 것 만큼만 사용하게 됩니다.(기본은 무한대로 계속 사용합니다.) 이럴 경우, swap 확률이 덜해서,  Redis가 좀 더 안정적인 성능을 보여줄 수도 있습니다.  다만 BGSAVE를 사용하면, 또 메모리 사용량이 늘어나게 됩니다.

 

그럼 이것만으로 끝일까요? Redis를 여러 대 사용한다면, 한 서버에 여러대의 Redis 인스턴스를 돌리게 되는데,( memcached 도 마찬가지입니다. ) 다음과 같은 구조가 됩니다.

ms

 

이렇게 되면 하나의 서버가 죽더라도 전체 데이터가 날라가지 않는 일부만 사라지거나, 문제가 적지만, AOF/RDB를 사용해야 한다면 성능상 문제가 발생할 수 있습니다.

즉, 간단한 캐시 레이어 하나를 설계하더라도, swap의 발생 가능성이 다양하고, 여러가지 의존관계가 생기게 됩니다. 그리고 이런 구조를 이해하는 것이 적절한 캐시 성능을

보장하게 됩니다.

[입 개발] twemproxy, redis, memcached 의 서버 사이드 proxy

혹시나 redis 에 관심이 많으신 분이 계시다면, 희소식이 있습니다. 아시는 분들은 이미 아시겠지만 twitter에서 만든 memcached 와 redis를 위한 서버 사이드 proxy 인 twemproxy 가 바로 그것입니다. 사실 memcached만 본다면 이미 moxi 라는 서버 사이드 proxy가 있기 때문에, 크게 생각을 안해도 되겠지만, redis를 쓰시는 입장에서는 꽤 구미가 당기는 소식일 수 밖에 없습니다. antirez도 열광하며, 블로그에 글을 올렸더군요. http://antirez.com/news/44

 

어차피 사용법이나 이런 것은 간단하지만, 간단하게 소개하자면, https://github.com/twitter/twemproxy 에서 해당 소스를 받을 실 수 있고, 혹시나 libtool이 설치가 안되어 있으면 autoreconf 시에 에러가 나므로, 설치해 주시면 될 것 같습니다. 두 번째로 설정을 보면 여러 그룹을 등록 할 수 있도록 되어 있는데, 여러개 다 안쓸 테니 그냥 conf/nutcraker.leaf.yml 정도만 수정하셔도  됩니다. 이 때 redis를 쓰시면 꼭 설정에 redis: true 가 있는지 확인하셔야 합니다.

 


redis: true

 

이게 없으면 memcached text protocol로 동작하게 됩니다. twemproxy는 memcached의 경우 binary protocol은 지원을 안해줍니다. 그 대신 연결이 안되는 서버는 접속 목록에서 제거해주는 기능도 있고 속도도 빠르다고 합니다.(아직 실제 서비스에서는 전 못써 봐서) 그리고, 사용하고 있는 업체들이 twitter, tumblr, pinterest 등으로 이미 트래픽으로는 검증된 회사들이라는 것도 장점이겠죠.

 

그럼 이제 좋은 얘기들은 널려 있으니, 어떤 분들은 쓰면 안되는지 간단하게 소개하려고 합니다. 일단 redis 기능 중에 구현안된 것들이 많습니다. 기본적인 기능은 사용이 가능하지만, 전체 서버를 대상으로 해야 하는 기능들은 계속 지원이 안될 것 같기도 합니다. 예를 들어, “AUTH”, “PING”, “MSET” 이런 것들이 안됩니다. 아마도 전체 서버로 리퀘스트를 요청한다음, 한 서버만 장애났을 때, 기존 프토토콜 앞에서 이를 전달할 수 있는 방법도 없고, rollback 도 어렵기 때문이지 않을까 싶긴 합니다.(개인적으로 저도 redis client-proxy, server-proxy를 만들어보다가 좌절하고 있는 부분이 바로 이 부분들 때문입니다. 그래서 twemproxy를 보니 과감하게 전부 패스군요 T.T)

그리고 keys 같은 명령도 쓰시는 분은 같은 이유로 안되므로, 쓰기가 곤란하실 듯합니다. 사실, keys는 쓰지 말라고 권장되어 있지만, 많은 분들이 쓰시고 계시므로 쿨럭…

 

그럼 간단하게 소개를 마칩니다. 다음번에는 소스에 대해서 좀 까보도록 하겠습니다.

 

 

 

[발 번역] Memcached를 걷어내자. 그건 너무 느리다.

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

해당 글은 http://blog.serverdensity.com/2012/05/11/removing-memcached-because-its-too-slow/ 글을 발번역한 것입니다. 오역에 주의하시길 바랍니다. DB에 로그등을 저장할 경우, DB의 저장 속도가 로그 생성 속도를 따라가지 못하는 케이스가 꽤 있고, 그 해결책으로 제시되는 방법 중에 하나가, 로그를 메모리 캐시 솔루션등에 일시적으로 모으고 그걸 배치 등으로 한번에 저장하는 방법을 많이 이용합니다. 그런데, 해당 기사는 속도 때문에 해당 아키텍처를 사용하다가 MongoDB 1.8에서  2.0으로 전환하면서 해당 솔루션을 제거했습니다. 왜 그런지에 대해서 알아두시면 나름 좋은 방법이 될 듯합니다.

개인적으로는 Server Density에서 사용한 이전 방법이 부하가 더 커질 경우에 사용할 수 있는 좋은 방법이라는 생각이 듭니다. 그리고, 필자의 의견에 동의를 못하는 것이, 필자가 말하는 것처럼, Moxi에서 24ms 이 나오는 것도 이상합니다. 보통 moxi를 사용하더라도 1ms 이하로 나오는 것이 정상이기 때문입니다. 댓글을 보면 Moxi나 Client Library 문제가 아닌가라는 의심이 나옵니다. 개인적으로는 Moxi보다는 클라이언트 라이브러리 이슈나 네트워크 위치나 설정에 따른 문제가 아닐까 의심됩니다. 저도 실제로 네트웍 이슈로 해당 속도가 느려지는 것을 경험해본적이 있습니다. 그리고 memcached 메모리가 swapping 되기 시작하면 속도가 꽤 느려질 수 있습니다. 그러나 필자가 말하는 컴포넌트를 하나 제거할 수 있다라는 장점은 상당히 큰 매력이네요. 여러가지 관점에서 고민해보시면 좋을 듯 합니다. @lqez 님도 비슷한 방법으로 재미를 보신걸로 알고 있습니다.

Memcached를 제거하자. 그건 너무 느리다.

Server Density 코드베이스를 변경하면서, Memcached 를 시스템에서 제거한 이야기를 짧게하려고 합니다. 여기에는 두가지 목적이 있습니다.

  1. UI 캐싱: 계정 데이터를 초기 로딩할 경우( server list, alert list, users lists 등 ) MongoDB 에서 바로 읽어서 해당 데이터가 변경될 때 까지 캐시해두고, 변경되면, 캐시를  제거했습니다.
  2. 병목 MongoDB  1.8에서의 글로벌 락의 성능 영향으로 인해서  모니터링 postback 을 MongoDB에 바로 저장할 수 없었습니다.  그래서 Memcached 에 먼저 데이터를 넣고, 수 많은 웹 클라이언트와는 대조적으로 몇 개의 데몬을 통해서 저장해야 했습니다.


Performance map

이 방법은 MongoDB 2.0이 나오기 전까지는 꽤 잘 동작했습니다. 꽤 똑똑한 Lock 양보 정책으로 Global Lock의 영향이 크게 감소되었고, 2.2에서는 database 수준의 락킹으로 더 좋아질 것입니다. 차후에는 동시성 관련 부분도 더 좋아질 것입니다.

이미 코드베이스에서는 Memcached를 이용하는 것에 영향을 받는 부분은 이미 제거해뒀고, 마침내 Memcached를 완전히 제거할 수 있다는 것을 성능 지표에서 보여주고 있습니다. MongoDB로의 직접 접근이 훨씬 빨라졌기 때문입니다. 확실히,  우리의 평균 DB 쿼리 응답 속도는 0.43 ms 이고 Memcached 로 부터는 24.2ms 가 나옵니다.

Database throughput

Response time

모든 종류의 데이터가 중요한 데이터 저장소로써, 다수의 MongoDB 클러스터가 있고( 시간관련 데이터들은 따로 분리되어 있지만 )  2개의 샤드가 있고 각각의 샤드는 4대의 데이터 노드로 구성되었습니다. 각각의 샤드는 미국내의 워싱턴 DC와 San Jose 에 있는 각각의 데이터 센터에 있고, 각 서버는 8GB 메모리에,  Intel Xeon-SandyBridge Quad Core 3.4Ghz CPUs, 100GB SSDs 장비에 Ubuntu 10.04 LTS를 이용해서 MongoDB를 구동중입니다.  각각은 2Gbps 네트웍으로 연결이 되어 있습니다. ( 역자 주:  8GB 메모리였다면, 메모리를 좀 더 올리는 것도 방법이 아니었을 까 합니다. SSD가 속도가 확실히 빠르긴 하지만, 일단 메모리가 많으면, SSD 보다 더 빠릅니다. )

Nodes

Memcached를 제거함으로써, 시스템이 더더욱 간단해지고, 코어 소프트웨어 스택은 단지 Apache, PHP, python, MongoDB, Ubuntu 로만 구성될 수 있을 것입니다.  이로 인해 Memcached를 독립된 클러스터에서 동작시킬 필요가 없어졌습니다.  Moxi proxy의 failover 처리를 위한 모니터링을 추가할 필요도 없어졌습니다., Ubuntu LTS를 통한 공식 버전을 사용하길 원할 경우에, 또 다른 불편인, PHP와 Python 에 memcached library를 추가할 필요도 없어졌습니다. 무엇보다 24ms 의 응답시간을 제거할 수 있었습니다.

[분산 캐시]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

&nbsp;

static rel_time_t realtime(const time_t exptime) {

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

&nbsp;

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

&nbsp;

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되는 것은 처리가 다르다.
  • 기타 등등