Перейти к содержанию

Redis недоступен

Runbook: сервис бросает ошибки на Redis-вызовах, cache_errors_total растёт, endpoint'ы fail-closed отдают 503. Эта страница — диагностика «что именно не так с Redis и чего делать».

Как правильно проектировать сервис на случай такого инцидента — ../how-to/handle-redis-outage.md. Reference по клиенту, TTL, naming — в ../conventions/caching.md.

Содержание

Симптомы

  • cache_errors_total{service=<name>} растёт.
  • В логах сервиса — redis: connection refused, i/o timeout, pool exhausted, WRONGPASS, READONLY You can't write against a read only replica.
  • Endpoint'ы с fail-closed (idempotency-key, reset-password) отдают
  • Endpoint'ы с fail-open работают медленнее (всё летит в БД).
  • /readyz 503 на сервисах, где Redis включён в ready-checks.
  • Dashboard → p99 latency по affected endpoint'ам растёт (если fail- open, запрос уходит в БД).

Быстрая проверка: это действительно Redis?

Признак действительно Redis:

  • Ошибки только на Redis-вызовах (redis: в тексте ошибки).
  • БД/Kafka запросы проходят нормально.
  • redis-cli PING не отвечает / таймаутит.

Если «кэш мимо» + при этом и БД медленная — скорее всего, БД перегружена из-за fail-open cache, см. db-slow-query.md. Тогда корень — Redis, а БД только страдает от отсутствия кэша.

1. Pod не может дозвониться

Симптом. dial tcp: connection refused, i/o timeout на DialTimeout, no such host при DNS-resolve.

Причины.

  • Redis pod упал (k8s CrashLoopBackOff).
  • Service DNS-имя изменилось / Kubernetes Service удалён.
  • NetworkPolicy блокирует трафик pod → redis.
  • Неверный REDIS_ADDR в env (typo в config).

Проверка.

# DNS внутри pod'а сервиса
kubectl exec -it <service-pod> -- sh -c "nslookup $REDIS_HOST"

# TCP connectivity
kubectl exec -it <service-pod> -- sh -c "nc -zv $REDIS_HOST 6379"

# ручной ping из pod'а
kubectl exec -it <service-pod> -- sh -c "redis-cli -h $REDIS_HOST PING"

# состояние redis pod'а
kubectl get pod -l app=redis
kubectl describe pod -l app=redis
kubectl logs -f <redis-pod> --tail=100

Фикс.

  • CrashLoopBackOff → читаем logs, фиксим (обычно memory limit недостаточен).
  • NetworkPolicy → эскалация к инфра-команде.
  • Typo в env → kubectl edit configmap <service>-config, rollout.

Обычно проблема не на стороне backend-сервиса. Мы ждём восстановления и на своей стороне проверяем, что fail-open работает как должен.

2. Таймаут на команде

Симптом. Connection устанавливается, но GET/SET/INCR возвращает i/o timeout через ReadTimeout.

Причины.

  • Medium Redis. CPU утилизирован > 90%, одна команда медленная.
  • Blocking command. Кто-то запустил KEYS * / FLUSHDB / SMEMBERS <huge set> — блокирует single-threaded Redis.
  • Большой payload. GET возвращает 10MB — сетевая передача занимает больше ReadTimeout.
  • Сеть. Packet loss между pod'ом сервиса и Redis.

Проверка.

# INFO stats + cpu
redis-cli INFO stats | grep -E "instantaneous_ops_per_sec|used_cpu"
redis-cli INFO commandstats | head -30

# SLOWLOG — какие команды тормозят
redis-cli SLOWLOG GET 20

# CLIENT LIST — что сейчас выполняется
redis-cli CLIENT LIST | head -20

# размеры ключей (sample)
redis-cli --bigkeys --i 0.01

SLOWLOG — главная диагностика. Покажет long-running команды с временем и клиентом.

Фикс.

  • Blocking command. Найти клиента (CLIENT LIST), kill'нуть через CLIENT KILL ID <id>. Фикс в коде — не использовать KEYS, заменить на SCAN.
  • Большой payload. Не храним объекты > 100KB в одном ключе. Разбивка на chunks или архитектурное решение (не в Redis).
  • CPU 100%. Либо слишком много RPS на один instance (scale — см. Redis cluster), либо сложные команды (LUA scripts).

