Суть Читай реальную конфигурацию pipeline и код консьюмера — outbox плюс CDC, порядок коммита оффсета, dedup и обработку DLQ — предскажи поведение и выбери фикс, который senior сделает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги pipeline живут не в прозе — они живут в границе транзакции, в порядке коммита, в отсутствующем уникальном ограничении и в конфиге коннектора. Читай каждый сниппет по pipeline заказов и выбирай фикс, к которому senior-инженер тянется первым.
Цель
Потренируй чтение швов event-driven pipeline так, как читаешь инцидент: замечай, где атомарность, порядок, идемпотентность или обработка DLQ тихо сломаны, и называй коррекцию с наибольшим рычагом.
Сниппет 1 — запись в outbox
-- хендлер заказа, одна транзакция БДBEGIN; INSERT INTO orders (id, customer_id, status, total_cents) VALUES ('ord_9f', 'cust_3a', 'placed', 4200); INSERT INTO outbox (id, aggregate_id, type, payload, created_at) VALUES ('evt_7c', 'ord_9f', 'order.placed', '{"order_id":"ord_9f"}', now());COMMIT;-- отдельно, позже, relay:-- SELECT * FROM outbox WHERE sent = false ...-- публикация в Kafka, затем UPDATE outbox SET sent = true
Викторина
Completed
Какую гарантию даёт эта структура и что в результате должны делать консьюмеры ниже по потоку?
Heads-up Транзакцию делят только два INSERT. Публикация relay и UPDATE флага sent — отдельные неатомарные шаги против двух систем, так что краш между ними переотправляет — at-least-once, а не exactly-once.
Heads-up Relay переотправляет при краше до mark, так что один event id может прийти дважды. Без dedup на консьюмере order.placed обработается дважды.
Heads-up Mark-затем-publish просто превращает дубль в потерянное событие: краш после UPDATE и до публикации — строка выглядит отправленной, но ничего не эмитнуто. Publish-затем-mark плюс идемпотентные консьюмеры — безопасный выбор.
Этот коннектор читает таблицу outbox и маршрутизирует события в Kafka-топик, ключуя по aggregate_id. Две настройки плохо взаимодействуют с низкотрафиковым развёртыванием плюс залипшим консьюмером. Что — настоящая production-опасность?
Heads-up Маршрутизация по aggregate_id (id заказа) как раз верна — она ключует события каждого заказа в одну партицию для порядка по заказу. Это и есть задуманный дизайн, а не опасность.
Heads-up Захват специально сделанной таблицы outbox — осознанный выбор: ты контролируешь её форму и избегаешь связывания консьюмеров с сырыми схемами таблиц. Захват всех доменных таблиц — альтернатива, а не баг здесь.
Heads-up SMT EventRouter существует именно чтобы маршрутизировать outbox-события по полю; route.by.field — поддерживаемая, частая настройка. Опасность — во взаимодействии slot/heartbeat, а не в маршрутизации.
Сниппет 3 — порядок коммита у консьюмера
for msg in consumer: # at-least-once Kafka-консьюмер event = parse(msg.value) consumer.commit() # коммит оффсета СНАЧАЛА charge_payment(event.order_id, # затем side effect event.amount_cents)
Викторина
Completed
Этот консьюмер оплаты коммитит оффсет до списания. В чём сбой и какой порядок правильный?
Heads-up Он предотвращает дубли ценой потери сообщений: краш после коммита роняет списание целиком. Для платежей потерянное списание хуже дубля; commit-after-process плюс идемпотентность — правильная комбинация.
Heads-up Порядок parse не влияет на гарантию. Дефект — в коммите оффсета до того, как выполнится side effect, что превращает краш в потерянное списание.
Heads-up Kafka переотправляет с закоммиченного оффсета. Поскольку оффсет уже закоммичен, событие потерянного списания никогда не переотправляется — оно пропало.
Сниппет 4 — обработка DLQ
def handle(event): try: process(event) # идемпотентный side effect except Exception: send_to_dlq(event) # любой сбой -> DLQ consumer.commit()
Викторина
Completed
Этот хендлер отправляет каждый сбой сразу в DLQ при первом исключении. Что идёт не так и какой senior-фикс?
Heads-up Он затапливает DLQ транзиентными сбоями, которые идемпотентный ретрай очистил бы, превращая автоматическое восстановление в ручной труд и пряча по-настоящему необрабатываемые сообщения в шуме.
Heads-up Коммит после того, как сообщение безопасно в DLQ, — правильно: он не даёт poison-сообщению блокировать партицию. Дефект — в DLQ при первом сбое без retry-бюджета.
Heads-up Идемпотентный process нужен в любом случае: at-least-once означает, что replay происходит при краше до коммита, а DLQ не делает dedup живых ретраев. Оставь идемпотентность; добавь retry-бюджет.
Итог
Каждый шов pipeline читается в коде и конфиге: outbox делает два INSERT атомарными, но publish-затем-mark у relay — at-least-once, так что консьюмеры должны делать dedup; CDC-коннектор, ключующий по aggregate_id, хранит порядок по заказу, но heartbeat.interval.ms = 0 без cap на slot — это простой из-за заполнения диска, ждущий залипания; коммит оффсета до side effect — at-most-once и тихо теряет списания, так что коммить после обработки и оставайся идемпотентным; а DLQ при первом исключении хоронит poison-сообщения под восстановимыми транзиентами, так что ретрай с бюджетом и карантин только при исчерпании. Читай границу транзакции, порядок коммита, уникальное ограничение и конфиг коннектора — именно там и живёт корректность pipeline.