Перейти к основному содержимому

3.06. Справочник по Memcached

Разработчику Аналитику Тестировщику
Архитектору Инженеру

Справочник по Memcached

Архитектурные параметры и опции запуска

Memcached — это демон, не имеющий встроенной конфигурационной системы в виде файлов (типа .conf). Все параметры передаются при запуске через аргументы командной строки либо через переменные окружения в wrapper-скриптах (например, systemd unit или init-скрипты). Поведение демона определяется комбинацией этих параметров, и их корректный подбор критичен для достижения баланса между производительностью, отказоустойчивостью и эффективным использованием ресурсов.

Общие параметры

-d — запуск в режиме демона (background). Без этого флага memcached остаётся в foreground, что удобно для отладки или запуска под supervisord/systemd.

-u <user> — задаёт имя пользователя, от имени которого будет работать процесс после инициализации. Требуется при запуске от root (например, при использовании привилегированных портов < 1024), но не рекомендуется оставлять демон под root в production.

-v, -vv, -vvv — уровни детализации логирования. -v выводит базовые события (старт, остановка, ошибки соединений), -vv добавляет информацию о входящих командах (get/set/cas и т.п.), -vvv включает трассировку сетевых пакетов и внутренних операций памяти. Логи пишутся в stderr или syslog, в зависимости от способа запуска.

-h — вывод справки по аргументам.

Параметры привязки и доступа

-l <ip> — указывает IP-адрес, на котором memcached будет принимать соединения. По умолчанию используется INADDR_ANY (0.0.0.0), что означает приём соединений на всех интерфейсах. Для изоляции в инфраструктуре рекомендуется явно указывать внутренний адрес (например, 127.0.0.1 в single-node конфигурации или 10.0.0.123 в кластере). Возможна передача нескольких адресов через запятую (начиная с версии 1.5.16).

-p <port> — TCP-порт для клиентских подключений. Стандартное значение — 11211. Этот порт зарегистрирован в IANA для memcached, и клиентские библиотеки используют его по умолчанию. Изменение может потребоваться при запуске нескольких инстансов на одном хосте.

-U <port> — UDP-порт для приёма команд. UDP-интерфейс поддерживает только ограниченный набор операций (get/multi-get без cas/touch), и его использование не рекомендуется в production из-за отсутствия контроля целостности и невозможности гарантировать доставку. По умолчанию UDP-порт отключён. Если указано 0, UDP отключается.

-6 — включает поддержку IPv6. Без этого флага демон слушает только IPv4. При совместной работе с -l следует указывать адреса в квадратных скобках (например, -l [::1]).

Параметры памяти

-m <num> — объём выделяемой оперативной памяти в мегабайтах. Это наиболее важный параметр, напрямую влияющий на ёмкость кэша и поведение LRU. Значение по умолчанию — 64 МБ. Рекомендуется выделять не более 50–70 % от свободной RAM на хосте, с учётом того, что memcached не поддерживает свопинг и при исчерпании RAM переходит к агрессивной выгрузке данных.

Механизм выделения памяти реализован через slab-аллокатор (см. ниже). Общая выделенная память фиксирована на весь срок работы процесса и не может быть изменена без перезапуска.

Параметры сетевого взаимодействия

-c <num> — максимальное количество одновременных соединений (TCP-сокетов). По умолчанию — 1024. Превышение приводит к отказу в установке нового соединения (error EMFILE или ENFILE). Значение должно учитывать лимиты ядра (ulimit -n) и ожидаемую нагрузку. В высоконагруженных системах часто устанавливают 10 000 и выше.

-t <num> — количество потоков обработки команд (worker threads). По умолчанию — 4. Это число определяет, сколько потоков будет обслуживать входящие запросы после принятия соединения. Каждый поток имеет собственную event loop (на базе epoll/kqueue/select). Важно: потоки НЕ создаются на каждое соединение — соединения распределяются между потоками по схеме «один поток — множество сокетов», что минимизирует накладные расходы на переключение контекста. Увеличение числа потоков целесообразно только при наличии нескольких ядер и высокой параллельной нагрузке. Слишком большое значение может вызвать конкуренцию за lock slab-аллокатора.

-R <num> — максимальное число запросов, обрабатываемых за один цикл event loop в каждом потоке. По умолчанию — 20. Служит для предотвращения «голодания» других соединений при большом числе запросов от одного клиента. Повышение может улучшить пропускную способность при равномерной нагрузке, но понижение — уменьшить задержки при неравномерной.

-a <mask> — маска прав доступа для Unix-сокета (если используется -s). Например, 0755.

-s <path> — путь к Unix-сокету. Альтернатива TCP-подключению. Обеспечивает минимальные накладные расходы при локальном вызове (например, frontend и memcached на одном хосте). Команды передаются через domain socket, без сетевого стека. Не совместим с кластеризацией.

Параметры безопасности и аутентификации

