오늘 우리 팀의 조실장님이 Memcached 관련해서 에러가 난다고 보고를 해주셨습니다. 실제 분석까지 대충 다 끝낸… 조실장 화이팅!!!
그래서 과연 그런가 싶어서 memcached 소스를 먼저 열었습니다. 일단 증상은 다음과 같습니다. memcached 에 값을 음수로 설정하고 incr/decr 을 하면 에러가 발생한다. 일단 조건들은 다음과 같습니다.
- client library 는 spymemcached
enum delta_result_type do_add_delta(conn *c, const char *key, const size_t nkey, const bool incr, const int64_t delta, char *buf, uint64_t *cas, const uint32_t hv) { char *ptr; uint64_t value; int res; item *it; it = do_item_get(key, nkey, hv); if (!it) { return DELTA_ITEM_NOT_FOUND; } /* Can't delta zero byte values. 2-byte are the "\r\n" */ if (it->nbytes <= 2) { return NON_NUMERIC; } if (cas != NULL && *cas != 0 && ITEM_get_cas(it) != *cas) { do_item_remove(it); return DELTA_ITEM_CAS_MISMATCH; } ptr = ITEM_data(it); if (!safe_strtoull(ptr, &value)) { do_item_remove(it); return NON_NUMERIC; } if (incr) { value += delta; MEMCACHED_COMMAND_INCR(c->sfd, ITEM_key(it), it->nkey, value); } else { if(delta > value) { value = 0; } else { value -= delta; } MEMCACHED_COMMAND_DECR(c->sfd, ITEM_key(it), it->nkey, value); } pthread_mutex_lock(&c->thread->stats.mutex); if (incr) { c->thread->stats.slab_stats[ITEM_clsid(it)].incr_hits++; } else { c->thread->stats.slab_stats[ITEM_clsid(it)].decr_hits++; } pthread_mutex_unlock(&c->thread->stats.mutex); snprintf(buf, INCR_MAX_STORAGE_LEN, "%llu", (unsigned long long)value); res = strlen(buf); /* refcount == 2 means we are the only ones holding the item, and it is * linked. We hold the item's lock in this function, so refcount cannot * increase. */ if (res + 2 <= it->nbytes && it->refcount == 2) { /* replace in-place */ /* When changing the value without replacing the item, we need to update the CAS on the existing item. */ ITEM_set_cas(it, (settings.use_cas) ? get_cas_id() : 0); memcpy(ITEM_data(it), buf, res); memset(ITEM_data(it) + res, ' ', it->nbytes - res - 2); do_item_update(it); } else if (it->refcount > 1) { item *new_it; new_it = do_item_alloc(ITEM_key(it), it->nkey, atoi(ITEM_suffix(it) + 1), it->exptime, res + 2, hv); if (new_it == 0) { do_item_remove(it); return EOM; } memcpy(ITEM_data(new_it), buf, res); memcpy(ITEM_data(new_it) + res, "\r\n", 2); item_replace(it, new_it, hv); // Overwrite the older item's CAS with our new CAS since we're // returning the CAS of the old item below. ITEM_set_cas(it, (settings.use_cas) ? ITEM_get_cas(new_it) : 0); do_item_remove(new_it); /* release our reference */ } else { /* Should never get here. This means we somehow fetched an unlinked * item. TODO: Add a counter? */ if (settings.verbose) { fprintf(stderr, "Tried to do incr/decr on invalid item\n"); } if (it->refcount == 1) do_item_remove(it); return DELTA_ITEM_NOT_FOUND; } if (cas) { *cas = ITEM_get_cas(it); /* swap the incoming CAS value */ } do_item_remove(it); /* release our reference */ return OK; }
NON_NUMERIC 에러를 리턴하는 경우는 코드에서 2가지 입니다.
- 기존 아이템이 2글자 이하일 경우
- safe_strtoull 결과가 false 일 때
이제 다시 safe_strtoull 함수를 살펴보면 다음과 같습니다. 처음에 데이터를 unsigned long long 으로 받으므로 해당 값을 singed long long 으로 바꾸고… 이게 0보다 적으면(즉 overflow 상황이면) 실제로 – 로 시작하는지 확인합니다. 그렇습니다. 이건 데이터가 문자열로 들어가 있다는 소리!!! 하여튼 왜 그런지는 모르겠지만 memcached 에서는 음수로 셋팅한 값을 incr/decr 하면 안됩니다.
bool safe_strtoull(const char *str, uint64_t *out) { assert(out != NULL); errno = 0; *out = 0; char *endptr; unsigned long long ull = strtoull(str, &endptr, 10); if ((errno == ERANGE) || (str == endptr)) { return false; } if (xisspace(*endptr) || (*endptr == '\0' && endptr != str)) { if ((long long) ull < 0) { /* only check for negative signs in the uncommon case when * the unsigned number is so big that it's negative as a * signed number. */ if (strchr(str, '-') != NULL) { return false; } } *out = ull; return true; } return false; }
재미난건 ascii 에서는 CLIENT_ERROR 에 메시지로 에러 값이, binary 에서는 PROTOCOL_BINARY_RESPONSE_DELTA_BADVAL 이라는 0x06 값이 전달됩니다.
반면에 Redis는 그런 구분 없이 음수형태도 그대로 저장됩니다.
void incrDecrCommand(client *c, long long incr) { long long value, oldvalue; robj *o, *new; o = lookupKeyWrite(c->db,c->argv[1]); if (o != NULL && checkType(c,o,OBJ_STRING)) return; if (getLongLongFromObjectOrReply(c,o,&value,NULL) != C_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; if (o && o->refcount == 1 && o->encoding == OBJ_ENCODING_INT && (value < 0 || value >= OBJ_SHARED_INTEGERS) && value >= LONG_MIN && value <= LONG_MAX) { new = o; o->ptr = (void*)((long)value); } else { 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(NOTIFY_STRING,"incrby",c->argv[1],c->db->id); server.dirty++; addReply(c,shared.colon); addReply(c,new); addReply(c,shared.crlf); }