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 был отдельным коммитом ранее.
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
# Ordinis НСИ ЦУОД — Operator Runbook
|
||||||
|
|
||||||
|
Карточки процедур для on-call инженера. Каждая карточка self-contained: можно
|
||||||
|
открыть в момент инцидента и пройти по шагам без необходимости понимать всю
|
||||||
|
архитектуру.
|
||||||
|
|
||||||
|
## Indexed by symptom
|
||||||
|
|
||||||
|
| Симптом | Карточка |
|
||||||
|
|---------------------------------------------------------------|----------|
|
||||||
|
| Spring Boot pod'ы залипли в `Init:0/1` после reboot | [vault-unseal.md](vault-unseal.md) |
|
||||||
|
| Kafka pod не Running, либо `vault-agent` не достукивается | [kafka-restart.md](kafka-restart.md) |
|
||||||
|
| Метрика `ordinis_outbox_dlq_size > 0` | [outbox-dlq.md](outbox-dlq.md) |
|
||||||
|
| Pod в `CrashLoopBackOff` с liquibase ошибкой в логах | [db-migrations.md](db-migrations.md) |
|
||||||
|
| API возвращает 401 на любые запросы | См. §1 ниже |
|
||||||
|
| API возвращает 500 на конкретный endpoint | См. §2 ниже |
|
||||||
|
| Admin UI не загружается (502/503) | См. §3 ниже |
|
||||||
|
|
||||||
|
## 1. 401 Unauthorized
|
||||||
|
|
||||||
|
Сейчас на staging `ORDINIS_AUTH_REQUIRED=true`. Anonymous = 401. Проверить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Header передан?
|
||||||
|
curl -v -H "Authorization: Bearer $TOKEN" ... | grep -i authorization
|
||||||
|
|
||||||
|
# 2. JWT валиден?
|
||||||
|
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq
|
||||||
|
# проверить exp, iss, realm_access.roles
|
||||||
|
|
||||||
|
# 3. Issuer правильный?
|
||||||
|
# должен быть https://auth.nstart.space/realms/nstart
|
||||||
|
```
|
||||||
|
|
||||||
|
Если JWT прошёл проверку, но 401 — backend логи:
|
||||||
|
```bash
|
||||||
|
kubectl logs -n ordinis-staging deploy/ordinis-app -c app --tail=50 \
|
||||||
|
| grep -iE "jwt|invalid_token|expired"
|
||||||
|
```
|
||||||
|
|
||||||
|
Типичные причины:
|
||||||
|
- `Invalid signature` — Keycloak ротировал ключи, но бэкенд кэширует старый. Restart pod.
|
||||||
|
- `Token expired` — refresh token не сработал, юзер должен перелогиниться.
|
||||||
|
- `Issuer mismatch` — JWT issued другим Keycloak realm'ом.
|
||||||
|
|
||||||
|
## 2. 500 Internal Server Error на endpoint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Найти trace_id из ответа
|
||||||
|
curl -i ... | grep -i traceid
|
||||||
|
# либо UI показывает traceId в alert
|
||||||
|
|
||||||
|
# Найти полный stack trace
|
||||||
|
kubectl logs -n ordinis-staging deploy/ordinis-app -c app --tail=200 \
|
||||||
|
| grep "<traceId>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Типичные причины (в наших коммитах):
|
||||||
|
- `could not determine data type of parameter $N` — Hibernate JPQL с `IS NULL`
|
||||||
|
на nullable timestamp. Решение — sentinel value (см. fix audit search).
|
||||||
|
- `DataIntegrityViolationException dictionary_records_no_overlap` — bitemporal
|
||||||
|
exclusion constraint, период пересекается. Friendly 409 уже маппится — если
|
||||||
|
500 значит код-путь неуспел поймать.
|
||||||
|
- `JsonMappingException` — payload поломан (фронт передал stale schema).
|
||||||
|
|
||||||
|
## 3. Admin UI не загружается / 502 / 503
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get pods -n ordinis-staging | grep admin-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
| Status | Причина |
|
||||||
|
|-----------------------|---------|
|
||||||
|
| `ImagePullBackOff` | Тег image не существует в registry. Проверь `kubectl describe`. Восстановить:<br>`kubectl set image deployment/ordinis-admin-ui -n ordinis-staging ui=repo.nstart.cloud/terravault/ordinis-admin-ui:main` |
|
||||||
|
| `CrashLoopBackOff` | nginx config invalid либо resolver недоступен. См. logs. |
|
||||||
|
| `Running 1/1`, но 502 | Upstream (writer/read-api) сам в дауне. Проверь те pod'ы. |
|
||||||
|
|
||||||
|
## 4. Что-то ещё — где смотреть
|
||||||
|
|
||||||
|
- **Pod state:** `kubectl get pods -n ordinis-staging`
|
||||||
|
- **Recent events:** `kubectl get events -n ordinis-staging --sort-by='.lastTimestamp' | tail -20`
|
||||||
|
- **Resource usage:** `kubectl top pod -n ordinis-staging`
|
||||||
|
- **Logs:** `kubectl logs -n ordinis-staging deploy/<name> -c app --tail=100`
|
||||||
|
- **Liveness/readiness probes:** `kubectl describe pod -n ordinis-staging <pod>`
|
||||||
|
- **Ingress:** `kubectl get ingress -n ordinis-staging`
|
||||||
|
- **Helm release:** `helm history ordinis -n ordinis-staging`
|
||||||
|
|
||||||
|
## 5. Эскалация
|
||||||
|
|
||||||
|
>30 мин downtime, не понятно куда копать → tech lead (zimin.an@nstart.space).
|
||||||
|
|
||||||
|
## 6. Связанные ресурсы
|
||||||
|
|
||||||
|
- API integration guide: [../tech/api-integration.md](../tech/api-integration.md)
|
||||||
|
- Альтум integration: [../integration/altum-imaging-order.md](../integration/altum-imaging-order.md)
|
||||||
|
- Project state: [../status/2026-05-05-state.md](../status/2026-05-05-state.md)
|
||||||
|
- Helm chart: `ordinis-infra/charts/ordinis/`
|
||||||
|
- Vault setup: `ordinis-infra/k8s/vault/setup.sh`
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Runbook: Liquibase миграции БД
|
||||||
|
|
||||||
|
**Источник правды:** `ordinis-migrations/src/main/resources/db/changelog/`
|
||||||
|
- `master.xml` — список включённых changelog'ов в порядке применения.
|
||||||
|
- `changes/00NN-*.xml` — конкретные изменения (CREATE TABLE, ADD COLUMN и т.д.).
|
||||||
|
|
||||||
|
Миграции применяются **автоматически** при старте `ordinis-app` через
|
||||||
|
`spring.liquibase` (Spring Boot starter). Если миграция падает — pod
|
||||||
|
не стартует, остальные pods тоже не поднимутся (read-api/projection-writer
|
||||||
|
имеют startup-зависимость от схемы БД).
|
||||||
|
|
||||||
|
## 1. Просмотр applied миграций
|
||||||
|
|
||||||
|
```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 -n ordinis-staging ordinis-postgres-1 -c postgres -- \
|
||||||
|
env PGPASSWORD="$PG_PASS" psql -h localhost -U ordinis_app -d ordinis -c \
|
||||||
|
"SELECT id, author, dateexecuted, exectype FROM databasechangelog ORDER BY orderexecuted;"
|
||||||
|
```
|
||||||
|
|
||||||
|
Колонка `exectype`:
|
||||||
|
- `EXECUTED` — миграция применена успешно
|
||||||
|
- `FAILED` — упала (pod не должен стартовать в этом случае)
|
||||||
|
- `MARK_RAN` — пропущена через preCondition
|
||||||
|
- `RERAN` — была применена повторно (manual)
|
||||||
|
|
||||||
|
## 2. Добавление новой миграции
|
||||||
|
|
||||||
|
1. Создать `ordinis-migrations/src/main/resources/db/changelog/changes/00NN-<topic>.xml`.
|
||||||
|
- Номер строго следующий (последний +1).
|
||||||
|
- Author = `ordinis`.
|
||||||
|
- Idempotent: использовать `<preConditions onFail="MARK_RAN">` для добавлений.
|
||||||
|
2. Зарегистрировать в `master.xml` через `<include file="changes/00NN-*.xml"/>`.
|
||||||
|
3. Тестировать локально на dev-Postgres:
|
||||||
|
```bash
|
||||||
|
docker run -d --rm --name ord-pg -e POSTGRES_PASSWORD=dev \
|
||||||
|
-p 5432:5432 postgres:18
|
||||||
|
mvn -pl ordinis-migrations -am liquibase:update \
|
||||||
|
-Dliquibase.url=jdbc:postgresql://localhost:5432/postgres \
|
||||||
|
-Dliquibase.username=postgres -Dliquibase.password=dev
|
||||||
|
```
|
||||||
|
4. Commit + push. CI пересобирает app image, deploy применяет новые миграции
|
||||||
|
при следующем старте pod'а.
|
||||||
|
|
||||||
|
## 3. Откат миграции
|
||||||
|
|
||||||
|
⚠️ **Liquibase rollback требует `<rollback>` блок в changeset**. Без него
|
||||||
|
автоматический откат невозможен — только manual SQL.
|
||||||
|
|
||||||
|
### Если changeset имеет `<rollback>`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Через kubectl exec на pod где есть mvn — обычно нет, проще локально:
|
||||||
|
mvn -pl ordinis-migrations -am liquibase:rollback \
|
||||||
|
-Dliquibase.url=jdbc:postgresql://192.168.100.161:5432/ordinis \
|
||||||
|
-Dliquibase.username=ordinis_app \
|
||||||
|
-Dliquibase.password=$PG_PASS \
|
||||||
|
-Dliquibase.rollbackCount=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual SQL (если rollback не задан)
|
||||||
|
|
||||||
|
1. Прочитать changeset XML — посмотреть какие изменения сделаны.
|
||||||
|
2. Написать обратные SQL (DROP TABLE, ALTER TABLE DROP COLUMN и т.д.).
|
||||||
|
3. Удалить запись из `databasechangelog`:
|
||||||
|
```sql
|
||||||
|
DELETE FROM databasechangelog WHERE id = '00NN-1-some-id' AND author = 'ordinis';
|
||||||
|
```
|
||||||
|
4. После этого Liquibase при следующем старте либо переприменит миграцию
|
||||||
|
(если она всё ещё в master.xml), либо считает её не-применённой.
|
||||||
|
|
||||||
|
## 4. Failed migration recovery
|
||||||
|
|
||||||
|
Сценарий: миграция упала, pod в CrashLoopBackOff.
|
||||||
|
|
||||||
|
### A) Миграция частично применилась
|
||||||
|
|
||||||
|
Liquibase помечает changeset как FAILED в `databasechangelog`. Чтобы
|
||||||
|
повторить:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DELETE FROM databasechangelog WHERE id = '00NN-1-broken' AND exectype = 'FAILED';
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого pod рестарт → Liquibase снова попытается. Если миграция всё ещё
|
||||||
|
broken, исправить XML (или применённый частично state в БД), пересобрать
|
||||||
|
image, redeploy.
|
||||||
|
|
||||||
|
### B) Миграция полностью применилась но с ошибкой в данных
|
||||||
|
|
||||||
|
Например column добавлена с NOT NULL без default. Manual SQL:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE my_table ALTER COLUMN broken_col DROP NOT NULL;
|
||||||
|
-- либо UPDATE my_table SET broken_col = 'default' WHERE broken_col IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
И исправить XML на `defaultValueComputed=...` для следующих deployments.
|
||||||
|
|
||||||
|
## 5. Текущий список миграций (на 2026-05-05)
|
||||||
|
|
||||||
|
| ID | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| 0001-extensions.xml | PostGIS, btree_gist (для tstzrange exclusion) |
|
||||||
|
| 0002-dictionary-definitions.xml | Таблица `dictionary_definitions` |
|
||||||
|
| 0003-dictionary-records.xml | Таблица `dictionary_records` + tstzrange exclusion |
|
||||||
|
| 0004-audit-log.xml | Таблица `audit_log` |
|
||||||
|
| 0005-outbox-events.xml | Таблица `outbox_events` + partial index |
|
||||||
|
| 0006-idempotency-keys.xml | Таблица `idempotency_keys` |
|
||||||
|
| 0007-add-version-columns.xml | JPA optimistic lock columns |
|
||||||
|
| 0008-fix-scope-check-uppercase.xml | Чек на scope в верхнем регистре |
|
||||||
|
| 0009-fix-records-scope-check.xml | Чек consistency dictionary↔record scope |
|
||||||
|
| 0010-envers-tables.xml | Hibernate Envers (revinfo, *_aud) |
|
||||||
|
| 0011-outbox-dlq.xml | DLQ column + partial indexes |
|
||||||
|
|
||||||
|
## 6. Эскалация
|
||||||
|
|
||||||
|
| Проблема | Действие |
|
||||||
|
|---|---|
|
||||||
|
| Migration на staging упала, pod не стартует | DELETE FAILED row + fix XML + redeploy. См. §4A. |
|
||||||
|
| Migration on prod упала | **СТОП**. Не делать manual fix без apply tech lead. Возможно нужен rollback всего deployment. |
|
||||||
|
| `databasechangelog` lock застрял (другая миграция параллельно) | `DELETE FROM databasechangelog_lock WHERE id=1; INSERT INTO databasechangelog_lock VALUES (1, false, NULL, NULL);` |
|
||||||
|
| Postgres недоступен | См. CNPG cluster status: `kubectl get cluster -n ordinis-staging` |
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Runbook: Kafka StrimziPodSet рестарт / поднятие
|
||||||
|
|
||||||
|
**Когда использовать:**
|
||||||
|
- Kafka pod (`ordinis-kafka-dual-0`) не Running после reboot ноды.
|
||||||
|
- Strimzi cluster operator не пересоздаёт pod — например после manual delete с paused reconciliation.
|
||||||
|
- StrimziPodSet есть, но `READY PODS = 0`.
|
||||||
|
- Кафка-операции отвечают timeout / connection refused.
|
||||||
|
|
||||||
|
## 1. Диагностика
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export KUBECONFIG=/tmp/kubeconfig-161
|
||||||
|
kubectl get kafka,kafkanodepool,strimzipodset -n ordinis-staging
|
||||||
|
kubectl get pods -n ordinis-staging | grep kafka
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что искать:**
|
||||||
|
|
||||||
|
| Симптом | Причина |
|
||||||
|
|---|---|
|
||||||
|
| `kafka.kafka.strimzi.io/ordinis-kafka` имеет `WARNINGS` колонку с текстом | reconciliation paused или config invalid — см. `kubectl describe kafka` |
|
||||||
|
| `strimzipodset/ordinis-kafka-dual` отсутствует | удалён — нужно unpause reconciliation чтобы Strimzi его создал заново |
|
||||||
|
| `kafkanodepool/dual` имеет `DESIRED REPLICAS=0` | scale=0 был задан — Strimzi не позволяет 0 реплик в Kafka cluster |
|
||||||
|
| Pod `ordinis-kafka-dual-0` в `Pending` | scheduler не нашёл ноду — `kubectl describe pod` |
|
||||||
|
| Pod `Running` но `0/1` ready | startup probe failed — `kubectl logs` |
|
||||||
|
|
||||||
|
## 2. Восстановление
|
||||||
|
|
||||||
|
### Вариант A: pause/unpause reconciliation
|
||||||
|
|
||||||
|
Если Kafka CR имеет `strimzi.io/pause-reconciliation=true` annotation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl annotate kafka.kafka.strimzi.io/ordinis-kafka -n ordinis-staging \
|
||||||
|
strimzi.io/pause-reconciliation- --overwrite
|
||||||
|
kubectl annotate kafkanodepool.kafka.strimzi.io/dual -n ordinis-staging \
|
||||||
|
strimzi.io/pause-reconciliation- --overwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
Strimzi в течение ~30 sec пересоздаст StrimziPodSet и поднимет pod.
|
||||||
|
|
||||||
|
### Вариант B: scale до 0 → нужно вернуть к 1+
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl patch kafkanodepool/dual -n ordinis-staging --type=merge \
|
||||||
|
-p '{"spec":{"replicas":1}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Strimzi требует минимум 1 replica** — при `replicas:0` валидация падает с
|
||||||
|
`InvalidResourceException: KafkaNodePools have 0 replicas`.
|
||||||
|
|
||||||
|
### Вариант C: StrimziPodSet удалён (например `kubectl delete strimzipodset`)
|
||||||
|
|
||||||
|
Если pause снят и replicas≥1, Strimzi пересоздаст автоматически. Дать
|
||||||
|
60 sec на reconciliation. Если не происходит:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Принудительно перепинговать operator
|
||||||
|
kubectl annotate kafka.kafka.strimzi.io/ordinis-kafka -n ordinis-staging \
|
||||||
|
strimzi.io/manual-rolling-update-
|
||||||
|
kubectl annotate kafka.kafka.strimzi.io/ordinis-kafka -n ordinis-staging \
|
||||||
|
strimzi.io/manual-rolling-update=true --overwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант D: Pod залип в Pending / CrashLoopBackOff
|
||||||
|
|
||||||
|
Force-delete + дать operator пересоздать:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl delete pod -n ordinis-staging ordinis-kafka-dual-0 \
|
||||||
|
--grace-period=0 --force
|
||||||
|
```
|
||||||
|
|
||||||
|
PVC `data-0-ordinis-kafka-dual-0` сохраняется, данные kraft-метаданных целы.
|
||||||
|
|
||||||
|
## 3. Подтверждение
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get pods -n ordinis-staging | grep kafka
|
||||||
|
# → ordinis-kafka-dual-0 1/1 Running
|
||||||
|
|
||||||
|
kubectl exec -n ordinis-staging ordinis-kafka-dual-0 -- \
|
||||||
|
bin/kafka-broker-api-versions.sh \
|
||||||
|
--bootstrap-server ordinis-kafka-kafka-bootstrap:9092 2>&1 | head -3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. После kafka up — рестарт consumer'ов
|
||||||
|
|
||||||
|
Spring Boot pods (`ordinis-app`, `ordinis-projection-writer`) могли застрять
|
||||||
|
в reconnect-loop. Force-delete для быстрого resume:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl delete pod -n ordinis-staging \
|
||||||
|
-l 'app.kubernetes.io/name in (ordinis-app,ordinis-projection-writer)' \
|
||||||
|
--grace-period=0 --force
|
||||||
|
```
|
||||||
|
|
||||||
|
OutboxPoller начнёт публиковать накопленные события сразу.
|
||||||
|
|
||||||
|
## 5. Проверка outbox lag
|
||||||
|
|
||||||
|
После рестарта Kafka outbox мог накопить unpublished events. Проверь:
|
||||||
|
|
||||||
|
```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/stats"
|
||||||
|
# → {"pending":0,"dlq":0} ← желаемое
|
||||||
|
```
|
||||||
|
|
||||||
|
Если pending растёт — см. [outbox-dlq.md](outbox-dlq.md).
|
||||||
|
|
||||||
|
## 6. Эскалация
|
||||||
|
|
||||||
|
| Проблема | Действие |
|
||||||
|
|---|---|
|
||||||
|
| Strimzi operator pod (kafka-test ns) не Running | `kubectl get pods -n kafka-test` → перезапустить |
|
||||||
|
| Consul недоступен (Strimzi его не использует, но Vault — да) | см. [vault-unseal.md](vault-unseal.md) |
|
||||||
|
| Kraft metadata corruption (редко) | DR: восстановить PVC из snapshot, переинициализировать topic |
|
||||||
|
| ACL нарушение / SASL auth fail | Проверить secret `ordinis-app-kafka` в Vault, перезапустить consumer |
|
||||||
|
|
||||||
|
>20 минут downtime → tech lead.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# 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) |
|
||||||
Reference in New Issue
Block a user