Memcached изначально разрабатывался как внутренний сервис без аутентификации. В открытых сетях это создаёт риски (см. уязвимость 2018 г., связанную с амплификацией DDoS через UDP). Для повышения безопасности:

  • отключается UDP (-U 0);
  • ограничивается интерфейс (-l 127.0.0.1 или внутренний VLAN);
  • используется фаервол (iptables/nftables).

Начиная с версии 1.5.13, появилась опциональная поддержка SASL-аутентификации:

-S — включает SASL (Simple Authentication and Security Layer). Требует сборки с флагом --enable-sasl, настройки /etc/sasl2/memcached.conf, и поддержки на стороне клиента (например, libmemcached с SASL). После включения все команды, кроме quit и version, требуют предварительной аутентификации (auth <user> <password>). Шифрования трафика (TLS) memcached не поддерживает — для этого требуется внешний прокси (stunnel, haproxy с TLS-терминацией).

Параметры slab-аллокатора и управления памятью

Memcached использует slab-аллокатор, чтобы избежать фрагментации кучи и обеспечить O(1)-сложность операций выделения/освобождения. Память делится на страницы (обычно 1 МБ), страницы группируются в slab classes — классы, каждый из которых хранит значения примерно одинакового размера.

-f <factor> — фактор роста размера слотов (chunks) между соседними slab-классами. По умолчанию — 1.25. Например: если slab 1 хранит объекты ≤ 96 Б, slab 2 — ≤ 120 Б (96 × 1.25), slab 3 — ≤ 150 Б (120 × 1.25) и т.д. Уменьшение фактора увеличивает число классов, снижает внутреннюю фрагментацию («wasted bytes per item»), но повышает накладные расходы на управление. Увеличение — наоборот.

-n <size> — минимальный размер резервируемого пространства под метаданные (ключ + заголовок) в байтах. По умолчанию — 48. Это базовый «накладной» объём на каждый элемент. Если реальный ключ + структурные данные (flags, expiry, cas id) занимают меньше — остаток остаётся неиспользованным в чанке.

-L — попытка использовать большие страницы памяти (HugeTLB, 2 МБ или 1 ГБ). Может снизить overhead TLB и повысить производительность на NUMA-системах. Но требует наличия достаточного количества huge pages (/proc/sys/vm/nr_hugepages) и прав. При неудаче демон продолжает работу с обычными страницами.

-- slab_automove <0|1|2> — стратегия балансировки slab-классов при нехватке памяти:

  • 0 — отключено (по умолчанию);
  • 1 — фоновый перенос страниц из малоиспользуемых slab-классов в переполненные (раз в 1–10 сек, по эвристике);
  • 2 — агрессивный режим: перенос происходит чаще и учитывает статистику hit/miss.

Это помогает бороться с slab fragmentation — ситуацией, когда в одном slab-классе есть свободные чанки, но новый объект не помещается в них из-за размера, тогда как в другом классе свободная память простаивает.

-- slab_chunk_max <size> — максимальный размер чанка (в байтах), который может быть выделен в отдельном slab-классе. По умолчанию — не ограничен, но логически ограничен размером страницы (≈1 МБ). Полезен для ограничения потребления памяти под очень крупные объекты.

Параметры статистики и отладки

-o <opt1,opt2,...> — список дополнительных параметров (comma-separated), вводимых единым флагом. Примеры:

  • verbose — эквивалент -vv;
  • maxconns_fast — при достижении -c возвращать ошибку SERVER_ERROR maxconns сразу, без попытки close() — повышает предсказуемость;
  • hashpower_init=<N> — начальная степень хэш-таблицы (размер = 2^N бакетов). По умолчанию — динамически подбирается, но можно зафиксировать (например, hashpower_init=16 → 65 536 бакетов). Недостаточный размер вызывает коллизии и замедление get/set; избыточный — излишний расход памяти на empty buckets;
  • no_lru_crawler — отключает фоновый LRU crawler, который ищет устаревшие объекты для принудительного удаления (даже до истечения TTL), чтобы поддерживать hit ratio;
  • lru_crawler_sleep=<ms> — задержка между обходами LRU crawler (по умолчанию 100 мс);
  • lru_crawler_tocrawl=<num> — сколько элементов проверять за один проход;
  • idle_timeout=<sec> — автоматическое закрытие «спящих» соединений без активности (по умолчанию 0 — отключено).

Протокол взаимодействия: команды, семантика, форматы данных

Memcached поддерживает два протокола: ASCII-протокол (человекочитаемый, используется по умолчанию) и binary-протокол (более компактный, позволяет избежать парсинга строк, поддерживает дополнительные флаги). Оба протокола передаются поверх TCP (или Unix-сокета); UDP поддерживает только подмножество ASCII-команд (в основном get, gets, quit) и не рекомендуется.

Все операции идемпотентны (кроме incr/decr и CAS-операций при конфликтах), и не имеют транзакционных гарантий. Memcached — это система с eventual consistency, не поддерживающая ACID.


Формат ключа