3. Pool исчерпан

Симптом. redis: connection pool timeout или ERR pool exhausted. Метрика redis_pool_idle_connections = 0, redis_pool_active_connections = PoolSize.

Причины.

  • Handler задерживает команды (слишком медленный downstream в цепочке).
  • PoolSize слишком мал для текущей нагрузки.
  • Goroutine leak держит соединения (см. memory-leak.md).
  • Долгий BLPOP / subscribe занимает соединение навсегда.

Проверка.

// в сервисе можно экспортировать pool stats
stats := rdb.PoolStats()
log.Info("redis pool", "hits", stats.Hits, "misses", stats.Misses,
    "timeouts", stats.Timeouts, "total", stats.TotalConns, "idle", stats.IdleConns)
# на Redis — сколько соединений открыто
redis-cli INFO clients

Фикс.

  • Увеличить PoolSize в UniversalOptions (× 2 как экспериментальный шаг). Default в go-redis — 10 × GOMAXPROCS, что на 16-core pod'е даёт 160. Если этого мало — проверь, не утекают ли соединения.
  • Leak: искать в goroutine-profile.
  • BLPOP и подобные — изолировать в отдельный pool (go-redis создаёт отдельное соединение на Subscribe, но для BLPOP нужно явно).

4. Memory full / eviction storm

Симптом. OOM command not allowed when used memory > 'maxmemory', высокий evicted_keys rate, записи пропадают сразу после SET.

Причины.

  • Redis упёрся в maxmemory, maxmemory-policy не успевает evict'ить.
  • Огромный ключ занимает гигабайт.
  • Утечка в application: ключи без TTL, бесконечные коллекции.

Проверка.

redis-cli INFO memory | grep -E "used_memory_human|maxmemory_human|maxmemory_policy"
redis-cli INFO stats | grep evicted_keys

# топ-N по размеру
redis-cli --bigkeys
redis-cli MEMORY USAGE <key>

# список ключей без TTL — через SCAN, не KEYS
redis-cli --scan | while read k; do
    [ "$(redis-cli TTL "$k")" = "-1" ] && echo "$k"
done | head -20

Фикс.

  • Найти источник. Bigkey (фото в base64 в cache?), утечка (ключи с TTL=-1)? См. caching.md — бесконечный TTL запрещён.
  • Изменить maxmemory-policy. Для cache — allkeys-lru. Если сейчас noeviction — Redis отказывает запросам при переполнении.
  • Scale вертикально (больше RAM) или горизонтально (Redis cluster). Инфра-задача.
  • Сократить TTL в сервисе временно, чтобы снизить нагрузку.

5. Cluster in-flight (для cluster-mode)

Симптом. MOVED 1234 redis-cluster-2:6379 в ответах, слоты переезжают между nodes.

Причина. Cluster resharding — штатная операция, в прогрессе. Клиент go-redis UniversalClient должен автоматически обрабатывать MOVED/ASK через AutoRepair / ReadFromReplicas.

Фикс.

  • В нормальном режиме — ничего, клиент сам догоняет.
  • Если клиент не догоняет (старая версия библиотеки) — рестарт pod'а.
  • Отключить частый Subscribe на время resharding'а (subscribers наиболее чувствительны к node-переключениям).

6. WRONGPASS / NOAUTH

Симптом. WRONGPASS invalid username-password pair или NOAUTH Authentication required.

Причины.

  • Неверный пароль в config (ротация ключа была, env не обновлён).
  • ACL правила изменились (user больше не существует / нет прав).
  • requirepass на Redis изменён, old-клиенты упали.

Проверка.

# проверить пароль в k8s secret
kubectl get secret <service>-redis-secret -o jsonpath='{.data.REDIS_PASSWORD}' | base64 -d

# ACL на redis
redis-cli ACL LIST

Фикс.

  • Обновить REDIS_PASSWORD в Secret, kubectl rollout restart deployment/<service>.
  • Координация с Redis-owner'ом при ротации — см. ../how-to/rotate-jwt-key.md (шаблон для key rotation применим и к Redis).

