Очереди сообщений
Брокеры сообщений, Kafka, гарантии доставки, паттерн outbox, dead-letter queue и идемпотентность в событийно-ориентированной системе.
12 вопросов
SeniorДизайнОчень частоВы строите API списания платежа на Go, который зовёт внешнего платёжного провайдера и записывает результат в собственную базу. Клиенты на нестабильной сети повторяют списание, в успехе которого не уверены, а балансировщик может доставить две копии одного запроса на два инстанса сразу. Требования:
- С клиента списывается ровно один раз за данную логическую попытку платежа, сколько бы раз запрос ни повторялся и ни дублировался.
- Два конкурентных дублирующих запроса одной попытки не должны оба дойти до провайдера — гонка обязана разрешиться в одно списание.
- Ретрай после успешного списания должен вернуть исходный результат, а не новое списание и не ошибку.
- Если процесс упал после того, как провайдер уже списал с карты, но до коммита локальной записи в БД, поздний ретрай не должен списать дважды.
Укажите, что клиент шлёт для идентификации ретрая, где вы храните состояние списания и каков порядок между вызовом провайдера и собственными записями.
Вы строите API списания платежа на Go, который зовёт внешнего платёжного провайдера и записывает результат в собственную базу. Клиенты на нестабильной сети повторяют списание, в успехе которого не уверены, а балансировщик может доставить две копии одного запроса на два инстанса сразу. Требования: - С клиента списывается ровно один раз за данную логическую попытку платежа, сколько бы раз запрос ни повторялся и ни дублировался. - Два конкурентных дублирующих запроса одной попытки не должны оба дойти до провайдера — гонка обязана разрешиться в одно списание. - Ретрай после успешного списания должен вернуть исходный результат, а не новое списание и не ошибку. - Если процесс упал после того, как провайдер уже списал с карты, но до коммита локальной записи в БД, поздний ретрай не должен списать дважды. Укажите, что клиент шлёт для идентификации ретрая, где вы храните состояние списания и каков порядок между вызовом провайдера и собственными записями.
Требуйте от клиента слать Idempotency-Key. При списании делайте INSERT строки по этому ключу внутри транзакции с уникальным ограничением; первый запрос побеждает и проводит списание, а дубликат упирается в ограничение и возвращает сохранённый результат вместо повторного списания. Сохраняйте ответ вместе с ключом, чтобы ретраи были детерминированы, и зовите платёжного провайдера лишь после резервирования строки.
Типичные ошибки
- ✗Использовать проверку
SELECT-затем-INSERTвместо уникального ограничения — она гонится и оба ретрая списывают - ✗Звать провайдера до резервирования строки ключа, и падение между вызовом и вставкой даёт двойное списание
- ✗Дедуплицировать по хешу тела с TTL вместо клиентского ключа — он истекает и повторно списывает медленный ретрай
Уточняющие вопросы
- →Почему уникальное ограничение лучше проверки
SELECT-затем-INSERTпри конкурентных дублях? - →Если процесс упал после списания у провайдера, но до коммита, как избежать двойного списания при ретрае?
JuniorТеорияЧастоЧто такое событийно-ориентированная архитектура и какую проблему она решает?
Что такое событийно-ориентированная архитектура и какую проблему она решает?
Компоненты общаются, публикуя и потребляя события через брокер, а не вызывая друг друга напрямую. Это даёт слабую связанность и асинхронность — производитель не знает своих потребителей — ценой более сложной отладки и лишь итоговой согласованности.
Типичные ошибки
- ✗Считать событийно-ориентированные системы сильно согласованными — по природе они итогово согласованы
- ✗Думать, что события — это просто логирование, а не реальный канал общения между компонентами
- ✗Полагать, что производитель обязан знать своих потребителей, что разрушило бы слабую связанность
Уточняющие вопросы
- →Какие недостатки добавляет асинхронность по сравнению с синхронным вызовом?
- →Как потребитель сигнализирует, что закончил обработку события?
JuniorТеорияЧастоЗачем нужен брокер сообщений и каковы основные сущности RabbitMQ?
Зачем нужен брокер сообщений и каковы основные сущности RabbitMQ?
Брокер разделяет производителей и потребителей через асинхронную буферизацию, fan-out и повторы. В RabbitMQ производитель публикует в exchange, который по routing key и привязкам маршрутизирует в одну или несколько queue, где потребители подписываются и шлют ack.
Типичные ошибки
- ✗Говорить, что у
RabbitMQесть partition — это понятие Kafka, а не AMQP - ✗Думать, что производители публикуют прямо в очередь, минуя exchange и слой маршрутизации
- ✗Путать роль брокера по развязке с гарантией доставки exactly-once
Уточняющие вопросы
- →Чем отличаются по маршрутизации типы exchange: direct, topic и fanout?
- →Чем потребление по offset с partition в стриминговой платформе
Kafkaотличается от ack в RabbitMQ?
MiddleТеорияЧастоЧто означают гарантии доставки at-least-once, at-most-once и exactly-once?
Что означают гарантии доставки at-least-once, at-most-once и exactly-once?
at-most-once никогда не повторяет доставку, поэтому сообщение может потеряться, но не задвоится. at-least-once повторяет при сбое, поэтому не теряется, но может прийти дважды — потребители обязаны быть идемпотентными. exactly-once сквозь систему труден и обычно приближается как effectively-once через дедупликацию.
Типичные ошибки
- ✗Менять их местами — at-least-once может задвоить, at-most-once может потерять, а не наоборот
- ✗Считать, что брокеры дают настоящий сквозной exactly-once бесплатно, без дедупликации у потребителя
- ✗Забывать, что at-least-once безопасен лишь когда обработчик потребителя идемпотентен
Уточняющие вопросы
- →Какая гарантия подходит для уведомления о платеже и почему?
- →Как дедупликация у потребителя превращает at-least-once в effectively-once?
MiddleТеорияЧастоЧто такое dead-letter queue, и как перечитать сбойные сообщения?
Что такое dead-letter queue, и как перечитать сбойные сообщения?
Dead-letter queue (DLQ) — это боковая очередь, куда брокер направляет сообщения, которые многократно не обрабатываются или превышают лимит повторов, чтобы «отравленное» сообщение не блокировало основную очередь. Причину разбирают или чинят, затем сообщения переигрывают — публикуя обратно в основную очередь или в очередь повторов. Потребители обязаны быть идемпотентными, ведь переигрывание может доставить сообщение более одного раза.
Типичные ошибки
- ✗Думать, что DLQ удаляет сообщения, а не паркует их для разбора и переигрывания
- ✗Переигрывать из DLQ без идемпотентных потребителей, из-за чего переотправленное сообщение обрабатывается дважды
- ✗Давать «отравленному» сообщению повторяться вечно в основной очереди вместо отвода в сторону
Уточняющие вопросы
- →Какая политика повторов/бэкоффа решает, когда сообщение уходит в DLQ?
- →Почему переобработка должна предполагать доставку at-least-once и дедуплицировать по ключу?
MiddleДизайнЧастоСпроектируйте устойчивую очередь фоновых задач на Postgres для пула воркеров на Go. Продюсеры ставят задачи; много конкурентных воркеров забирают и выполняют их. Требования:
- Задачи устойчивы — падение процесса или передеплой не должны терять поставленную или выполняющуюся работу; очередь не может жить только в памяти.
- Много воркеров захватывают задачи конкурентно так, что двое никогда не выполняют одну задачу одновременно и не блокируют друг друга, борясь за следующую доступную задачу.
- Воркер, захвативший задачу и упавший на середине, не должен заклинить её навсегда — после таймаута видимости/аренды задача снова доступна для захвата.
- Сбойные задачи повторяются с backoff до максимального числа попыток, после чего уходят в dead-letter вместо вечных повторов.
Опишите форму таблицы и состояния задачи, как воркер атомарно захватывает задачу, как таймаут видимости возвращает задачу упавшего воркера и поток повторов/dead-letter.
Спроектируйте устойчивую очередь фоновых задач на Postgres для пула воркеров на Go. Продюсеры ставят задачи; много конкурентных воркеров забирают и выполняют их. Требования:
- Задачи устойчивы — падение процесса или передеплой не должны терять поставленную или выполняющуюся работу; очередь не может жить только в памяти.
- Много воркеров захватывают задачи конкурентно так, что двое никогда не выполняют одну задачу одновременно и не блокируют друг друга, борясь за следующую доступную задачу.
- Воркер, захвативший задачу и упавший на середине, не должен заклинить её навсегда — после таймаута видимости/аренды задача снова доступна для захвата.
- Сбойные задачи повторяются с backoff до максимального числа попыток, после чего уходят в dead-letter вместо вечных повторов.
Опишите форму таблицы и состояния задачи, как воркер атомарно захватывает задачу, как таймаут видимости возвращает задачу упавшего воркера и поток повторов/dead-letter.
Храните задачи в таблице jobs с полями status, run_at и attempts. Воркеры захватывают через SELECT ... FOR UPDATE SKIP LOCKED в транзакции, помечая строку running с дедлайном аренды, чтобы она была невидима до таймаута. При успехе удаляйте или ставьте done; при сбое увеличивайте attempts и переносите run_at с backoff, уводя в dead-letter после максимума.
Типичные ошибки
- ✗Делать обычный
SELECT, затемUPDATEбезSKIP LOCKED, из-за чего воркеры дерутся за строку или блокируют друг друга - ✗Держать захватившую транзакцию открытой на всю задачу, занимая соединение БД и блокировку на минуты
- ✗Держать очередь только в канале — тогда падение теряет все задачи в работе и в ожидании
Уточняющие вопросы
- →Как
SKIP LOCKEDпозволяет многим воркерам брать разные задачи без блокировок? - →Что вернёт задачу, чей воркер упал на середине, и как обеспечивается таймаут видимости?
MiddleДизайнЧастоСпроектируйте веерную рассылку уведомлений для Go-сервиса. Одно доменное событие (например, «заказ отправлен») должно достичь многих подписчиков по двум каналам — email и push — каждый через своего внешнего провайдера, который может быть медленным или временно сбоить. Требования:
- HTTP-запрос, порождающий событие, возвращается сразу; вызовы провайдеров не идут на пути запроса и не добавляют задержку пользователю.
- Ни одно уведомление не теряется при деплое, перезапуске или падении процесса сразу после порождения события — оно обязано пережить это в устойчивом хранилище, не только в памяти.
- Сбой одного канала или получателя не должен блокировать или откатывать доставки остальным; каждая доставка повторяется самостоятельно.
- Поскольку отправка из-за повторов возможна не раз, ретрай не должен доставить дублирующее письмо или push получателю.
Опишите, как событие уходит с пути запроса, как оно разворачивается веером в поканальные доставки и какую гарантию доставки берут воркеры.
Спроектируйте веерную рассылку уведомлений для Go-сервиса. Одно доменное событие (например, «заказ отправлен») должно достичь многих подписчиков по двум каналам — email и push — каждый через своего внешнего провайдера, который может быть медленным или временно сбоить. Требования: - HTTP-запрос, порождающий событие, возвращается сразу; вызовы провайдеров не идут на пути запроса и не добавляют задержку пользователю. - Ни одно уведомление не теряется при деплое, перезапуске или падении процесса сразу после порождения события — оно обязано пережить это в устойчивом хранилище, не только в памяти. - Сбой одного канала или получателя не должен блокировать или откатывать доставки остальным; каждая доставка повторяется самостоятельно. - Поскольку отправка из-за повторов возможна не раз, ретрай не должен доставить дублирующее письмо или push получателю. Опишите, как событие уходит с пути запроса, как оно разворачивается веером в поканальные доставки и какую гарантию доставки берут воркеры.
В запросе пишите одно событие в устойчивый брокер или outbox и сразу возвращайтесь. Консьюмер разворачивает его веером: находит подписчиков, затем ставит по задаче на канал (email, push), чтобы каждая доставка повторялась независимо. Воркеры зовут провайдеров email и push, трактуя каждую отправку как at-least-once с ключами идемпотентности, чтобы повторы не задваивали.
Типичные ошибки
- ✗Звать провайдеров инлайн в goroutine запроса — медленный или упавший провайдер теряет уведомления при деплое или падении
- ✗Слать все каналы в одной транзакции, из-за чего один сбойный получатель блокирует или откатывает остальных
- ✗Полагаться на канал в памяти, который теряет все уведомления в очереди при перезапуске процесса
Уточняющие вопросы
- →Зачем разворачивать в задачу на канал, а не в одну задачу на всё событие?
- →Как ключи идемпотентности не дают повтору
at-least-onceотправить дубль письма?
MiddleТеорияИногдаКак группа потребителей в стриминговой платформе Kafka распределяет партиции и ребалансирует при сбое?
Как группа потребителей в стриминговой платформе Kafka распределяет партиции и ребалансирует при сбое?
Внутри группы потребителей каждая партиция назначается ровно одному потребителю, поэтому N партиций на N потребителей дают по одной партиции каждому и максимальный параллелизм; лишние потребители простаивают. Координатор группы запускает ребалансировку, когда участник входит или умирает, переназначая его партиции выжившим — ненадолго приостанавливая чтение, пока коммитятся смещения.
Типичные ошибки
- ✗Думать, что несколько потребителей в группе могут читать одну партицию одновременно
- ✗Ждать прироста пропускной способности от числа потребителей сверх числа партиций — лишние простаивают
- ✗Считать, что партиции мёртвого потребителя выбрасываются, а не переназначаются ребалансировкой
Уточняющие вопросы
- →Почему добавление 7-го потребителя к топику из 6 партиций оставляет одного простаивать?
- →Какой разрыв в чтении вносит ребалансировка, и как закоммиченные смещения его ограничивают?
MiddleТеорияИногдаКак стриминговая платформа Kafka гарантирует порядок событий?
Как стриминговая платформа Kafka гарантирует порядок событий?
Kafka гарантирует порядок только внутри одной партиции, а не по всему топику. Сообщения в одной партиции дописываются и читаются в порядке смещений; у сообщений в разных партициях нет взаимного порядка. Чтобы связанные события шли по порядку, давайте им одинаковый ключ партиции, чтобы они хешировались в одну партицию — ценой ограничения пропускной способностью одного потребителя.
Типичные ошибки
- ✗Считать, что Kafka упорядочивает сообщения глобально по топику, а не только внутри партиции
- ✗Слать связанные события без общего ключа партиции, из-за чего они разлетаются и теряют порядок
- ✗Ждать сохранения порядка от числа партиций — оно повышает параллелизм, но ломает межпартиционный порядок
Уточняющие вопросы
- →Какой компромисс по пропускной способности вы принимаете, направляя горячий ключ в одну партицию?
- →Как увеличение числа партиций топика влияет на порядок существующих ключей?
MiddleТеорияИногдаЧто такое паттерн outbox и какую проблему согласованности он решает?
Что такое паттерн outbox и какую проблему согласованности он решает?
Он решает проблему dual-write: запись в БД и публикация в брокер двумя неатомарными шагами могут оставить одно сделанным, а другое потерянным. Сервис пишет доменное изменение и строку события в одной транзакции, а отдельный relay читает таблицу outbox и публикует.
Типичные ошибки
- ✗Думать, что relay публикует внутри той же транзакции — relay работает отдельно, после коммита
- ✗Считать, что outbox требует распределённый 2PC — его смысл как раз обойтись одной локальной транзакцией
- ✗Забывать, что relay должен помечать или удалять опубликованные строки, иначе событие уходит снова и снова
Уточняющие вопросы
- →Какую гарантию доставки даёт relay — at-least-once или exactly-once?
- →Как не дать relay опубликовать одну и ту же строку дважды?
SeniorТеорияИногдаКак поддерживать согласованность двух сервисов, когда у них разные базы данных?
Как поддерживать согласованность двух сервисов, когда у них разные базы данных?
Одна ACID-транзакция не может охватить две базы данных. Блокирующий вариант — 2PC, применяется редко. Распространённый вариант — паттерн saga: локальные транзакции, где у каждого шага есть компенсирующая транзакция, отменяющая его при сбое, что даёт итоговую согласованность.
Типичные ошибки
- ✗Считать, что одна ACID-транзакция охватит две базы — нет, изоляция кончается на границе БД
- ✗Думать, что saga даёт сильную согласованность — она даёт итоговую с видимыми промежуточными состояниями
- ✗Забывать, что каждому шагу saga нужна компенсирующая транзакция для отката при сбое
Уточняющие вопросы
- →Почему 2PC применяют редко, несмотря на сильную согласованность?
- →Что произойдёт, если сама компенсирующая транзакция упадёт?
SeniorТеорияИногдаКак idempotency key и ACK/NACK делают обработку сообщений безопасной при повторах?
Как idempotency key и ACK/NACK делают обработку сообщений безопасной при повторах?
Идемпотентный обработчик даёт один и тот же результат, сколько бы раз ни запускался. Потребитель хранит idempotency key и пропускает уже виденный ключ. ACK сообщает брокеру, что сообщение обработано; NACK или его отсутствие вызывает повторную доставку — так at-least-once плюс идемпотентность безопасны.
Типичные ошибки
- ✗Слать ACK до обработки — сбой посреди обработчика теряет сообщение, ведь брокер не повторит доставку
- ✗Думать, что идемпотентность убирает повторные доставки — она убирает повторный эффект, доставка всё равно происходит
- ✗Брать нестабильный ключ вроде id goroutine или метки времени вместо стабильного ключа на сообщение
Уточняющие вопросы
- →Где потребителю хранить виденные idempotency key и как долго?
- →Почему ACK нужно слать только после полностью успешной обработки?