Ключ (<key>) в протоколе — это последовательность байтов, не содержащая пробелов, управляющих символов (\r, \n, \0) или не-ASCII символов без явного согласования.

  • Максимальная длина ключа: 250 байт (не символов!). Это жёсткое ограничение, заложенное на уровне аллокатора метаданных.
  • Ключ чувствителен к регистру: "User:1" и "user:1" — разные объекты.
  • Рекомендуется использовать только ASCII-символы [a-zA-Z0-9._-]. Юникод требует кодирования (например, UTF-8 → base64 или urlencode), что увеличивает размер и снижает читаемость.
  • Использование одинакового префикса (например, "sess:", "user_profile:") облегчает отладку и позволяет эффективно использовать flush_prefix в некоторых прокси (например, mcrouter), хотя сам memcached не поддерживает префиксы как отдельную сущность.

Формат значения

Значение (<value>) — произвольная последовательность байтов, максимум 1 МБ (1 048 576 байт) на один элемент. Это ограничение введено для предотвращения блокировки event loop на сериализацию/десериализацию, а также для совместимости со slab-аллокатором (chunk size ≤ page size).

  • Значение не интерпретируется — memcached хранит его как blob.
  • Типизация (строка, число, сериализованный объект) — на стороне клиента.
  • Для хранения структурных данных (JSON, protobuf, msgpack) клиент обязан выполнять сериализацию/десериализацию. Memcached не проверяет формат.
  • Передача бинарных данных допустима как в ASCII-, так и в binary-протоколе (в ASCII-протоколе значение передаётся «как есть», без base64 — но клиент должен корректно обрабатывать \r\n внутри значения).

Управляющие флаги (flags)

Поле flags32-битное беззнаковое целое, передаваемое при записи (set, add, replace и др.) и возвращаемое при чтении.

  • Memcached не использует flags внутренне — это пользовательское поле.
  • Типичные применения:
    • 0x01 — сжатие (gzip/snappy);
    • 0x02 — сериализация (JSON/MsgPack);
    • 0x04 — зашифровано;
    • 0x08 — требует CAS-проверки на стороне приложения;
    • Комбинации: flags = 0x03 → сжато и сериализовано.
  • Клиентская библиотека обязана сохранять и интерпретировать flags; сервер лишь сохраняет и возвращает их.

Время жизни (exptime)

Поле exptime задаёт срок хранения объекта в секундах или как Unix timestamp.

  • Если exptime == 0 — объект хранится бессрочно (до eviction или явного удаления).
  • Если 0 < exptime ≤ 60 × 60 × 24 × 30 = 2 592 000 — интерпретируется как относительное время (секунд с момента записи).
  • Если exptime > 2 592 000 — интерпретируется как абсолютный Unix timestamp (UTC). Это позволяет задавать срок до конкретного момента, независимо от времени записи.
  • По истечении exptime объект переходит в состояние logically expired, но физически может оставаться в памяти до:
    • попытки чтения (lazy expiration: при get обнаруживается просрочка и возвращается NOT_FOUND);
    • обхода LRU crawler’ом;
    • нехватки памяти (eviction).

Нет механизма «callback при истечении» — приложение должно само проверять актуальность.


Команды ASCII-протокола

Все команды завершаются \r\n. Ответы также строковые, с кодами состояния.

Команды записи

set <key> <flags> <exptime> <bytes> [noreply]\r\n<value>\r\n
— универсальная команда записи. Перезаписывает существующее значение или создаёт новое.
Опция noreply (если указана) подавляет ответ сервера — используется для ускорения массовой записи, но делает невозможным обнаружение сетевых ошибок.

add <key> <flags> <exptime> <bytes> [noreply]\r\n<value>\r\n
— записывает значение только если ключ отсутствует. Если ключ существует — возвращает NOT_STORED.

replace <key> <flags> <exptime> <bytes> [noreply]\r\n<value>\r\n
— записывает значение только если ключ существует. Если ключ отсутствует — NOT_STORED.

append <key> <exptime> <bytes> [noreply]\r\n<value>\r\n
— дописывает байты в конец существующего значения. Не затрагивает flags и exptime (значение exptime в команде игнорируется, но должен быть указан — историческая причина; в новых версиях можно передавать 0). Если ключ не существует — NOT_STORED.

prepend <key> <exptime> <bytes> [noreply]\r\n<value>\r\n
— дописывает байты в начало существующего значения. Аналогично append.

Команды чтения

get <key1> [<key2> ... <keyN>]\r\n
— запрос одного или нескольких ключей (multi-get). Ответ: для каждого найденного ключа — блок:
VALUE <key> <flags> <bytes>\r\n<value>\r\n, завершается END\r\n.
Для отсутствующих ключей — ничего не возвращается (тихий miss).
Multi-get позволяет уменьшить сетевые round-trip’ы.

gets <key1> [<key2> ...]\r\n
— как get, но возвращает дополнительное поле CAS-идентификатор в ответе:
VALUE <key> <flags> <bytes> <cas_uniq>\r\n...
Используется для последующей CAS-операции.

CAS — проверка и установка

CAS (Check-And-Set) — механизм optimistic concurrency control.

