보통 가장 유명한 캐시 솔루션을 뽑으라면… Memcached 나 Redis 가 될 가능성이 높습니다.(당연히 다른 좋은 솔루션들도 많지만…) 가장 많이 쓰이는 건 이 두가지가 아닐듯 합니다.
Redis의 경우는 text protocol 만 지원하지만, memcached의 경우는 text와 binary protocol 두 가지를 모두 지원합니다. 실제로 binary protocol을 사용하면 당연히 속도도 더 빠릅니다.
그런데… 당연히 text/binary protocol 두 가지를 지원하다고 하면 두 동작이 모두 동일할꺼라고 생각하기 쉽습니다. 그러나… memcached 에서 text와 binary protocol은 미묘하게 동작이 다른 경우가 있습니다. 그리고 그 대표적인 예가 incr 입니다.
먼저 text protocol에서 “incr” 커맨드의 동작을 살펴보면 다음과 같습니다. 실제 text protocol은 process_command 라는 함수에서 동작하게 됩니다. 소스가 엄청 길어서 중간에 “incr”를 찾아보면 process_arithmetic_command 함수를 호출한다는 것만 알면됩니다.
static void process_command(conn *c, char *command) { token_t tokens[MAX_TOKENS]; size_t ntokens; int comm; assert(c != NULL); MEMCACHED_PROCESS_COMMAND_START(c->sfd, c->rcurr, c->rbytes); if (settings.verbose > 1) fprintf(stderr, "<%d %s\n", c->sfd, command); /* * for commands set/add/replace, we build an item and read the data * directly into it, then continue in nread_complete(). */ c->msgcurr = 0; c->msgused = 0; c->iovused = 0; if (add_msghdr(c) != 0) { out_of_memory(c, "SERVER_ERROR out of memory preparing response"); return; } ntokens = tokenize_command(command, tokens, MAX_TOKENS); if (ntokens >= 3 && ((strcmp(tokens[COMMAND_TOKEN].value, "get") == 0) || (strcmp(tokens[COMMAND_TOKEN].value, "bget") == 0))) { process_get_command(c, tokens, ntokens, false); } else if ((ntokens == 6 || ntokens == 7) && ((strcmp(tokens[COMMAND_TOKEN].value, "add") == 0 && (comm = NREAD_ADD)) || (strcmp(tokens[COMMAND_TOKEN].value, "set") == 0 && (comm = NREAD_SET)) || (strcmp(tokens[COMMAND_TOKEN].value, "replace") == 0 && (comm = NREAD_REPLACE)) || (strcmp(tokens[COMMAND_TOKEN].value, "prepend") == 0 && (comm = NREAD_PREPEND)) || (strcmp(tokens[COMMAND_TOKEN].value, "append") == 0 && (comm = NREAD_APPEND)) )) { process_update_command(c, tokens, ntokens, comm, false); } else if ((ntokens == 7 || ntokens == 8) && (strcmp(tokens[COMMAND_TOKEN].value, "cas") == 0 && (comm = NREAD_CAS))) { process_update_command(c, tokens, ntokens, comm, true); } else if ((ntokens == 4 || ntokens == 5) && (strcmp(tokens[COMMAND_TOKEN].value, "incr") == 0)) { process_arithmetic_command(c, tokens, ntokens, 1); } else if (ntokens >= 3 && (strcmp(tokens[COMMAND_TOKEN].value, "gets") == 0)) { process_get_command(c, tokens, ntokens, true); } else if ((ntokens == 4 || ntokens == 5) && (strcmp(tokens[COMMAND_TOKEN].value, "decr") == 0)) { process_arithmetic_command(c, tokens, ntokens, 0); } else if (ntokens >= 3 && ntokens <= 5 && (strcmp(tokens[COMMAND_TOKEN].value, "delete") == 0)) { process_delete_command(c, tokens, ntokens); } else if ((ntokens == 4 || ntokens == 5) && (strcmp(tokens[COMMAND_TOKEN].value, "touch") == 0)) { process_touch_command(c, tokens, ntokens); } else if (ntokens >= 2 && (strcmp(tokens[COMMAND_TOKEN].value, "stats") == 0)) { process_stat(c, tokens, ntokens); } else if (ntokens >= 2 && ntokens <= 4 && (strcmp(tokens[COMMAND_TOKEN].value, "flush_all") == 0)) { time_t exptime = 0; rel_time_t new_oldest = 0; set_noreply_maybe(c, tokens, ntokens); pthread_mutex_lock(&c->thread->stats.mutex); c->thread->stats.flush_cmds++; pthread_mutex_unlock(&c->thread->stats.mutex); if (!settings.flush_enabled) { // flush_all is not allowed but we log it on stats out_string(c, "CLIENT_ERROR flush_all not allowed"); return; } if (ntokens != (c->noreply ? 3 : 2)) { exptime = strtol(tokens[1].value, NULL, 10); if(errno == ERANGE) { out_string(c, "CLIENT_ERROR bad command line format"); return; } } /* If exptime is zero realtime() would return zero too, and realtime(exptime) - 1 would overflow to the max unsigned value. So we process exptime == 0 the same way we do when no delay is given at all. */ if (exptime > 0) { new_oldest = realtime(exptime); } else { /* exptime == 0 */ new_oldest = current_time; } if (settings.use_cas) { settings.oldest_live = new_oldest - 1; if (settings.oldest_live <= current_time) settings.oldest_cas = get_cas_id(); } else { settings.oldest_live = new_oldest; } out_string(c, "OK"); return; } else if (ntokens == 2 && (strcmp(tokens[COMMAND_TOKEN].value, "version") == 0)) { out_string(c, "VERSION " VERSION); } else if (ntokens == 2 && (strcmp(tokens[COMMAND_TOKEN].value, "quit") == 0)) { conn_set_state(c, conn_closing); } else if (ntokens == 2 && (strcmp(tokens[COMMAND_TOKEN].value, "shutdown") == 0)) { if (settings.shutdown_command) { conn_set_state(c, conn_closing); raise(SIGINT); } else { out_string(c, "ERROR: shutdown not enabled"); } } else if (ntokens > 1 && strcmp(tokens[COMMAND_TOKEN].value, "slabs") == 0) { if (ntokens == 5 && strcmp(tokens[COMMAND_TOKEN + 1].value, "reassign") == 0) { int src, dst, rv; if (settings.slab_reassign == false) { out_string(c, "CLIENT_ERROR slab reassignment disabled"); return; } src = strtol(tokens[2].value, NULL, 10); dst = strtol(tokens[3].value, NULL, 10); if (errno == ERANGE) { out_string(c, "CLIENT_ERROR bad command line format"); return; } rv = slabs_reassign(src, dst); switch (rv) { case REASSIGN_OK: out_string(c, "OK"); break; case REASSIGN_RUNNING: out_string(c, "BUSY currently processing reassign request"); break; case REASSIGN_BADCLASS: out_string(c, "BADCLASS invalid src or dst class id"); break; case REASSIGN_NOSPARE: out_string(c, "NOSPARE source class has no spare pages"); break; case REASSIGN_SRC_DST_SAME: out_string(c, "SAME src and dst class are identical"); break; } return; } else if (ntokens == 4 && (strcmp(tokens[COMMAND_TOKEN + 1].value, "automove") == 0)) { process_slabs_automove_command(c, tokens, ntokens); } else { out_string(c, "ERROR"); } } else if (ntokens > 1 && strcmp(tokens[COMMAND_TOKEN].value, "lru_crawler") == 0) { if (ntokens == 4 && strcmp(tokens[COMMAND_TOKEN + 1].value, "crawl") == 0) { int rv; if (settings.lru_crawler == false) { out_string(c, "CLIENT_ERROR lru crawler disabled"); return; } rv = lru_crawler_crawl(tokens[2].value); switch(rv) { case CRAWLER_OK: out_string(c, "OK"); break; case CRAWLER_RUNNING: out_string(c, "BUSY currently processing crawler request"); break; case CRAWLER_BADCLASS: out_string(c, "BADCLASS invalid class id"); break; case CRAWLER_NOTSTARTED: out_string(c, "NOTSTARTED no items to crawl"); break; } return; } else if (ntokens == 4 && strcmp(tokens[COMMAND_TOKEN + 1].value, "tocrawl") == 0) { uint32_t tocrawl; if (!safe_strtoul(tokens[2].value, &tocrawl)) { out_string(c, "CLIENT_ERROR bad command line format"); return; } settings.lru_crawler_tocrawl = tocrawl; out_string(c, "OK"); return; } else if (ntokens == 4 && strcmp(tokens[COMMAND_TOKEN + 1].value, "sleep") == 0) { uint32_t tosleep; if (!safe_strtoul(tokens[2].value, &tosleep)) { out_string(c, "CLIENT_ERROR bad command line format"); return; } if (tosleep > 1000000) { out_string(c, "CLIENT_ERROR sleep must be one second or less"); return; } settings.lru_crawler_sleep = tosleep; out_string(c, "OK"); return; } else if (ntokens == 3) { if ((strcmp(tokens[COMMAND_TOKEN + 1].value, "enable") == 0)) { if (start_item_crawler_thread() == 0) { out_string(c, "OK"); } else { out_string(c, "ERROR failed to start lru crawler thread"); } } else if ((strcmp(tokens[COMMAND_TOKEN + 1].value, "disable") == 0)) { if (stop_item_crawler_thread() == 0) { out_string(c, "OK"); } else { out_string(c, "ERROR failed to stop lru crawler thread"); } } else { out_string(c, "ERROR"); } return; } else { out_string(c, "ERROR"); } } else if ((ntokens == 3 || ntokens == 4) && (strcmp(tokens[COMMAND_TOKEN].value, "verbosity") == 0)) { process_verbosity_command(c, tokens, ntokens); } else { out_string(c, "ERROR"); } return; }
다시 process_arithmetic_command를 살펴보면 다음과 같습니다.
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_of_memory(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 */ } }
case DELTA_ITEM_NOT_FOUND 를 보면 아이템이 없으면 incr_misses를 증가시키고, NOT_FOUND를 리턴합니다. 즉 아이템이 없으면 단순히 없다고 리턴하는 것입니다.
그런데 incr의 binary protocol에서의 동작을 살펴보면 조금 미묘하게 다릅니다.
일단 binary protocol 에서 incr이 수행될때 최종 함수는 complete_incr_bin 이라는 함수입니다.
static void complete_incr_bin(conn *c) { item *it; char *key; size_t nkey; /* Weird magic in add_delta forces me to pad here */ char tmpbuf[INCR_MAX_STORAGE_LEN]; uint64_t cas = 0; protocol_binary_response_incr* rsp = (protocol_binary_response_incr*)c->wbuf; protocol_binary_request_incr* req = binary_get_request(c); assert(c != NULL); assert(c->wsize >= sizeof(*rsp)); /* fix byteorder in the request */ req->message.body.delta = ntohll(req->message.body.delta); req->message.body.initial = ntohll(req->message.body.initial); req->message.body.expiration = ntohl(req->message.body.expiration); key = binary_get_key(c); nkey = c->binary_header.request.keylen; if (settings.verbose > 1) { int i; fprintf(stderr, "incr "); for (i = 0; i < nkey; i++) { fprintf(stderr, "%c", key[i]); } fprintf(stderr, " %lld, %llu, %d\n", (long long)req->message.body.delta, (long long)req->message.body.initial, req->message.body.expiration); } if (c->binary_header.request.cas != 0) { cas = c->binary_header.request.cas; } switch(add_delta(c, key, nkey, c->cmd == PROTOCOL_BINARY_CMD_INCREMENT, req->message.body.delta, tmpbuf, &cas)) { case OK: rsp->message.body.value = htonll(strtoull(tmpbuf, NULL, 10)); if (cas) { c->cas = cas; } write_bin_response(c, &rsp->message.body, 0, 0, sizeof(rsp->message.body.value)); break; case NON_NUMERIC: write_bin_error(c, PROTOCOL_BINARY_RESPONSE_DELTA_BADVAL, NULL, 0); break; case EOM: out_of_memory(c, "SERVER_ERROR Out of memory incrementing value"); break; case DELTA_ITEM_NOT_FOUND: if (req->message.body.expiration != 0xffffffff) { /* Save some room for the response */ rsp->message.body.value = htonll(req->message.body.initial); snprintf(tmpbuf, INCR_MAX_STORAGE_LEN, "%llu", (unsigned long long)req->message.body.initial); int res = strlen(tmpbuf); it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration), res + 2); if (it != NULL) { memcpy(ITEM_data(it), tmpbuf, res); memcpy(ITEM_data(it) + res, "\r\n", 2); if (store_item(it, NREAD_ADD, c)) { c->cas = ITEM_get_cas(it); write_bin_response(c, &rsp->message.body, 0, 0, sizeof(rsp->message.body.value)); } else { write_bin_error(c, PROTOCOL_BINARY_RESPONSE_NOT_STORED, NULL, 0); } item_remove(it); /* release our reference */ } else { out_of_memory(c, "SERVER_ERROR Out of memory allocating new item"); } } else { pthread_mutex_lock(&c->thread->stats.mutex); if (c->cmd == PROTOCOL_BINARY_CMD_INCREMENT) { c->thread->stats.incr_misses++; } else { c->thread->stats.decr_misses++; } pthread_mutex_unlock(&c->thread->stats.mutex); write_bin_error(c, PROTOCOL_BINARY_RESPONSE_KEY_ENOENT, NULL, 0); } break; case DELTA_ITEM_CAS_MISMATCH: write_bin_error(c, PROTOCOL_BINARY_RESPONSE_KEY_EEXISTS, NULL, 0); break; } }
아까 전과 비슷한 코드들이 있습니다. 다시 case DELTA_ITEM_NOT_FOUND 를 찾아보면 뭔가 다른 부분을 볼 수 있을 것입니다. else 문 부분은 text_protocol과 동일한데, if 문이 뭔가 추가되었네요.
if (req->message.body.expiration != 0xffffffff) { /* Save some room for the response */ rsp->message.body.value = htonll(req->message.body.initial); snprintf(tmpbuf, INCR_MAX_STORAGE_LEN, "%llu", (unsigned long long)req->message.body.initial); int res = strlen(tmpbuf); it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration), res + 2); if (it != NULL) { memcpy(ITEM_data(it), tmpbuf, res); memcpy(ITEM_data(it) + res, "\r\n", 2); if (store_item(it, NREAD_ADD, c)) { c->cas = ITEM_get_cas(it); write_bin_response(c, &rsp->message.body, 0, 0, sizeof(rsp->message.body.value)); } else { write_bin_error(c, PROTOCOL_BINARY_RESPONSE_NOT_STORED, NULL, 0); } item_remove(it); /* release our reference */ } else { out_of_memory(c, "SERVER_ERROR Out of memory allocating new item"); } }
코드를 보면 expiration 이 -1이 아니면 값을 셋팅하는 부분이 있습니다. 즉 binary protocol에서는 해당 키가 없더라도… 해당 시점에 생성이 가능합니다. text protocol 에서는 불가능 한데 말이죠. 또한, 최초에 아이템이 없을때만 생성이되고, 있을 경우에는 expiration을 변경하지도 않습니다. 코드를 보니… 궁금증들이 해결이 되는 것 같습니다.