Оффсеты и семантики доставки
Урок про то, как консьюмер запоминает прогресс и какие гарантии доставки из этого следуют.
Коммит оффсета — сохранение позиции «до какого события я дочитал»; от того, когда вы коммитите относительно обработки, зависит семантика доставки: at-most-once, at-least-once или exactly-once.
Зачем это нужно
Консьюмер падает и перезапускается — с какого места читать? С последнего закоммиченного оффсета. Но если закоммитить до обработки, а потом упасть — событие потеряется. Если после — при падении между обработкой и коммитом оно обработается повторно. Порядок «обработать/закоммитить» и определяет гарантию.
Три семантики
| Семантика | Порядок | Эффект при сбое |
| at-most-once | коммит ДО обработки | возможна потеря, дублей нет |
| at-least-once | коммит ПОСЛЕ обработки | возможны дубли, потерь нет |
| exactly-once | обработка и коммит атомарны | ни потерь, ни дублей |
at-least-once и идемпотентная обработка
По умолчанию выбирают at-least-once: сначала обработали, потом закоммитили. Потерь нет, но возможны повторы. Чтобы повтор не навредил, обработчик делают идемпотентным — повторная обработка того же события даёт тот же результат. Например, UPSERT по ключу события или проверка «уже видел этот id».
at-least-once:
обработать(e) -> записать в БД -> commit(offset)
падение между записью и commit -> e обработается снова
-> спасает идемпотентность (UPSERT, дедуп по id)
exactly-once
Exactly-once в Kafka достижим внутри платформы (read-process-write): транзакционный продюсер атомарно записывает результат и коммитит оффсет в одной транзакции. Если транзакция не завершилась — ни результат, ни оффсет не зафиксированы, повтор начнётся с чистого листа. Это мощно, но дороже и работает в границах Kafka; запись во внешнюю систему «ровно один раз» всё равно требует идемпотентности на её стороне.
Как работает под капотом
Оффсеты консьюмера хранятся в специальном внутреннем топике __consumer_offsets — то есть Kafka использует саму себя как хранилище прогресса. Автокоммит (enable.auto.commit=true) периодически коммитит в фоне по таймеру, что удобно, но опасно: можно закоммитить ещё не обработанные сообщения. Для контроля над семантикой автокоммит выключают и коммитят вручную после успешной обработки. Транзакционный режим (isolation.level=read_committed у консьюмера) заставляет читателя видеть только зафиксированные транзакции — основа exactly-once-конвейеров.
Частые ошибки
- Автокоммит при тяжёлой обработке. Оффсет может закоммититься до завершения работы — потеря при сбое.
- Расчёт на exactly-once во внешней БД «из коробки». EOS Kafka — внутри Kafka; внешний приёмник всё равно нужно делать идемпотентным.
- at-least-once без идемпотентного обработчика. Повторы создадут двойные списания/письма.
Итоги
- Семантику задаёт порядок «обработать/закоммитить»: до — at-most-once, после — at-least-once.
- at-least-once + идемпотентный обработчик — практичный дефолт без потерь.
- Exactly-once в Kafka — транзакционный read-process-write; вовне всё равно нужна идемпотентность.