cas <key> <flags> <exptime> <bytes> <cas_uniq> [noreply]\r\n<value>\r\n
— записывает значение только если текущий CAS-идентификатор объекта совпадает с указанным в команде.
Если совпадает — STORED.
Если не совпадает (кто-то изменил объект между gets и cas) — EXISTS.
Если объект удалён — NOT_FOUND.

CAS-идентификатор — 64-битное целое, инкрементируемое при каждом изменении объекта (включая touch, append, prepend). Гарантирует, что клиент работает с актуальной версией.

Команды управления временем жизни

touch <key> <exptime> [noreply]\r\n
— обновляет exptime существующего объекта без изменения значения и флагов.
Если ключ не существует — NOT_FOUND.
Полезно для продления TTL «горячих» объектов.

Арифметические операции

incr <key> <value> [noreply]\r\n
decr <key> <value> [noreply]\r\n
— атомарные операции над целочисленными значениями в десятичной системе.

Требования:

  • Значение должно быть неотрицательным 64-битным целым, записанным в виде строки (например, "42").
  • value в команде — тоже целое (1 ≤ value ≤ 2⁶⁴−1).
  • Результат не может быть отрицательным: decr останавливается на 0.
  • Если ключ не существует — NOT_FOUND (в отличие от Redis, где можно сделать INCR по несуществующему ключу как 0 + N).
  • CAS-идентификатор инкрементируется при изменении.

Расширение incr/decr для float не поддерживается — только целые.

Команды удаления и сброса

delete <key> [noreply]\r\n
— удаляет объект немедленно. Ответ: DELETED или NOT_FOUND.

flush_all [<delay>] [noreply]\r\n
помечает все существующие объекты как устаревшие, но не освобождает память сразу.
Если указан delay (в секундах) — операция применяется асинхронно через указанное время.
После flush_all новые get возвращают NOT_FOUND, но память остаётся занята до eviction или LRU-очистки.
⚠️ Опасная команда: в production её следует блокировать на уровне фаервола или прокси.

Сервисные команды

version\r\nVERSION 1.6.30\r\n
— возвращает версию демона.

stats\r\n
— выводит ключевые метрики:

STAT pid 12345
STAT uptime 86400
STAT time 1732218000
STAT version 1.6.30
STAT libevent 2.1.12-stable
STAT pointer_size 64
STAT rusage_user 123.456
STAT rusage_system 78.901
STAT curr_connections 42
STAT total_connections 10000
STAT connection_structures 50
STAT reserved_fds 20
STAT cmd_get 1000000
STAT cmd_set 500000
STAT get_hits 950000
STAT get_misses 50000
STAT ...

stats items\r\n
— статистика по каждому slab-классу:

STAT items:1:number 1000
STAT items:1:age 3600
STAT items:1:evicted 10
STAT items:1:evicted_nonzero 5
STAT items:1:evicted_time 120
STAT items:1:outofmemory 0
STAT items:1:tailrepairs 0
...

stats slabs\r\n
— детали по slab-аллокатору:

STAT 1:chunk_size 96
STAT 1:chunks_per_page 10922
STAT 1:total_pages 1
STAT 1:total_chunks 10922
STAT 1:used_chunks 1000
STAT 1:free_chunks 9922
STAT 1:free_chunks_end 0
STAT 1:mem_requested 96000
STAT 1:get_hits 95000
...
STAT active_slabs 5
STAT total_malloced 5242880

stats malloc\r\n (если собран с --enable-mallinfo)
— информация о внутреннем malloc (редко используется).

quit\r\n
— корректное закрытие соединения.


Binary-протокол (кратко)

Binary-протокол использует фиксированный 24-байтный header:

Offset  Size   Field
0 1 magic (0x80 — request, 0x81 — response)
1 1 opcode
2 2 key length
4 1 extras length
5 1 data type (обычно 0x00)
6 2 vbucket id (для совместимости с membase/Couchbase; в memcached игнорируется)
8 4 total body length
12 4 opaque (копируется в ответ)
16 8 CAS (при наличии)

Преимущества:

  • отсутствие парсинга текста → выше производительность;
  • поддержка CAS в set без отдельного gets;
  • флаг quiet (opcode с | 0x80) — noreply на уровне протокола;
  • getq (get quiet) — не возвращает NOT_FOUND, уменьшает трафик при массовых miss’ах;
  • возможность пакетной отправки запросов без ожидания ответа.

Основные opcodes:

  • 0x00get
  • 0x01set
  • 0x02add
  • 0x03replace
  • 0x04delete
  • 0x05incr
  • 0x06decr
  • 0x07quit
  • 0x08flush
  • 0x09getq
  • 0x0Anoop
  • 0x0Bversion
  • 0x0Cgetk (возвращает ключ + значение)
  • 0x0Dgetkq
  • 0x0Eappend
  • 0x0Fprepend
  • 0x10stat
  • 0x11setq
  • 0x12addq
  • 0x13replaceq
  • 0x14deleteq
  • 0x15incrementq
  • 0x16decrementq
  • 0x17quitq
  • 0x18flushq
  • 0x19appendq
  • 0x1Aprependq

