Гарантии доставки вебхуков
Вебхуки — асинхронные уведомления «из системы к подписчику» по HTTP(S). Сеть ненадежна: ответы теряются, пакеты приходят дубликатами или вне порядка. Поэтому гарантии доставки строятся не «по TCP», а на уровне протокола вебхуков и доменной идемпотентности.
Ключевая цель: обеспечить at-least-once доставку с порядком по ключу (там, где необходимо), дать подписчику материалы для идемпотентной обработки и инструмент reconcile для восстановлений.
1) Уровни гарантий
Best-effort — одноразовая попытка, без ретраев. Приемлемо только для «неважных» событий.
At-least-once (рекомендуется) — возможны дубликаты и out-of-order, но событие будет доставлено при условии доступности подписчика в разумный срок.
Effectively-exactly-once (на уровне эффекта) — достигается комбинацией идемпотентности и dedup-хранилища на стороне подписчика/отправителя. На транспорте HTTP «exactly-once» невозможно.
2) Контракт вебхука: минимально необходимое
Заголовки (пример):
X-Webhook-Id: 5d1e6a1b-4f7d-4a3d-8b3a-6c2b2f0f3f21 # глобальный ID события
X-Delivery-Attempt: 3 # номер попытки
X-Event-Type: payment.authorized.v1 # тип/версия
X-Event-Time: 2025-10-31T12:34:56Z # ISO8601
X-Partition-Key: psp_tx_987654 # ключ порядка
X-Seq: 418 # монотонный номер по ключу
X-Signature-Alg: HMAC-SHA256
X-Signature: t=1730378096,v1=hex(hmac(secret, t body))
Content-Type: application/json
Тело (пример):
json
{
"id": "5d1e6a1b-4f7d-4a3d-8b3a-6c2b2f0f3f21",
"type": "payment.authorized.v1",
"occurred_at": "2025-10-31T12:34:56Z",
"partition_key": "psp_tx_987654",
"sequence": 418,
"data": {
"payment_id": "psp_tx_987654",
"amount": "10.00",
"currency": "EUR",
"status": "AUTHORIZED"
},
"schema_version": 1
}
Требование к получателю: отвечать быстро `2xx` после буферизации и валидации подписи, а бизнес-обработку делать асинхронно.
3) Порядок и причинность
Порядок по ключу: гарантия «не уедет» только внутри одного `partition_key` (напр., `player_id`, `wallet_id`, `psp_tx_id`).
Глобальный порядок не гарантируется.
На стороне отправителя — очередь с сериализацией по ключу (один потребитель/шардинг), на стороне получателя — inbox с `(source, event_id)` и опционально ожидание пропущенных `seq`.
Если пропуски критичны — предоставьте pull-API `GET /events?after=checkpoint` для статуса «догнать и свериться».
4) Идемпотентность и дедупликация
Каждый вебхук несет стабильный `X-Webhook-Id`.
Получатель хранит `inbox(event_id)`: PK — `source + event_id`; повторы → no-op.
Побочные эффекты (запись в БД/кошелек) выполняются только один раз при первом «видении» события.
Для команд «с эффектом» используйте Idempotency-Key и кэш результатов на время окна ретраев.
5) Ретраи, backoff и окна
Политика ретраев (референс):- Ретраить на `5xx/timeout/connection error/409-Conflict (retryable)/429`.
- Не ретраить на `4xx` кроме `409/423/429` (и только при согласованной семантике).
- Экспоненциальный backoff + full jitter: 0.5s, 1s, 2s, 4s, 8s, … до `max=10–15 мин`; TTL окна ретраев: например, 72 часа.
- Уважать `Retry-After` у получателя.
- Иметь общий дедлайн: «признать событие не доставленным» и перевести его в DLQ.
yaml retry:
initial_ms: 500 multiplier: 2.0 jitter: full max_delay_ms: 900000 ttl: 72h retry_on: [TIMEOUT, 5xx, 429]
6) DLQ и redrive
DLQ — «кладбище» ядовитых или истекших по TTL событий с полной метаинформацией (пэйлоад, заголовки, ошибки, попытки, хэши).
Веб-консоль/API для redrive (точечная повторная доставка) с опциональной правкой endpoint/секрета.
Rate-limited redrive и batch-redrive с приоритезацией.
7) Безопасность
mTLS (по возможности) или TLS 1.2+.
Подпись тела (HMAC с секретом per tenant/endpoint). Верификация:1. Извлечь `t` (timestamp) из заголовка, проверить скользящее окно (например, ±5 мин).
8) Квоты, rate limits и справедливость
Fair-Queue per tenant/subscriber: чтобы один подписчик/тенант не забил общий пул.
Квоты и burst-лимиты на исходящий трафик и per-endpoint.
Реакция на `429`: чтить `Retry-After`, тротлить поток; при длительном лимитировании — degrade (отправка только критичных типов событий).
9) Жизненный цикл подписки
Register/Verify: POST endpoint → challenge/response или out-of-band подтверждение.
Lease (по желанию): подпись действует до `valid_to`; пролонгация — явная.
Secret rotation: `current_secret`, `next_secret` с `switch_at`.
Test ping: искусственное событие для проверки маршрута перед включением основных топиков.
Health-пробы: периодические HEAD/GET с проверкой latency и TLS профиля.
10) Эволюция схем (версии событий)
Версионирование типа события: `payment.authorized.v1` → `…v2`.
Эволюция — additive (новые поля → MINOR версии API), breaking → новый тип.
Регистр схем (JSON-Schema/Avro/Protobuf) + автоматическая валидация перед отправкой.
Заголовок `X-Event-Type` и поле `schema_version` в теле — оба обязательны.
11) Наблюдаемость и SLO
Метрики (по типу/тенанту/подписчику):- `deliveries_total`, `2xx/4xx/5xx_rate`, `timeout_rate`, `signature_fail_rate`.
- `attempts_avg`, `p50/p95/p99_delivery_latency_ms` (от публикации до 2xx).
- `dedup_rate`, `out_of_order_rate`, `dlq_rate`, `redrive_success_rate`.
- `queue_depth`, `oldest_in_queue_ms`, `throttle_events`.
- Доля доставок ≤ 60 c (p95) — 99.5% для критичных событий.
- DLQ ≤ 0.1% за 24 ч.
- Signature failures ≤ 0.05%.
Логи/трейсинг: `event_id`, `partition_key`, `seq`, `attempt`, `endpoint`, `tenant_id`, `schema_version`, `trace_id`.
12) Референсный алгоритм отправителя
1. Записать событие в транзакционный outbox.
2. Определить partition_key и seq; поместить в очередь.
3. Воркер берет по ключу, формирует запрос, подписывает, отправляет с таймаутами (connect/read).
4. При `2xx` — признать доставленным, зафиксировать латентность и seq-чекпоинт.
5. При `429/5xx/timeout` — ретрай согласно политике.
6. По TTL → DLQ и алерт.
13) Референсный обработчик (получатель)
1. Принять запрос, проверить TLS/proto.
2. Валидация подписи и окна времени.
3. Быстрый ACK 2xx (после синхронной записи в локальный inbox/очередь).
4. Асинхронный воркер читает `inbox`, проверяет `event_id` (дедуп), при необходимости — упорядочивает по `seq` внутри `partition_key`.
5. Выполняет эффекты, пишет «offset/seq чекпоинт» для reconcile.
6. В случае ошибки — локальные ретраи; «ядовитые» задачи → локальная DLQ с алертами.
14) Reconcile (пулл-контур)
Для «непробиваемых» инцидентов:- `GET /events?partition_key=…&after_seq=…&limit=…` — отдать все пропущенные.
- Токен-чекпоинт: `after=opaque_token` вместо seq.
- Идемпотентный redelivery: те же `event_id`, та же подпись по новому `t`.
15) Полезные заголовки и коды
2xx — принял (даже если бизнес-обработка позже).
410 Gone — endpoint закрыт (отправитель прекращает доставку и помечает подписку как «в архив»).
409/423 — временная блокировка ресурса → ретрай разумен.
429 — слишком часто → троттл и backoff.
400/401/403/404 — конфигурационная ошибка; остановить ретраи, открыть тикет.
16) Мульти-тенант и регионы
Отдельные очереди и лимиты per tenant/endpoint.
Data residency: отправка из региона данных; сквозные заголовки `X-Tenant`, `X-Region`.
Изоляция сбоев: падение одного подписчика не влияет на остальных (separate pools).
17) Тестирование
Contract tests: фиксированные примеры тел/подписей, проверка валидации.
Chaos: дроп/дубликаты, shuffle порядка, задержки сети, `RST`, `TLS`-ошибки.
Load: burst-шторма, замер p95/p99.
Security: анти-реплей, устаревшие timestamp, неправильные секреты, ротация.
DR/Replay: массовый redrive из DLQ в изолированном стенде.
18) Плейбуки (runbooks)
1. Рост `signature_fail_rate`
Проверить дрейф часов, истекший `tolerance`, ротацию секретов; временно включить «dual secret».
2. Очередь стареет (`oldest_in_queue_ms` ↑)
Увеличить воркеры, включить приоритизацию критичных топиков, временно понизить частоту «шумных» типов.
3. Шторм `429` у подписчика
Включить троттлинг и паузы между попытками; сдвинуть менее критичные типы событий.
4. Массовые `5xx`
Открыть circuit-breaker для конкретного endpoint, перевести в режим defer & batch; сигнал подписчику.
5. Заполнение DLQ
Остановить не-критичную публикацию, включить batch-redrive с низким RPS, поднять алерты владельцам подписок.
19) Типичные ошибки
Синхронная тяжелая обработка до ответа 2xx → ретраи и дубликаты.
Нет подписи тела/окна времени → уязвимость к подмене/реплею.
Отсутствие `event_id` и `inbox` → невозможно сделать идемпотентность.
Попытка «глобального порядка» → вечные блокировки очередей.
Ретраи без jitter/лимитов → усиление инцидента (thundering herd).
Единый общий пул на всех подписчиков → «шумный» кладет всех.
20) Чек-лист перед продом
- Контракт: `event_id`, `partition_key`, `seq`, `event_type.vN`, подпись HMAC и timestamp.
- Отправитель: outbox, сериализация по ключу, ретраи с backoff+jitter, TTL, DLQ и redrive.
- Получатель: быстрая запись в inbox + 2xx; идемпотентная обработка; локальная DLQ.
- Безопасность: TLS, подписи, анти-реплей, dual-secret, ротация.
- Квоты/лимиты: fair-queue per tenant/endpoint, уважение `Retry-After`.
- Reconcile API и чекпоинты; документация для подписчиков.
- Наблюдаемость: p95/потоки/ошибки/DLQ, трассировка по `event_id`.
- Версионирование событий и политика эволюции схем.
- Плейбуки инцидентов и «кнопка» глобального пауза/разморозки.
Заключение
Надежные вебхуки — это протокол поверх HTTP, а не просто «POST с JSON». Четкий контракт (ID, ключ порядка, подпись), идемпотентность, ретраи с jitter, справедливая очередь и хорошо отлаженные плейбуки превращают «лучший-случай» в предсказуемый и измеряемый механизм доставки. Стройте at-least-once + порядок по ключу + reconcile, и система спокойно переживет сеть, пики нагрузки и человеческие ошибки.