Бухгалтерия «Гонца» строит журнал проводок: читает оплаты из топика payments и на каждую пишет проводку в топик ledger. Казалось бы, простая перекладка. Но в уроке про оффсеты мы видели, что после падения обработчик получает часть сообщений снова. Для журнала это значит двойное зачисление. Сегодня соберём всё, что знаем о гарантиях, и разберём транзакции Kafka: как сделать, чтобы каждая оплата дала ровно одну проводку.
Перевод между счетами
Ты переводишь другу тысячу рублей. Банк делает две записи: списание с твоего счёта и зачисление на счёт друга. Если между ними отключится свет, нельзя, чтобы деньги списались и не зачислились, или зачислились дважды. Поэтому банк проводит обе записи как одно целое: обе или ни одной. Пока перевод не завершён, его не видно ни в твоей выписке, ни в выписке друга.
В Kafka то же самое умеют транзакции.
Три гарантии
Вспомним, что мы уже знаем:
| Гарантия | Что значит | Когда получается |
|---|---|---|
| at-most-once | может потеряться, не повторится | оффсет коммитят до обработки |
| at-least-once | не потеряется, может повториться | оффсет коммитят после обработки |
| exactly-once | результат ровно один | транзакции внутри Kafka |
Повтор появляется в двух местах. Первое: продюсер не дождался ответа и отправил запись ещё раз. Его закрывает идемпотентный продюсер из урока про подтверждения. Второе: консьюмер обработал сообщение, записал результат, но упал до коммита оффсета. После перезапуска он прочитает то же сообщение и запишет результат снова. Вот это место и закрывают транзакции.
Как устроена транзакция
Продюсер с транзакциями получает имя, transactional.id. Дальше всё идёт по шагам:
initTransactions: продюсер регистрируется у координатора транзакций. Если где-то ещё жив прежний экземпляр с тем же именем, его ограждают: дальше он получает ошибкуProducerFencedException. Так зависший обработчик не испортит журнал.beginTransaction: начало транзакции.- Записи в любые топики и партиции.
sendOffsetsToTransaction: оффсеты прочитанных оплат кладутся в ту же транзакцию.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 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.