Binary-протокол предпочтителен в high-load сценариях и при использовании языков с эффективной работой с бинарными буферами (C, Rust, Java NIO).


Внутренние механизмы

Memcached достигает высокой производительности не только за счёт простоты API, но и благодаря продуманной архитектуре управления памятью. Ключевой элемент — slab-аллокатор, разработанный специально для устранения фрагментации кучи и обеспечения O(1)-сложности операций. Ниже приводится исчерпывающее описание его работы, эвристик, а также сопутствующих механизмов: LRU-политики, eviction, статистики фрагментации и поведения при граничных нагрузках.


Slab-аллокатор: устройство и принцип работы

При запуске демона с параметром -m <N> выделяется единый непрерывный блок виртуальной памяти размером N мегабайт. Этот блок не расширяется динамически — его размер фиксирован на всё время работы процесса.

Память делится на страницы (pages) фиксированного размера — по умолчанию 1 МБ (1 048 576 байт). Каждая страница принадлежит ровно одному slab-классу (slab class).

Slab-класс — это группа страниц, предназначенных для хранения объектов примерно одинакового размера. Внутри класса все страницы структурно идентичны.

Каждая страница разбивается на чанки (chunks) — фиксированные по размеру ячейки хранилища. Размер чанка определяется на этапе инициализации slab-класса и не изменяется в течение работы. Размер чанка всегда не меньше, чем сумма:

  • длина ключа (≤ 250 байт);
  • размер метаданных (~48 байт: item header — CAS, flags, expiry, next/prev указатели, длина ключа и значения);
  • размер значения.

Чанк выравнивается по границе, кратной 8 байтам (для совместимости с 64-битными указателями).

Формирование slab-классов

Slab-классы генерируются динамически при старте, на основе параметров:

  • -n <size> — минимальный размер чанка (по умолчанию 48 байт);
  • -f <factor> — мультипликативный коэффициент роста (по умолчанию 1.25);
  • ограничение: чанк ≤ размер страницы (≈1 МБ).

Алгоритм:

  1. class_1.chunk_size = max(-n, MIN_CHUNK_SIZE)
  2. class_2.chunk_size = ceil(class_1.chunk_size * -f)
  3. class_3.chunk_size = ceil(class_2.chunk_size * -f)
    … и так до тех пор, пока chunk_size + item_overhead ≤ 1 048 576.

Пример (упрощённый, без выравнивания):

Slab-classChunk size (байт)Объекты на страницу (≈1 МБ)Примечание
196~10 922мелкие объекты: сессии, флаги
2120~8 738
3150~6 990
4188~5 576
32102 36010крупные JSON-объекты
33127 9508
421 048 5761максимальный размер

Если клиент пытается сохранить объект размером 151 байт, он попадает в slab 4 (chunk = 188 байт). Разница (188 − 151 = 37 байт) — внутренняя фрагментация, которая не используется и не может быть задействована другими объектами.

Распределение объектов по slab-классам

При выполнении set key ...:

  1. Вычисляется общий размер: total = len(key) + len(value) + item_overhead.
  2. Выбирается минимальный slab-класс, для которого chunk_size ≥ total.
  3. Если в выбранном классе есть свободный чанк — объект помещается туда.
  4. Если свободных чанков нет:
    • выделяется новая страница (если есть свободная память в пуле);
    • или инициируется eviction (см. ниже).

Важно: объект никогда не переходит между slab-классами, даже если его размер изменяется (append, prepend). При append к объекту в классе с chunk=120 новое значение в 130 байт не поместится — операция завершится ошибкой SERVER_ERROR object too large for cache. Это ограничение делает append/prepend малоприменимыми на практике.


Фрагментация памяти: типы и измерение

В Memcached различают два вида фрагментации:

  1. Внутренняя фрагментация (per-item waste)
    — разница между размером чанка и фактически используемым объёмом (chunk_size − (key + value + overhead)).
    Измеряется через статистику:

    stats slabs
    → STAT <class>:mem_requested 96000 # суммарный запрошенный объём
    → STAT <class>:total_malloced 1048576 # объём выделенной памяти (pages × 1MB)

    Waste = total_malloced − mem_requested.

  2. Внешняя фрагментация (slab imbalance)
    — ситуация, когда в одних slab-классах есть свободные чанки, а в других — переполнение и eviction, несмотря на наличие свободной общей памяти.
    Например: класс 1 заполнен на 99 %, класс 10 — на 10 %, но объект на 500 байт не может использовать свободные чанки класса 10, если его размер требует класса 15.

Оценка внешней фрагментации:

stats items
→ STAT items:15:evicted 1000 # много eviction в классе 15
→ STAT items:10:free_chunks 5000 # но в классе 10 — свобода

Меры борьбы:

  • подбор -f (ближе к 1.1 — больше классов, меньше внутренней фрагментации);
  • --slab_automove 1/2 — фоновый перенос страниц;
  • мониторинг распределения размеров объектов и корректировка ключей (например, разбивка крупных объектов на части: profile:123:part1, profile:123:part2).

