Кеширование и Memcached

Почему TTL = 0 или 2592000 секунд — это почти всегда ошибка
Самая распространённая ошибка новичков — установка максимального времени жизни ключа (TTL) "на всякий случай" или, наоборот, TTL = 0 для данных, которые редко меняются. В Memcached при TTL = 30 дней (2 592 000 секунд) вы гарантированно получите «залипание» устаревших данных: если сервер БД упал и восстановился с новыми записями, кэш ещё месяц будет отдавать старые битые данные. С другой стороны, TTL = 0 (бесконечное хранение) — прямой путь к переполнению памяти и вытеснению горячих ключей самыми старыми. Профессионалы вычисляют TTL не «на глаз», а исходя из SLA сервиса: для каталога товаров — 5–15 минут, для юзер-сессий — 15–30 минут, для агрегатов (счётчики, статистика) — 300–600 секунд. Конкретное правило: TTL должен быть как минимум в 2–3 раза меньше, чем средний интервал между изменениями данных в базе.
- TLT = 0 — запрещено в production. Используйте только для временных блокировок (mutex) с программным удалением.
- Максимальный TTL в мс — при указании 0 мс Memcached может проигнорировать ключ. Ставьте 1 мс для атомарных счётчиков.
- Не используйте float — Memcached округляет до целых секунд. float-значение может стать 0.
Неочевидные последствия флагов сжатия: когда экономия трафика убивает производительность
Многие считают, что сжатие данных (флаг COMPRESSED) всегда полезно. На практике при размере объекта до 2 КБ сжатие только тратит CPU: Memcached вынужден упаковывать/распаковывать каждый ключ, а выигрыш в трафике составляет 3–7%. Оптимальный порог включения сжатия — 8–12 КБ для текстовых данных (HTML, JSON) и 1–2 КБ для бинарных (изображения, сериализованные объекты). Ещё один подводный камень: при использовании библиотек-клиентов (например, php-memcached) по умолчанию включено сжатие для всех ключей. Если вы храните в кэше сериализованные объекты и они меньше 1 КБ, отключайте сжатие явно командой $memcached->setOption(Memcached::OPT_COMPRESSION, false). В противном случае каждый GET будет тратить 0.001–0.005 мс на декомпрессию, что при 10 000 RPS даёт 10–50 мс дополнительной задержки.
- Порог включения сжатия — проверяйте через A/B тест: отключите сжатие для объектов < 4 КБ, замерьте CPU сервера.
- SASL-аутентификация — если включена, сжатие может работать некорректно (баг в некоторых версиях libmemcached). Лучше отключить.
- Игнорируйте флаг COMPRESSED в клиентах на Go/Java — они часто его не поддерживают. Используйте простое кодирование base64.
Инвалидация кэша: три рабочих сценария вместо «очистить всё»
Главный вопрос после «зачем кэш» — «как его сбросить?». Типовой антипаттерн: при любом изменении в БД вызывать flush_all. Это приводит к мгновенному «замерзанию» нагрузки: все конкурирующие запросы одновременно идут в базу, вызывая её падение или деградацию. Профессиональный подход — точечная инвалидация по трём схемам: 1) инвалидация по ключу (удаляем конкретный id), 2) по тегу (храним тег как отдельный ключ, проверяем при GET), 3) по версии данных (версионная метка в самом значении). Лучший вариант для веб-приложений — комбинация тегов и TTL: тег обновляется при изменении любой сущности, а TTL страхует от потери тега при сбое. Пример: ключ «user_123_orders» и ключ-тег «orders_version:123». При обновлении заказа — меняем значение тега (инкремент). Все считывающие процессы увидят несовпадение и перезапросят данные.
- Lock-файлы — если база реплицируется, используйте отдельный Redis для хранения тегов (Memcached не умеет блокировать чтение).
- Cache stampede — защита: при инвалидации не удаляйте ключ, а заменяйте его на «вычислить заново» с новым TTL=1 сек для mutex.
- Размер тега — не храните тег как массив объектов. Только одно 32-битное число или UUID. Иначе перегрузка I/O.
Кластеризация Memcached: как не потерять данные при добавлении ноды
Memcached не поддерживает репликацию и шардинг «из коробки». Консистентное хеширование (ketama) решает проблему перераспределения ключей при добавлении нод, но не защищает от выпадения одной ноды — тогда ~1/N часть данных становится недоступна. Практический совет: всегда используйте минимум 3 ноды в кластере и настраивайте коэффициент репликации на уровне приложения (пишите одни и те же данные в два разных Memcached). Например, клиент отправляет SET на ноду A и ноду B. При выпадении A — данные читаются из B. Минус: удвоение использования памяти и трафика. Более тонкий момент: консистентное хеширование работает корректно только при одинаковом алгоритме на всех клиентах. Если у вас смесь PHP и Python — проверьте, что оба используют libketama с одинаковым хешем (MD5). Иначе ключ user_123 попадёт на разные ноды с разных языков.
- Twemproxy — прокси, который делает шардинг прозрачным. Но не поддерживает мульти-гет (mget) через несколько нод.
- Mcrouter — Facebook-тул для репликации и шардинга. Умеет делать failover за 5 мс.
- Используйте consistent hashing только с флагом
libketamaво всех клиентах. Другие алгоритмы (mod) — потеря данных при решардинге.
Профилирование и мониторинг: 4 метрики, которые большинство игнорирует
Стандартный мониторинг (hit ratio, get/set RPS) недостаточен. Профессионалы следят за: 1) evictions@rate — вытеснение ключей из-за нехватки памяти. Если evictions > 0, значит, кэш переполнен и часть горячих данных вытеснена. Нужно увеличивать память или уменьшать TTL. 2) connection structures — максимальное число открытых соединений по умолчанию 1024. Если превышено — новые клиенты получают timeout или отказ. Нагрузочное тестирование покажет реальный пик. 3) slab rebalancing — если вы используете slab_automove, смотрим статистику перемещения страниц между slab-классами. Частое перемещение означает, что размеры ключей сильно варьируются (например, 1 байт и 1 МБ). Исправить — хранить большие объекты в файловом хранилище, а в Memcached — только ссылки. 4) waiting threads — количество потоков, ожидающих освобождения памяти. Если > 0 — кэш буквально «зависает» при записи.
Для мониторинга используйте команду stats через telnet или библиотеки (например, memcached-tool). В production обязательно собирайте лог команд get с задержкой > 1 мс — это выявит медленные ноды или неправильную конфигурацию сети. Ещё один лайфхак: если hit ratio выше 90%, но TTL очень большой — вы рискуете получить cache poisoning (отравление кэша старыми данными). Лучше снизить TTL до 15–30 минут, даже если hit ratio упадёт до 80% — это безопаснее для достоверности данных.
- Автоматическое расширение памяти — не включайте slab_automove на продакшене без предварительной настройки. Лучше задать фиксированное количество slab-классов под типовые размеры ключей.
- UDP-протокол — ускоряет запись в 2–3 раза, но теряет пакеты. Используйте только для не критичных к потере данных.
- Количество потоков (threads) — не ставьте больше числа ядер CPU. При 4 ядрах — 2–3 потока. Иначе падает производительность из-за конкуренции за мьютексы.
Резюме: что отличает профессионала в работе с Memcached
Главное отличие — осознанный выбор, а не слепое следование настройкам по умолчанию. Профессионал никогда не использует flush_all в production, не ставит TTL «от балды» и не включает сжатие без замеров. Он знает, что Memcached — это линейно масштабируемый кэш ключей, а не база данных, поэтому не пытается хранить в нём отношения или сложные структуры. Вместо этого он моделирует схему инвалидации на основе: 1) частоты обновления данных, 2) критичности задержки (latency), 3) доступности памяти. Если вы научитесь управлять этими тремя параметрами — 90% проблем с кэшированием исчезнут. Оставшиеся 10% — это косяки в клиентских библиотеках и сетевые задержки, которые легко отлавливаются профилированием.
Помните: самое дорогое в кэшировании — не аппаратура, а потеря достоверности данных. Лучше отдавать слегка устаревшие данные, чем непредсказуемые. Настройте TTL с запасом, тегируйте ключи и не забывайте про мониторинг evictions. Только так Memcached станет вашим союзником, а не источником головной боли.
Добавлено: 23.04.2026
