Модуль 5. Инциденты: big keys, eviction, OOM, лаг реплики — симптом, причина, лечение

Цель модуля: научиться вести инцидент Redis-очереди от симптома к причине и лечению — по метрикам, а не по интуиции. Шесть реальных инцидентов, каждый воспроизводится на стенде: вы увидите график раньше, чем увидите его в проде.

Перед началом: Модули 1–4 (особенно карта метрик и стенд obs). Формат: каждый инцидент — симптом → метрики → причина → лечение → воспроизведение. В конце — инцидент-квиз «найди причину по графику». Трудоёмкость: ~10–12 ч (разборы 5 ч · воспроизведения 5 ч · квиз 1 ч).


📖 5.1. Методика: три вопроса дежурного

Порядок разбора — всегда одинаковый, и он экономит часы:

  1. Очередь или сервер? Панели 1–3 дашборда (throughput, age, pending). Если age растёт при живом throughput — проблема в обработке; если throughput упал — проблема в записи или в Redis.
  2. Что изменилось? Деплой воркера, рост трафика, новая фича (кеш-инвалидация с KEYS?), рестарт узла. Инциденты не случаются в вакууме — 80 % имеют «а что поменяли перед этим».
  3. Одна метрика ведёт, остальные подтверждают. «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), все остальные команды ждут.

Диагностика:

bash
redis-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 перестал расти: записи отбиваются.

Причина (три канонические):

  1. Стрим без MaxLen — история за месяц лежит в памяти;
  2. TTL-записи (идемпотентные метки) без TTL — забыли третьим аргументом;
  3. Память кончилась на 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 "Инцидент-квиз: найди причину по графику"

1. Stream age растёт, throughput XADD стабилен, XACK-и просели вдвое. Что смотреть первым?
2. used_memory на maxmemory, stream_length замер, продюсер ловит ошибки «OOM command not allowed». Первое лечебное действие?
3. Всплеск latency у всех операций разом, blocked_clients вырос. Наиболее вероятный пострадавший/виновник?
4. Пачки сообщений в стриме «с пропусками», XADD возвращал ID, ошибок нет. Наиболее вероятная причина?

✅ Итоги модуля

ИнцидентВедущая метрикаЯдро причины
Big keyslatency команд, fragmentationpayload в записи — Redis однопоточный
Горячий стримstream_age при невысоком CPUодин стрим = один слот/поток
Evictionevicted_keys (тихие!)очередь и кеш на одном инстансе с LRU
OOMused/max, rejected_callsстрим без MaxLen, метки без TTL, PEL
Реплика читаетXINFO/lаг + дубли в логахчтение консьюмером с реплики — вне контракта
Blocked clientsblocked_clients + SLOWLOGдлинная команда стопорит всё

🔗 Мостик к Модулю 6

Шесть инцидентов — это шесть границ Redis-как-брокера, увиденных болью. Остался последний шаг курса: собрать эти границы в проектное решение — таблицу «Streams vs Kafka vs RabbitMQ», чек-лист «наша очередь на Redis — осознанно или временно» и план миграции, если очередь переросла Redis. Итоговый модуль — для ревью перед продакшеном, а не для чтения ради.