LRU (Least Recently Used) и его реализация

Memcached использует сегментированную LRU-политику, разделённую по slab-классам. Каждый slab-класс имеет собственный LRU-список, состоящий из трёх цепочек:

  1. HOT LRU — недавно использованные и часто запрашиваемые объекты.
  2. WARM LRU — регулярно используемые, но менее «горячие».
  3. COLD LRU — редко используемые; кандидаты на eviction.

Кроме того, в версиях ≥ 1.5.0 появился TEMP LRU — отдельная очередь для объектов с очень коротким TTL (например, < 60 сек), чтобы не «засорять» основные списки.

Алгоритм перемещения между LRU
  • При get объект перемещается в голову своей LRU-цепочки (HOT → WARM → COLD при остывании).
  • Периодически (раз в несколько секунд) запускается LRU crawler — фоновый поток, который:
    • обходит COLD-список;
    • удаляет объекты с истёкшим exptime;
    • перемещает «тёплые» объекты из COLD → WARM, если их last_accessed_time слишком свежий;
    • собирает статистику hit ratio по классам.

Команды, влияющие на LRU-позицию:

  • get, gets — перемещают в голову HOT;
  • touch — обновляет last_accessed_time, не возвращая данные → «подогрев» без сетевого трафика;
  • flush_all — не сбрасывает LRU-списки, но делает все объекты «невидимыми».

Eviction (вытеснение объектов)

Eviction происходит только при нехватке свободных чанков в нужном slab-классе и отсутствии возможности выделить новую страницу.

Алгоритм:

  1. Выбирается slab-класс, в который должен быть помещён объект.
  2. Если в нём нет свободных чанков и нет свободной общей памяти:
    • из COLD LRU данного класса удаляется самый «холодный» объект (с самым давним last_accessed_time);
    • если COLD пуст — берётся из WARM, затем из HOT (крайне нежелательно).
  3. Объект удаляется без уведомления клиента (silent eviction).
  4. Метрики обновляются: evicted, evicted_time, evicted_nonzero.

Важно: eviction не зависит от TTL. Объект с exptime = 1 час может быть вытеснен раньше объекта с exptime = 10 сек, если первый — cold, а второй — hot.

Метрики eviction в stats items:

  • evicted — общее число вытесненных объектов;
  • evicted_nonzero — из них с ненулевым TTL (то есть «полезных»);
  • evicted_time — время (в секундах), сколько в среднем прожил вытесненный объект до eviction;
  • outofmemory — количество попыток записи, приведших к eviction (не всегда == evicted, т.к. один set может вызвать несколько eviction при фрагментации).

Если evicted_nonzero > 0 — индикатор неоптимальной конфигурации: либо памяти недостаточно, либо распределение размеров несбалансировано.


Стратегии минимизации eviction

  1. Правильный подбор -m
    Используйте stats slabstotal_malloced и mem_requested, чтобы оценить эффективность использования. Цель: mem_requested / total_malloced ≥ 0.85.

  2. Анализ распределения размеров
    Соберите выборку реальных (key_len + value_len) и постройте гистограмму. Если пики приходятся на границы между slab-классами (например, 119/120/121), скорректируйте -f или структуру данных.

  3. Использование префиксов и шардирования на клиенте
    Клиентская библиотека (например, libmemcached) может распределять ключи по разным инстансам memcached (consistent hashing). Это снижает нагрузку на один узел и уменьшает вероятность локального переполнения.

  4. Отказ от крупных объектов
    Объекты > 512 КБ резко снижают эффективность slab-аллокатора (1 страница = 1–2 объекта). Лучше хранить ссылки на внешнее хранилище (например, value = "s3://bucket/key"), а не сами данные.

  5. Настройка -- slab_automove
    При включении (1 или 2) демон в фоне перераспределяет страницы между классами, если обнаруживает дисбаланс. Требует дополнительного CPU, но снижает evicted_nonzero.


Поведение при отказах и ограничения

  • Сбой инстанса → полная потеря данных в нём. Memcached не обеспечивает персистентность и не реплицирует данные.
  • Переполнение памяти → eviction, затем — при исчерпании всех ресурсов — отказ в записи (SERVER_ERROR out of memory), если отключён eviction (через --enable-shutdown + OOM-killer).
  • Переполнение соединенийSERVER_ERROR maxconns (если -R maxconns_fast) или зависание.
  • Некорректные клиенты:
    • отправка \r\n внутри значения в ASCII-протоколе → повреждение потока;
    • слишком частые flush_all → деградация hit ratio;
    • отсутствие обработки NOT_STORED/EXISTS → логические ошибки в приложении.

Клиентские библиотеки, интеграция, мониторинг и сравнительный анализ

Memcached как протокол получил широкое распространение благодаря простоте и эффективности. Для большинства языков существуют как официальные, так и сторонние клиентские реализации. Однако качество, поддерживаемые функции и поведение при отказах сильно различаются. Ниже приводится развернутый обзор, ориентированный на эксплуатацию в production-средах: выбор библиотеки, настройка пулов соединений, стратегии отказоустойчивости, метрики и сравнение с Redis в контексте кэширования.


