Files
mdm-ordinis/docs/ops/outbox-dlq.md
T
Zimin A.N. adc40aca22 docs(ops): operator runbook — kafka, outbox DLQ, db migrations + index
Карточки процедур для on-call. Self-contained: можно открыть в момент
инцидента без необходимости понимать всю архитектуру.

- README.md — index by symptom + быстрые гайды по 401, 500, admin-ui 502.
- kafka-restart.md — pause/unpause, replicas=0 fix, force-delete, restart
  consumer'ов после Kafka up.
- outbox-dlq.md — диагностика DLQ через UI/API/SQL, типичные причины
  (orphan topic, ACL, broken payload), replay через UPDATE dlq_at=NULL.
- db-migrations.md — Liquibase changelog, добавление, откат, recovery
  failed migration через DELETE databasechangelog.

Vault unseal был отдельным коммитом ранее.
2026-05-05 17:26:30 +03:00

111 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Runbook: Outbox DLQ — диагностика и replay
**Когда использовать:**
- Метрика `ordinis_outbox_dlq_size > 0` (Prometheus alert).
- В UI `/outbox` → ненулевое количество в "В DLQ".
- В логе writer: `Outbox event id=N → DLQ after 1000 attempts`.
## 1. Что это
Outbox publisher делает at-most ~1000 попыток (`ordinis.outbox.attempt-max`)
опубликовать каждое событие в Kafka. После — событие маркируется `dlq_at`
и исключается из polling. Без cap poller бесконечно ретраит мёртвые сообщения
(orphan topic, broken ACL, malformed payload), забивая лог и lag-метрику.
## 2. Просмотр DLQ
### Через admin UI
- `https://ordinis.k8s.265.nstart.cloud/outbox` → таблица со всеми DLQ
записями: ID, eventType, topic, attemptCount, lastError, dlqAt.
### Через API
```bash
curl -sk -H "Authorization: Bearer $TOKEN" \
--resolve ordinis.k8s.265.nstart.cloud:443:192.168.100.161 \
"https://ordinis.k8s.265.nstart.cloud/api/v1/admin/outbox/dlq?page=0&size=50" \
| jq '.content[] | {id, eventType, kafkaTopic, attemptCount, lastError}'
```
### Напрямую в БД
```sql
-- через kubectl exec на postgres pod
SELECT id, event_type, kafka_topic, dictionary_name, attempt_count,
LEFT(last_error, 200) AS error,
dlq_at, created_at
FROM outbox_events
WHERE dlq_at IS NOT NULL
ORDER BY dlq_at DESC
LIMIT 50;
```
## 3. Типичные причины
| `last_error` содержит | Причина | Решение |
|----------------------------------------------------|------------------------------------------------------|--------------------------------------|
| `Topic <name> not present` / `UnknownTopicOrPartition` | Топик удалён / переименован / не создан Strimzi'ем | Создать topic либо удалить запись (`DELETE FROM outbox_events WHERE id=N`) |
| `TopicAuthorizationException` / `AclAuthorization` | SASL user не имеет PRODUCE rights | Strimzi `KafkaUser` ACL — добавить permission |
| `TimeoutException` | Kafka broker недоступен | См. [kafka-restart.md](kafka-restart.md). После up — replay (см. §4) |
| `RecordTooLargeException` | Payload > `message.max.bytes` | Увеличить limit в Kafka либо сократить payload |
| `JsonMappingException` / `JsonProcessingException` | Payload поломан — нечитаемая JSON | DELETE запись (нечего публиковать) + investigate writer code |
| `SerializationException` | Kafka schema registry не разрешает (если включён) | См. schema registry config |
## 4. Replay — пометить как unpublished снова
После исправления причины (топик создан, ACL дан) — снять `dlq_at`, обнулить
attempt_count, poller подхватит.
```sql
-- одну запись
UPDATE outbox_events
SET dlq_at = NULL, attempt_count = 0, last_error = NULL
WHERE id = 12345;
-- весь DLQ (после fix корневой причины)
UPDATE outbox_events
SET dlq_at = NULL, attempt_count = 0, last_error = NULL
WHERE dlq_at IS NOT NULL;
```
Через ~1 sec OutboxPoller подхватит и попытается опубликовать заново.
## 5. Hard delete (когда replay не нужен)
Если событие безнадёжно (стейл данных, удалённая запись, тестовый мусор):
```sql
DELETE FROM outbox_events WHERE id = 12345;
DELETE FROM outbox_events WHERE dlq_at IS NOT NULL AND dictionary_name = 'old_test_dict';
```
⚠️ **Audit:** если событие было `RecordCreated` / `RecordUpdated` — consumer'ы
могут не получить change. Альтум-кэш будет stale до следующего read с `?at`.
## 6. Подключение к staging Postgres
```bash
export KUBECONFIG=/tmp/kubeconfig-161
PG_PASS=$(kubectl get secret -n ordinis-staging ordinis-postgres-app \
-o jsonpath='{.data.password}' | base64 -d)
kubectl exec -it -n ordinis-staging ordinis-postgres-1 -c postgres -- \
env PGPASSWORD="$PG_PASS" psql -h localhost -U ordinis_app -d ordinis
```
## 7. Превентивные меры
- **Alert на `ordinis_outbox_dlq_size > 0`** — задействовать когда настроим
Prometheus alerts (v1 closure backlog).
- **Kafka topic provisioning** — все топики `ordinis.cuod.events.{public,internal,restricted}`
должны быть pre-created в Strimzi `KafkaTopic` CR при разворачивании
кластера. Если автоcreate enabled — broker создаст с дефолтами что может
не совпадать (replication factor 1 vs 3 и т.д.).
- **Schema validation** — outbox `payload` валидируется только Jackson'ом.
Schema registry интеграция запланирована на v2.
## 8. Эскалация
| Проблема | Действие |
|---|---|
| DLQ растёт > 100/час | tech lead. Probably broken topic config or producer code |
| DLQ есть после рестарта Kafka — ничего не replays автоматически | Manual `UPDATE outbox_events SET dlq_at=NULL` после убеждения что Kafka OK |
| Дубликаты после replay (consumer получил оба раза) | Consumer должен быть idempotent (наш consumer-side имеет idempotency_keys) |