Какие проблемы могут возникнуть при реализации горутины, которая читает из таблицы outbox и пишет в Kafka
Что проверяют
Понимаете ли вы, какие гарантии даёт паттерн outbox и где они теряются в самом релее. Следом обычно спрашивают, что будет, если сервис упадёт между отправкой в Kafka и отметкой в таблице, и как запустить несколько реплик.
Как отвечать
Outbox убирает двойную запись: событие пишется в ту же транзакцию, что и бизнес-данные. Но у горутины-релея свои проблемы:
- дубли: отправили в Kafka, упали до
UPDATE ... SET sent_at, после рестарта событие уйдёт снова. Это at-least-once, поэтому консьюмеры должны быть идемпотентными, а у сообщения нужен стабильный ID; - потеря: обратная ошибка, пометить строку отправленной до подтверждения брокера;
- несколько реплик: копии сервиса читают одни строки. Помогает
SELECT ... FOR UPDATE SKIP LOCKED LIMIT nили выбор лидера; - порядок:
SKIP LOCKEDи параллельная отправка перемешивают события одного агрегата. Ключ сообщения делают равным ID агрегата, чтобы события шли в одну партицию, и внутри агрегата отправляют последовательно; - долгая транзакция: открытая на время ожидания Kafka транзакция держит блокировки и соединение из пула;
- ядовитое сообщение: одна запись, которая не сериализуется или превышает лимит размера, блокирует очередь. Нужны счётчик попыток, backoff и отдельный статус для разбора;
- рост таблицы: отправленные строки удаляют или архивируют, для выборки нужен частичный индекс по неотправленным.
Про Go отдельно: цикл строится на context.Context и time.Ticker, остановка через signal.NotifyContext, перед выходом дождаться текущей пачки и сбросить буфер продюсера. Паника в горутине роняет весь процесс, recover ставят внутри неё самой. В franz-go идемпотентный продюсер включён по умолчанию, в librdkafka его включают enable.idempotence=true: это убирает дубли от ретраев продюсера, но не от падения релея.
Где спотыкаются
Обещают exactly-once. Между базой и Kafka нет общей транзакции, гарантия только at-least-once, и это нормально при идемпотентных консьюмерах.
Запускают релей в каждой реплике без блокировок и получают события по несколько раз.
Опрашивают таблицу каждые 10 мс без индекса. При большом потоке лучше CDC через Debezium.