Потоковая обработка

Exactly-Once Semantics

Банк списал деньги дважды. Склад показывает -5 единиц товара. Уведомление пришло три раза. Всё это - последствия отсутствия exactly-once гарантий в распределённых системах. At-least-once доставка проста, at-most-once тоже. Exactly-once - это то, за что борются инженеры Netflix, LinkedIn и Uber каждый день.

  • **Kafka Transactions** появились в версии 0.11 (2017) именно из-за запросов финтех-компаний: дубли в платёжных пайплайнах стоили реальных денег
  • **Apache Flink** строит exactly-once через checkpointing + Chandy-Lamport algorithm: используется в Alibaba для обработки транзакций Singles' Day
  • **Stripe** публично описывает idempotency keys как фундаментальный API-дизайн: без них retry-логика неизбежно создаёт дубли

Идемпотентность операций

Платёж прошёл, сеть оборвалась, клиент не получил ответ. Что делать? Повторить запрос - и рискнуть списать деньги дважды. Не повторять - и рискнуть потерять платёж. **Идемпотентность** решает эту дилемму: операция, применённая несколько раз, даёт тот же результат, что и одно применение.

Идемпотентная операция безопасна для повтора. `PUT /orders/123` с одними и теми же данными - идемпотентна: результат одинаков при первом и пятом вызове. `POST /orders` - нет: каждый вызов создаёт новый заказ. В streaming системах where retries are unavoidable - идемпотентность первичная защита от дублей.

**Idempotency key** - UUID или hash входных параметров, который клиент генерирует и передаёт с каждым запросом. Stripe, Twilio, большинство payment API требуют его для safe retry. Kafka producer имеет встроенную идемпотентность через `enable.idempotence=true`.

Какая из операций идемпотентна по определению?

Дедупликация: bloom filter и id store

Идемпотентность на стороне отправителя не решает всё: в распределённой системе одно сообщение может прийти дважды из-за retry самого брокера. **Дедупликация** - это обнаружение и отбрасывание дублей на стороне потребителя. Задача проста в теории и нетривиальна при миллионах сообщений в секунду.

Два основных подхода: **id store** (Redis/БД с уже обработанными ID) и **Bloom filter** (вероятностная структура, O(1) проверка, возможны ложные срабатывания). Id store точен, но растёт бесконечно. Bloom filter компактен, но иногда ложно считает сообщение дублем - приемлемо для аналитики, неприемлемо для платежей.

**TTL для id store:** хранить ID вечно нельзя. TTL должен покрывать максимальное окно retry: если система retry-ит в течение 24 часов, TTL = 25+ часов. Kafka Streams и Flink имеют встроенное окно дедупликации.

Bloom filter даёт false positive (говорит 'дубль', хотя сообщение новое). В каком случае это неприемлемо?

Транзакционная запись в Kafka

Задача: прочитать сообщение из Kafka, обработать, записать результат в Kafka и подтвердить offset - всё атомарно. Если упасть между записью и подтверждением, сообщение придёт снова. **Kafka Transactions** решают это: produce + commit offset атомарны.

Kafka транзакции работают через **transactional.id** у producer. Брокер гарантирует: либо все сообщения в транзакции видны консьюмерам, либо ни одно. Consumer должен читать с `isolation.level=read_committed` - иначе увидит незакоммиченные сообщения от упавших producer-ов.

**Цена транзакций:** латентность вырастает на 2-10 мс из-за round-trip к брокеру для begin/commit. Throughput снижается на 20-30%. Используйте только там где exactly-once критична - платежи, инвентарь, идемпотентные агрегации.

Consumer читает из Kafka топика с транзакционными producer-ами. Что нужно настроить для корректного поведения?

End-to-End exactly-once

Kafka транзакции дают exactly-once внутри Kafka. Но реальная система сложнее: данные читаются из Kafka, записываются в PostgreSQL, отправляются в S3. Каждый переход - потенциальное место дублей. **End-to-end exactly-once** - это гарантия на всём пути от источника до sink.

На практике end-to-end exactly-once строится комбинацией техник: идемпотентность операций + дедупликация + транзакции там где поддерживаются. Kafka Streams и Apache Flink реализуют это через **checkpointing**: периодически сохраняют состояние обработки, при сбое восстанавливают с последнего checkpoint и повторяют с идемпотентными операциями.

**At-least-once + idempotent sink = exactly-once эффект.** Часто проще и дешевле, чем полноценные транзакции. Если sink поддерживает upsert по ключу - это надёжная стратегия для большинства аналитических пайплайнов.

Exactly-once гарантии Kafka автоматически распространяются на все downstream системы.

Kafka exactly-once работает только между Kafka топиками. Для внешних sink (БД, S3) нужны отдельные механизмы: idempotent writes, 2PC или checkpoint+replay.

Kafka не может контролировать транзакции внешних систем. End-to-end exactly-once - это комбинация механизмов, а не одна настройка.

Kafka транзакции гарантируют exactly-once. Запись в PostgreSQL из этой же транзакции:

Exactly-Once Semantics

  • Идемпотентность: операция безопасна для повтора - idempotency key + хранение результата в БД
  • Дедупликация: id store (точный) или Bloom filter (быстрый, ~1-5% ложных срабатываний); TTL покрывает retry-окно
  • Kafka транзакции: produce + commitOffset атомарны; consumer читает с isolation.level=read_committed
  • End-to-end: Kafka гарантии только внутри Kafka; для внешних sink - idempotent upsert или checkpoint+replay

Связанные темы

Exactly-once строится на фундаменте других концепций стриминга:

  • Kafka: основы — Базовые гарантии доставки и offset management
  • Event Sourcing — Append-only log + idempotent projections - естественная exactly-once модель
  • Distributed Transactions — 2PC как альтернатива idempotent writes для cross-system exactly-once

Вопросы для размышления

  • В каких бизнес-операциях exactly-once критична, а в каких at-least-once достаточно?
  • Чем idempotency key на уровне API отличается от дедупликации на уровне брокера?
  • Почему at-least-once + idempotent sink часто предпочтительнее полноценных транзакций?

Связанные уроки

  • dist-12-consistency
Exactly-Once Semantics

0

1

Войти