Runbook: Event store / outbox projection lag

Severity (default): P1 — пишущий путь работает, но проекции (ES/CH/in-PG read models) отстают. Owner: core team / on-call. Связанные алерты:

  • outbox_unpublished_count > 10000 за 5 мин.
  • outbox_lag_seconds p95 > 5 за 5 мин.
  • outbox_publish_attempts_total{outcome="failed", attempts>=5} > 0.
  • ES alias не свежее последнего event_store записи.

Симптом

  • Search / админка показывают устаревшие данные.
  • event_store растёт, но в ES/CH ничего не приходит.
  • outbox накапливает unpublished строки.

Диагностика

-- 1. Размер unpublished outbox
SELECT count(*) FROM outbox WHERE published_at IS NULL;
 
-- 2. Самая старая unpublished
SELECT id, topic, enqueued_at, attempts, now() - enqueued_at AS age
FROM outbox WHERE published_at IS NULL ORDER BY enqueued_at LIMIT 10;
 
-- 3. Топики с проблемой
SELECT topic, count(*), min(enqueued_at), max(attempts)
FROM outbox WHERE published_at IS NULL GROUP BY topic ORDER BY count(*) DESC;
 
-- 4. Застрявшие (attempts >= 5)
SELECT id, topic, attempts, enqueued_at, headers->>'last_error' AS last_error
FROM outbox WHERE attempts >= 5 AND published_at IS NULL LIMIT 20;
# 5. Лидер outbox relay живой?
kubectl -n tracium exec deploy/redis -- \
  redis-cli GET "tracium:outbox-relay:leader"
 
# 6. Логи outbox-relay
kubectl -n tracium logs deploy/outbox-relay --since=15m | grep -E "ERROR|WARN"

Возможные причины:

  • A: Outbox relay не работает (нет лидера / упал).
  • B: Kafka недоступен / запись отвергается.
  • C: Один топик со сломанной схемой → все его события копят attempts.
  • D: ES indexer (downstream consumer) умер → каскадный lag.

Смягчение

A: Перезапуск relay

kubectl -n tracium rollout restart deploy/outbox-relay
# Проверить, что лидерство восстановилось
sleep 15 && kubectl -n tracium exec deploy/redis -- redis-cli GET "tracium:outbox-relay:leader"

B: Kafka недоступна

См. отдельный runbook kafka-cluster-down.md (TODO). Outbox продолжит копить — данные не теряются. После восстановления Kafka — relay сам догонит.

C: Застрявший топик

-- Найти broken записи
SELECT id, topic, payload->>'event_type' AS et, attempts
FROM outbox WHERE attempts >= 10 AND published_at IS NULL;

Опции:

  1. Если payload broken (после code rollback вышла невалидная схема) — UPDATE outbox SET payload = ... WHERE id = ... (с явным аудитом, только через runbook).
  2. Если broker отвергает (например, сообщение слишком большое) — увеличить max.message.bytes или сжать.
  3. Окончательный skip — UPDATE outbox SET published_at = now(), headers = headers || '{"manual_skip":true,"by":"<operator>","reason":"<>"}'::jsonb WHERE id = .... Только если consumer не пострадает.

D: Downstream consumer умер

См. kafka-consumer-lag.md.

Устранение root cause

  • A: HA outbox-relay (несколько реплик; лидерство стабильнее).
  • B: Kafka SLO + chaos test.
  • C: валидация схем в outbox writer (проверка до публикации).
  • D: лучшие алерты на здоровье consumer’ов (RED + auto-restart).

Эскалация

  • 15 мин без mitigation → #oncall-data.
  • При риске потери данных (outbox рядом с лимитом размера таблицы) → #oncall-prod + DBA.

Связано

  • ADR-0020 (Outbox pattern).
  • 20-architecture/event-sourcing.md §“Outbox publication SLA”.
  • kafka-consumer-lag.md