Раздел 26 · Data Engineering

Гарантии доставки и exactly-once на практике

junior~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Бухгалтерия «Гонца» строит журнал проводок: читает оплаты из топика payments и на каждую пишет проводку в топик ledger. Казалось бы, простая перекладка. Но в уроке про оффсеты мы видели, что после падения обработчик получает часть сообщений снова. Для журнала это значит двойное зачисление. Сегодня соберём всё, что знаем о гарантиях, и разберём транзакции Kafka: как сделать, чтобы каждая оплата дала ровно одну проводку.

Перевод между счетами

Ты переводишь другу тысячу рублей. Банк делает две записи: списание с твоего счёта и зачисление на счёт друга. Если между ними отключится свет, нельзя, чтобы деньги списались и не зачислились, или зачислились дважды. Поэтому банк проводит обе записи как одно целое: обе или ни одной. Пока перевод не завершён, его не видно ни в твоей выписке, ни в выписке друга.

В Kafka то же самое умеют транзакции.

Три гарантии

Вспомним, что мы уже знаем:

ГарантияЧто значитКогда получается
at-most-onceможет потеряться, не повторитсяоффсет коммитят до обработки
at-least-onceне потеряется, может повторитьсяоффсет коммитят после обработки
exactly-onceрезультат ровно одинтранзакции внутри Kafka

Повтор появляется в двух местах. Первое: продюсер не дождался ответа и отправил запись ещё раз. Его закрывает идемпотентный продюсер из урока про подтверждения. Второе: консьюмер обработал сообщение, записал результат, но упал до коммита оффсета. После перезапуска он прочитает то же сообщение и запишет результат снова. Вот это место и закрывают транзакции.

Как устроена транзакция

Продюсер с транзакциями получает имя, transactional.id. Дальше всё идёт по шагам:

  1. initTransactions: продюсер регистрируется у координатора транзакций. Если где-то ещё жив прежний экземпляр с тем же именем, его ограждают: дальше он получает ошибку ProducerFencedException. Так зависший обработчик не испортит журнал.
  2. beginTransaction: начало транзакции.
  3. Записи в любые топики и партиции.
  4. sendOffsetsToTransaction: оффсеты прочитанных оплат кладутся в ту же транзакцию.
  5. commitTransaction или abortTransaction.

При коммите Kafka пишет в каждую затронутую партицию служебную запись, маркер COMMIT, а при отмене маркер ABORT. Маркер занимает оффсет, поэтому в транзакционных топиках между записями видны пропуски номеров.

Записи транзакции попадают в лог сразу, ещё до коммита. Видны ли они, решает консьюмер:

  • read_uncommitted читает всё подряд, включая отменённое и незавершённое. У Java-клиента и консольного консьюмера это режим по умолчанию.
  • read_committed пропускает отменённые записи и не идёт дальше первой открытой транзакции. В KafkaJS и в нашем пакете @batschool/kafka это режим по умолчанию.

Граница, до которой читает read_committed, называется LSO, last stable offset. Если транзакция повисла, LSO стоит на месте, и read_committed ждёт. Чтобы ждать не вечно, у транзакции есть таймаут transaction.timeout.ms, 60 секунд по умолчанию в Java-клиенте: по его истечении координатор отменяет транзакцию сам.

урок из курса

Урок из курса «Data Engineering». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.