Клиентские библиотеки

Клиентская библиотека не просто инкапсулирует сетевые вызовы — она определяет поведение системы в целом: как обрабатываются ошибки, как распределяются ключи, как управляются соединения, как обеспечивается согласованность при сбоях.

Ключевые требования к production-библиотеке:

  1. Поддержка binary-протокола — обязательна для снижения overhead парсинга и поддержки CAS в set.
  2. Connection pooling — повторное использование TCP-соединений для устранения стоимости handshake (SYN/ACK) и TIME_WAIT.
  3. Consistent hashing (или ketama) — алгоритм распределения ключей по узлам кластера, минимизирующий перераспределение при добавлении/удалении узла.
    Простое hash(key) % N приводит к ~100 % кэш-промахов при изменении N. Consistent hashing ограничивает перераспределение ~1/N.
  4. Поддержка нескольких инстансов и отказоустойчивость — автоматическое исключение узла при ошибках и возврат после восстановления (через health-check или таймаут).
  5. Интеграция с метриками — возможность сбора get_hits, get_misses, connection_errors, timeout_errors в систему мониторинга.
  6. Безопасная обработка timeout’ов — отмена операции без утечки сокетов.
  7. Поддержка SASL, если аутентификация включена на сервере.
Обзор по языкам
ЯзыкБиблиотекаСтатусОсобенностиРекомендация
C/C++libmemcachedОфициальная, активно поддерживаетсяПоддержка binary, ketama, connection pooling, SASL, memstat, memcp, memcat CLI-утилиты. Используется в PHP-расширении memcached (PECL).✅ Рекомендуется
C#/.NETEnyimMemcachedCoreПопулярная, open-sourceАсинхронная (async/await), поддержка Microsoft.Extensions.Caching.Abstractions, health-check, конфигурация через appsettings.json. Поддерживает ketama и binary.✅ Рекомендуется для .NET Core/5+
Memcached.ClientLibraryУстаревшаяСинхронная, .NET Framework 2.0, отсутствие асинхронности.❌ Не использовать
JavaXMemcachedАктивно развиваетсяВысокая производительность, NIO, binary, SASL, dynamic node discovery.✅ Рекомендуется
spymemcachedУстарела (последний релиз — 2017)Поддержка только ASCII, проблемы с таймаутами в высоконагруженных сценариях.❌ Избегать
PythonpymemcacheАктивно поддерживается (PyPA)Чистый Python, поддержка binary, SASL (через pymemcache.client.hash), serde для сериализации, hasher=Hasher.KETAMA.✅ Рекомендуется
python-memcachedУстаревшаяТолько ASCII, отсутствие connection pooling в стандартной конфигурации.❌ Не использовать
Node.jsmemjsЛёгкая, современнаяПоддержка binary, connection pooling, consistent hashing, Promise/async.✅ Рекомендуется
node-memcachedУстаревшаяКоллбэки, отсутствие binary, проблемы с большим числом соединений.❌ Избегать
PHPmemcached (PECL)Официальное расширениеОбёртка над libmemcached, поддержка всех функций. Требует компиляции.✅ Рекомендуется
Memcache (PECL)УстаревшееТолько ASCII, отсутствие CAS, binary, SASL.❌ Не использовать

⚠️ Важно: в PHP расширения memcached и Memcache — разные пакеты. Установка:

pecl install memcached  # ← это правильное
# НЕ pecl install memcache

