최근에 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로 하고… 못 고친다는…