7. READONLY (replica)

Симптом. READONLY You can't write against a read only replica.

Причина. Клиент обращается с WRITE-командой к replica'е, а не к primary. Это происходит при:

  • Failover — primary упал, replica ещё не promote'нут.
  • ReadFromReplicas: true в клиенте применяется и к writes (конфигурационная ошибка).
  • DNS указывает на replica.

Фикс.

  • Дождаться failover (обычно < 30s с Sentinel / Redis Cluster).
  • Проверить client config: MasterName (для Sentinel), addr list (для Cluster). Writes всегда должны идти на primary.

Команды диагностики

# общий health
redis-cli PING                  # PONG = жив
redis-cli INFO server           # version, uptime, mode
redis-cli INFO replication      # master/slave, connected_slaves

# нагрузка
redis-cli INFO stats
redis-cli INFO clients          # сколько клиентов
redis-cli INFO commandstats     # топ команд по calls/usec

# память
redis-cli INFO memory
redis-cli MEMORY STATS
redis-cli --bigkeys --i 0.01    # топ-10 big-ключей, с rate-limit

# slow log
redis-cli CONFIG GET slowlog-log-slower-than   # default: 10000 µs
redis-cli SLOWLOG GET 20                       # последние 20
redis-cli SLOWLOG RESET                        # сброс

# keyspace
redis-cli DBSIZE
redis-cli --scan --pattern "review:*" | wc -l  # сколько review-ключей

# ACL
redis-cli ACL WHOAMI
redis-cli ACL LIST

# клиенты (убить зависший)
redis-cli CLIENT LIST
redis-cli CLIENT KILL ID <id>

# в pod'е сервиса
kubectl exec -it <service-pod> -- sh -c "redis-cli -h $REDIS_HOST PING"
kubectl logs <service-pod> | grep -i redis | tail -50

Порядок действий

  1. Подтверди, что это Redis. Ошибки содержат redis:, БД/Kafka работают.
  2. PING. Пинг отвечает? Нет → §1 (networking / pod).
  3. INFO memory. OOM? → §4.
  4. SLOWLOG GET 20. Есть долгие команды? → §2.
  5. CLIENT LIST. Много connected, мало idle? → §3 (pool).
  6. INFO replication. На primary ли мы? → §7.
  7. Если ничего — логи Redis pod'а: kubectl logs <redis-pod>. Обычно там подсказка.
  8. Пока разбираемся — отключи fail-closed endpoint'ы. Если affected endpoint'ы критичны, временно переключи фичу на degraded mode (через feature-flag или config), чтобы запросы не давали 503. См. ../how-to/handle-redis-outage.md.

Что не делать

  • FLUSHDB / FLUSHALL в prod — уничтожает весь кэш, создаёт stampede на БД.
  • KEYS * — блокирует Redis на секунды/минуты. Используй SCAN.
  • DEBUG SEGFAULT — роняет Redis намеренно. Не смешно.
  • Рестарт сервис-pod'ов «чтобы пересоединились» — без диагностики это ничего не даст, только размножит connection spike.
  • CONFIG SET maxmemory 0 в прод — неограниченная память ест весь node, приведёт к OOM kill всего Redis.
  • Перезапуск Redis pod'а без координации. В cluster-mode это триггерит failover. Уведомить инфра-команду.

Чеклист

  • Подтвердил: ошибки связаны с Redis (не БД, не Kafka).
  • PING из pod'а сервиса отвечает / не отвечает — записал.
  • INFO memory проверен — нет OOM.
  • SLOWLOG GET 20 — нет долгих команд / есть и виновник найден.
  • CLIENT LIST — pool не забит в нормальном режиме.
  • Application логи подтверждают ту же картину (timeout / refused / pool exhausted).
  • Fail-open endpoint'ы работают через БД (пусть и медленнее).
  • Fail-closed endpoint'ы отдают 503, не 500.
  • Incident log с root cause написан.
  • Post-incident: усилить monitoring на тот класс проблем, что случился.

Связанные разделы