И еще один забавный случай про то, как мы наткнулись на баг, но теперь уже в самом memcached. Это даже, наверное, не баг, а особенность, не упомянутая в документации.

Всем известно, что у memcached есть текстовый протокол. Можно на tcp порт memcached (11211 по умолчанию) зайти телнетом и написать парочку команд. И есть в протоколе команда stats cachedump, которая не документирована, но которой пользуется утилита memcached-tool, входящая в поставку memcached.

Usage: memcached-tool <host[:port]> [mode]

memcached-tool 10.0.0.5:11211 display    # shows slabs
memcached-tool 10.0.0.5:11211            # same.  (default is display)
memcached-tool 10.0.0.5:11211 stats      # shows general stats
memcached-tool 10.0.0.5:11211 dump       # dumps keys and values

Ожидается, что при вызове memcached-tool 127.0.0.1:11211 dump мы получим все ключи и значения (если пренебречь атомарностью). И как-то заметили мы, что содержимое нашего memcached кластера занимает слишком много памяти. Мы, вроде-бы, храним только объекты определенного типа, которых по всей системе не так много, чтобы занять все эти гигабайты, а памяти занято много.

Первая реакция была сделать дамп и посмотреть, что же там лежит. Дамп оказался сильно меньше, чем мы рассчитывали. Причем stats показывал числа, которые были очень близки к фактическому потреблению памяти, а в дампе не было и десятой части от этого. Пришлось лезть в код, и, буквально, сразу было найдено следующее


char *item_cachedump(const unsigned int slabs_clsid, const unsigned int limit, unsigned int *bytes) {
unsigned int memlimit = 2 * 1024 * 1024;   /* 2MB max response size */
...
buffer = malloc((size_t)memlimit);
...
while (it != NULL && (limit == 0 || shown < limit)) {
...
if (bufcurr + len + 6 > memlimit)  /* 6 is END\r\n\0 */
break;
...
}
...
}

Т.е. dump не вернет больше, чем 2 мегабайта ключей. Это ставило крест на анализе содержимого.

Собравшись с духом, я решил, что самое простое решение в данном случае — это добавить к функции item_cachedump() помимо limit еще и offset, соответственно добавив нужное в протокол. Клиент (в данном случае memcached-tool), когда получит, скажем, 900 ключей, сделает еще один запрос, но с offset 900. Если он получит в ответ 0 ключей, значит, он дошел до конца.

Конечно, между запросами содержимое кеша может измениться, и мы можем получить те-же ключи, что и в предыдущем запросе, т.к. в “начало” списка добавилось новое, но на самом деле и оригинальный механизм дампа не был защищен от изменений данных и получая список ключей memcached-tool мог не обнаружить само содержимое ключа.

После всех исправлений (а также рестарта memcached и накопления новых объемов) был получен полный дамп и стало понятно, где у нас текли ключи.

Если кто-то столкнулся с такой же проблемой, то вот у меня в репозитории есть версия 1.4.4 с патчем. Если интересно, почему 1.4.4, так это потому-что именно эта версия входит в базовый репозиторий CentOS 6.

Кстати, с версией 1.4.4 связана еще одна интересная история. В версии 1.4.18 появился LRU_CRAWLER. Это отдельный тред, который обходит весь LRU (Least Recently Used) список, который содержит все ключи, упорядоченные по времени использования. И если у какого ключа закончился TTL, то он его уничтожает.

Но у нас в 1.4.4 такого еще не было. И “протухший” ключ уничтожался при его чтении, или когда заканчивалась память и memcached начинал вытеснять старые записи. Таким образом, если старые ключи никто не читал и память к максимуму не подходила, то они лежали там и их никто не трогал.

Это нам сильно мешало в определении количества живых записей в кеше для аналитики и capacity planning. Поэтому, на основе “улучшенного” dump, про который я написал выше, мы сделали свой crawler, который запускался из cron-а раз в 30 минут и “дотрагивался” до всех ключей, чей expiry был меньше, чем текущее время.