Типичная интеграция в приложение (на примере C#)

// appsettings.json
{
"Memcached": {
"Servers": [
{ "Address": "10.0.0.11", "Port": 11211 },
{ "Address": "10.0.0.12", "Port": 11211 }
],
"ConnectTimeout": 500,
"ReceiveTimeout": 1000,
"SendTimeout": 1000,
"UseBinaryProtocol": true,
"KeyTransformer": "Enyim.Caching.Memcached.TigerHashKeyTransformer"
}
}

// Startup.cs
services.AddEnyimMemcached(options =>
{
Configuration.GetSection("Memcached").Bind(options);
});

services.AddStackExchangeRedisCache(...); // ← не путать с Redis!

Шаблон «кэш-aside» (cache-aside pattern) — стандартный подход:

public async Task<UserProfile> GetUserProfileAsync(int userId)
{
var cacheKey = $"user:profile:{userId}";
var cached = await _memcachedClient.GetAsync<UserProfile>(cacheKey);

if (cached != null) return cached;

// Промах → идём в БД
var fromDb = await _userRepository.GetByIdAsync(userId);
if (fromDb == null) return null;

// Сохраняем в кэш с TTL = 5 минут
await _memcachedClient.AddAsync(cacheKey, fromDb, TimeSpan.FromMinutes(5));

return fromDb;
}

public async Task UpdateUserProfileAsync(int userId, UserProfile update)
{
var result = await _userRepository.UpdateAsync(userId, update);
if (result)
{
var cacheKey = $"user:profile:{userId}";
// Вариант 1: перезапись
await _memcachedClient.SetAsync(cacheKey, update, TimeSpan.FromMinutes(5));
// Вариант 2: инвалидация (часто предпочтительнее)
// await _memcachedClient.RemoveAsync(cacheKey);
}
}

Почему AddAsync, а не SetAsync, при первом запросе?
→ Минимизирует race condition: если два запроса одновременно получают miss, оба пойдут в БД, но только один успешно выполнит Add. Второй получит false и может использовать значение, уже записанное первым. Это снижает нагрузку на БД при «холодном старте».


Мониторинг Memcached: ключевые метрики и alert-условия

Memcached предоставляет > 100 метрик через stats. Для production-эксплуатации достаточно 15–20.

ГруппаМетрикаОписаниеПорог тревоги
Доступностьcurr_connectionsТекущие соединения> 90 % от -c
connection_structuresВыделено структур под соединенияРост > 10 %/час → утечка
Операцииcmd_get, cmd_setОбщее число операцийТренд ↓ при постоянной нагрузке → проблема клиентов
get_hits, get_missesКэш-попадания/промахиHit ratio = hits / (hits + misses) • < 90 % — требуется анализ • < 70 % — критично
ПамятьbytesТекущий объём данных в байтах> 90 % от -m
limit_maxbytesЗначение -m в байтахКонтрольная точка
evicted, evicted_nonzeroЧисло вытесненных объектовevicted_nonzero > 0 — сигнал к действию
LRUexpired_unfetched, evicted_unfetchedОбъекты, удалённые без single getВысокое значение → короткое TTL или неиспользуемые ключи
Сетьbytes_read, bytes_writtenТрафикАномальный всплеск → DDoS или ошибка в клиенте
Ошибкиlisten_disabled_numСколько раз слушание отключалось из-за нехватки памяти> 0 — критично
accepting_conns1/0 — принимает ли соединения0 → недоступен

Grafana dashboard (рекомендуемые панели):

  1. Hit ratio (5m avg) — линейный график;
  2. Memory usage % — гистограмма по узлам;
  3. Eviction rate (per sec) — гистограмма;
  4. Connections vs limit;
  5. Top 10 slab classes by evicted_nonzero.

Alert-правила (Prometheus + Alertmanager):

- alert: MemcachedLowHitRatio
expr: rate(memcached_get_hits_total[5m]) / (rate(memcached_get_hits_total[5m]) + rate(memcached_get_misses_total[5m])) < 0.85
for: 10m
labels:
severity: warning

- alert: MemcachedEvictionNonZero
expr: rate(memcached_evicted_nonzero_total[5m]) > 0.1 # >1 раз в 10 сек
for: 5m
labels:
severity: warning

- alert: MemcachedOutOfMemory
expr: memcached_listen_disabled_num > 0
for: 1m
labels:
severity: critical

Memcached vs Redis в роли кэша

КритерийMemcachedRedis
Модель данныхТолько key → value (blob)Поддержка 5+ типов: string, hash, list, set, zset и др.
Макс. размер объекта1 МБ512 МБ (по умолчанию; можно увеличить)
КластеризацияНа стороне клиента (consistent hashing)Встроенная (Redis Cluster) — шардирование + репликация
ПерсистентностьНетRDB, AOF, AOF fsync — настраивается
РепликацияНетMaster-slave, Sentinel, Redis Cluster
Высокая доступностьТребует внешнего балансировщика/проксиВстроена (quorum-based failover)
LRU/вытеснениеSlab-based LRU, per-classОбщий LRU/LFU/ volatile-ttl, configurable policy
CAS и конкурентностьПоддержка CAS (64-bit)WATCH/MULTI/EXEC (транзакции), GET+SET атомарно через Lua
Метрики и интроспекцияstats — низкоуровневыеINFO, MEMORY USAGE, CLIENT LIST, Slow Log
Сетевой протоколASCII / BinaryRESP (Redis Serialization Protocol) — binary-like, но человекочитаемый
Типичная latency (локально)0.1–0.3 мс0.2–0.5 мс
Потребление памяти на объектВыше из-за slab-фрагментацииНиже — динамическое выделение
Использование CPUОчень низкое (event-driven)Умеренное (однопоточный, но с фоновыми задачами)

Когда выбирать Memcached:

  • Чистое кэширование (read-heavy, временные данные);
  • Максимальная latency predictability;
  • Простая горизонтальная масштабируемость через клиентский шардинг;
  • Минимальные требования к HA (например, stateless frontend-tier).

Когда выбирать Redis:

  • Требуется персистентность или репликация;
  • Нужны richer data structures (например, лидерборды через ZSET);
  • Сценарии beyond caching: очереди, блокировки, rate-limiting;
  • Требуется встроенная HA без внешних компонентов.

Примечание: в современных архитектурах часто используют оба:

  • Redis — для stateful-компонентов (сессии, очереди, rate-limit);
  • Memcached — для volatile-кэша (HTML-фрагменты, API-результаты), где потеря данных допустима.