Потоковая обработка
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 часто предпочтительнее полноценных транзакций?