Модуль 5. Инциденты: big keys, eviction, OOM, лаг реплики — симптом, причина, лечение
Цель модуля: научиться вести инцидент Redis-очереди от симптома к причине и лечению — по метрикам, а не по интуиции. Шесть реальных инцидентов, каждый воспроизводится на стенде: вы увидите график раньше, чем увидите его в проде.
Перед началом: Модули 1–4 (особенно карта метрик и стенд obs). Формат: каждый инцидент — симптом → метрики → причина → лечение → воспроизведение. В конце — инцидент-квиз «найди причину по графику». Трудоёмкость: ~10–12 ч (разборы 5 ч · воспроизведения 5 ч · квиз 1 ч).
📖 5.1. Методика: три вопроса дежурного
Порядок разбора — всегда одинаковый, и он экономит часы:
- Очередь или сервер? Панели 1–3 дашборда (throughput, age, pending). Если age растёт при живом throughput — проблема в обработке; если throughput упал — проблема в записи или в Redis.
- Что изменилось? Деплой воркера, рост трафика, новая фича
(кеш-инвалидация с
KEYS?), рестарт узла. Инциденты не случаются в вакууме — 80 % имеют «а что поменяли перед этим». - Одна метрика ведёт, остальные подтверждают. «Evicted растёт» — ведёт; «latency вырос» — подтверждает. Не разбирайте инцидент одновременным взглядом на 40 графиков.
📖 5.2. Инцидент «Big keys»: одна запись на 4 МБ
Симптом: XADD внезапно стал отвечать 30–50 мс вместо 0.2 мс; дашборд
показывает всплески latency, throughput очереди просел вдвое. Память росла
плавно и вдруг подскочила.
Метрики: redis_commands_duration_seconds по XADD вверх;
mem_fragmentation_ratio пополз к 1.5; used_memory ступенькой.
Причина: в Values записи попал payload целиком — PDF-квитанции по 800 КБ, а раз в минуту — выгрузка на 4 МБ. Redis однопоточный: пока он копирует мегабайты (в буферы репликации, в AOF, в ответ XREAD), все остальные команды ждут.
Диагностика:
bashredis-cli --bigkeys # крупнейшие ключи по типам redis-cli MEMORY USAGE orders # размер стрима redis-cli XRANGE orders - + COUNT 100 | wc -c # средний размер записи redis-cli SLOWLOG GET 10 # увидите XADD на мегабайтах
Лечение:
- payload — в объектное хранилище (S3/Object Storage), в стрим — ссылка и метаданные. Классический лимит: тело записи ≤ 10–100 КБ;
- maxLen/обрезка старых — лечит объём, но не размер одной записи;
- воспроизведение: скрипт пишет в стрим записи по 2 МБ раз в секунду —
и рядом heartbeat-цикл с
PING; смотрите, как ping-латентность (а это латентность всего Redis) страдает вместе с XADD. Это и есть урок однопоточности.
📖 5.3. Инцидент «Горячий стрим»: один узел на всех
Симптом: orders обрабатывается нормально, но каждую пятницу
(распродажа) age растёт до минут. CPU Redis в это время — 25 %, казалось
бы, есть запас. Воркеров добавили — не помогло.
Метрики: stream_age растёт по группе orders, pending под
потолок Count×воркеры, а общий redis_cpu невысок, evictions нет.
Причина: «Reads-blocked» очередью — клиенты последовательно приходят на один и тот же узел; но главное — сам трафик направлен в один стрим, у которого один slot. В Redis Streams запись и чтение — всё равно через единственный поток узла, а consumer group добавляет накладных расходов на PEL: каждое сообщение — учёт по консьюмеру.
Лечение:
- шардирование стримов по бизнес-ключу:
{orders:0..15}с выбором поhash(order_id) % 16— 16 независимых стримов и групп (в кластере ещё и по узлам); - малый
Limit/Countна PEL, чтобы учёт не рос; - если шардирование превратилось в 64 стрима и очередь на 50k+ сообщений/с — это сигнал: Redis добрался до потолка по назначению (§6). «Пятничные» нагрузки — не повод расширять Redis, это повод переехать на Kafka.
📖 5.4. Инцидент «Eviction под нагрузкой»: тихие потери в очереди
Симптом: очередь «работает», но раз в день «пропадают» пачки сообщений: продюсер получил ID, а в стриме — дырка. Ни ошибок, ни warning'ов. Дашборд молчит.
Метрики: redis_evicted_keys_total нет — стоп, вот грабли: при
allkeys-lru на кеше с очередью эвикция не пишет ошибок: метрика
растёт, а приложение ничего не замечает. Панели Evictions на дашборде
были нулевые, потому что их просто не смотрели.
Причина: maxmemory_policy allkeys-lru: Redis спасал себя от OOM,
выдавливая в том числе записи стрима — это следствие решения «используем
один инстанс и для кеша, и для очереди».
Лечение:
- для очереди —
noeviction+ алерт на used/max > 70 %: лучше ошибкаOOM command not allowed when used memory > maxmemory, чем тихая потеря; - разделить: кеш и очередь — разные инстансы (или базы — нет, это один и тот же maxmemory, разделение по базам не спасёт!);
- воспроизведение:
maxmemory 64mb,allkeys-lru, продюсер пишет стрим; параллельно скрипт набивает кеш — и через минуту считайте дырки в стриме по ID. Боль лучше таблицы.
📖 5.5. Инцидент «OOM: записи отвергаются»
Симптом: продюсер ловит OOM command not allowed when used memory > maxmemory; расписание очереди встало в тупик (записи от воркеров падают,
потом и вовсе обработка не стартует). Осторожность: после §5.4 мы сами
сделали noeviction — это правильный выбор, но нужно понимать его цену.
Метрики: used_memory стоит на maxmemory; rejected_calls растёт;
stream_length перестал расти: записи отбиваются.
Причина (три канонические):
- Стрим без
MaxLen— история за месяц лежит в памяти; - TTL-записи (идемпотентные метки) без TTL — забыли третьим аргументом;
- Память кончилась на PEL: тысячи не подтверждённых сообщений в руках «зависшего» воркера.
Лечение и профилактика:
XADD ... MAXLEN ~ 100000(аппроксимация) — контракт retention, а не настройка;- метки дедупа — только с TTL (§3.2), лага обработки — только с лимитами;
- алерт used/max > 70 % за минуты до OOM (это и есть разница между инцидентом и плановым действием);
- аварийный протокол:
redis-cli --bigkeys→ увеличить maxmemory (если железо позволяет) → включить/ужать MaxLen → разобрать PEL (XPENDING,XCLAIMспасателем).
📖 5.6. Инцидент «Реплика читает старьё»
Симптом: «мы обработали дважды»: два воркера, подключённые через
Sentinel, поймали одну и ту же пачку. Оба в логах — оба живые, оба
получили XREADGROUP одинаковые ID.
Метрики: master_link_status:up, lag реплики 0.2–1 с; в дашборде
репликация живая; ошибок нет.
Причина: инстанс воркера A переподключился после сетевой икоты
и начал читать XREADGROUP с реплики (ReadOnly-клиент, попытка
сэкономить мастер): но PEL — на мастере. Реплика отдаёт сообщения
по своему (отстающему) состоянию групп, а XACK уходит на мастер —
получаем параллельную выдачу и двойную обработку.
Правило-лечение: для очереди на Redis всегда читайте и
подтверждайте только с мастера. Реплика — для read-only кеша и
аналитики, не для консьюмеров. В go-redis — не подсовывайте
ReadOnly: true в sentinel-клиент воркера. И WAIT-осознанность из §2.1:
если хочется синхронности — договаривайтесь ею, а не чтением реплик.
📖 5.7. Инцидент «Blocked clients: очередь замерла целиком»
Симптом: все операции встали одномоментно на 5 секунд. Всплеск
latency на всем дашборде; redis_blocked_clients вырос с 8 до 40.
Потом само прошло.
Метрики: blocked_clients вверх; SLOWLOG — в конце списка что-то
«безобидное», например, Lua-скрипт или SINTERSTORE по большим множествам.
Причина: однопоточность + длинная команда = остановка конвейера.
Все XREAD BLOCK «заблокированы» не от того, что блокирующая команда
плоха, а от того, что ждут единственный поток. Самые частые виновники
в очереди на Redis: тяжёлый Lua (дедуп-скрипт по большому диапазону),
KEYS в отладке, BGREWRITEAOF в момент пиковой нагрузки (вилка ОС,
но деградация реальная).
Лечение:
- Lua — только O(1) по объёму (сравнение метки, один XADD); диапазоны — не в Lua;
BGREWRITEAOF/BGSAVE— планировать вне пиков (или следить заaof_rewrite_in_progressна дашборде);CLIENT LISTиCLIENT KILL— для обезвреживания клиента-виновника;- воспроизведение: запустите
redis-cli --evalс Lua на 10⁶-элементном множестве под нагрузкой продюсера и замерьте heartbeat-латентность.
💻 Практика
Воспроизведите все шесть инцидентов на стенде (make up-obs) и
заполните таблицу «симптом / ведущая метрика / причина / лечение» —
своими словами, по-русски, с командами воспроизведения.
Для каждого инцидента — короткий отчёт: 5–8 строк — сколько заняла
диагностика и каким запросом в Prometheus вы «увидели» инцидент
(например, rate(redis_evicted_keys_total[5m])).
Норматив: шесть инцидентов — шесть отчётов; нельзя «угадать» причину, не воспроизведя.
🧩 Quiz "Инцидент-квиз: найди причину по графику"
✅ Итоги модуля
| Инцидент | Ведущая метрика | Ядро причины |
|---|---|---|
| Big keys | latency команд, fragmentation | payload в записи — Redis однопоточный |
| Горячий стрим | stream_age при невысоком CPU | один стрим = один слот/поток |
| Eviction | evicted_keys (тихие!) | очередь и кеш на одном инстансе с LRU |
| OOM | used/max, rejected_calls | стрим без MaxLen, метки без TTL, PEL |
| Реплика читает | XINFO/lаг + дубли в логах | чтение консьюмером с реплики — вне контракта |
| Blocked clients | blocked_clients + SLOWLOG | длинная команда стопорит всё |
🔗 Мостик к Модулю 6
Шесть инцидентов — это шесть границ Redis-как-брокера, увиденных болью. Остался последний шаг курса: собрать эти границы в проектное решение — таблицу «Streams vs Kafka vs RabbitMQ», чек-лист «наша очередь на Redis — осознанно или временно» и план миграции, если очередь переросла Redis. Итоговый модуль — для ревью перед продакшеном, а не для чтения ради.