Прочитай реальный код consumer'а, конфиг брокера и обработчик outbox, затем выбери ответ, на который подписался бы senior: баг порядка ack, дубль из-за visibility timeout, двойная запись из inline-публикации и безграничный буфер.
SDSenior◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Баги сообщений живут в коде и конфиге, а не в прозе: куда ты поставил ack, во что выставлен visibility timeout, владеет ли relay публикацией и есть ли у буфера граница. Прочитай каждый сниппет, протрассируй падение и выбери ответ, на который подписался бы senior-инженер.
Потренируй петлю, что ты гоняешь на дизайн- или код-ревью: найди несущую строку, протрассируй, что делает с ней падение, и выбери изменение, что уважает семантику доставки, а не прячет её.
Сниппет 1 — порядок ack
def handle(msg): queue.ack(msg) # подтверждено первым charge_card(msg.amount) # затем побочный эффект
Викторина
Completed
Какую семантику доставки это даёт и в чём риск для платёжного обработчика?
Heads-up Ack не запускает списание; он велит брокеру забыть сообщение. Падение после ack, но до charge_card теряет работу без переотправки. Ack-первым — это at-most-once (с потерями), обратное заявленному.
Heads-up Нет exactly-once по ненадёжной сети. Ack-первым — это at-most-once (возможна потеря); чтобы приблизить once-эффект, ты делаешь ack ПОСЛЕ обработки (at-least-once) и дедуплицируешь. Позиция ack решает гарантию.
Heads-up Освобождение очереди до запуска списания — ровно так ты тихо теряешь платёж при падении. Ack после списания (at-least-once) и дедуп по idempotency-ключу, чтобы переотправка была безвредна.
Сниппет 2 — visibility timeout
# SQS consumervisibility_timeout: 30 # секунд# измеренное время обработки задачи: p50 = 12с, p99 = 75с# обработчик НЕ идемпотентен
Викторина
Completed
С этими числами что происходит с самыми медленными задачами и какова починка?
Heads-up Брокер знает лишь, что таймаут истёк без удаления; он не отличит медленного воркера от упавшего. На 30с он переотправляет, и 75с задача бежит дважды параллельно. Размерь таймаут выше p99 и heartbeat'ь.
Heads-up DLQ для сообщений, что ПАДАЮТ раз за разом, а не для медленных. Медленная, но успешная задача просто переотправляется (дублируется), когда истекает visibility timeout. Подними таймаут / heartbeat.
Heads-up Это делает куда хуже — теперь даже p50 задачи (12с) дублируются. Таймаут должен быть ВЫШЕ реального времени обработки (p99), а не ниже него, плюс heartbeat и идемпотентность.
Сниппет 3 — обработчик outbox
with db.transaction() as tx: tx.insert(order) tx.insert(outbox_row(OrderPlaced)) # атомарно с заказом — хорошо broker.publish(OrderPlaced) # также опубликовано inline, «ради экономии хопа» tx.commit()# отдельный relay ТОЖЕ читает outbox и публикует
Викторина
Completed
Outbox-строка записана верно, так почему inline `broker.publish` — баг?
Heads-up Она тихо воссоздаёт ровно проблему, которую outbox убирает. Падение после inline-публикации, но до commit испускает событие для откатившегося заказа (фантом), и relay дважды публикует. Удали inline-публикацию.
Heads-up Ретрай нетранзакционной публикации не делает её атомарной с коммитом; она всё равно рискует фантомным событием и всё равно дублирует relay. Починка структурна: публикует только relay.
Сниппет 4 — буфер
const queue = [] // в памяти, без макс. размераfunction enqueue(job) { queue.push(job) } // никогда не отвергает// воркер вычерпывает ~1k/с; producer'ы могут рвануть до ~8k/с во время всплесков// симптом: во время всплесков RSS пода лезет вверх, пока его не убьют по OOM
Викторина
Completed
Почему под падает под всплесками и какое изменение верно?
Heads-up Больше памяти лишь оттягивает OOM под устойчивой перегрузкой — безграничная очередь растёт без предела. Починка — граница, применяющая backpressure, а не большая куча, что наполняется медленнее.
Heads-up Быстрее вычерпывание помогает, но без границы достаточно большой всплеск всё равно раздувает очередь до OOM. Нужны граница + fail-fast, чтобы producer'ы чувствовали backpressure во время всплеска, а не просто больше consumer'ов.
Heads-up Нет размера, что «всегда влезает» при устойчивом прибытие > обслуживания — очередь растёт без предела по определению. Граница (и сброс при заполнении) — то, что держит сбой малым, а не больший буфер.
Вспомните перед уходом
01
Как позиция ack решает семантику доставки и что верно для платежа?
02
Почему inline-публикация в обработчике outbox возвращает двойную запись?
Итог
Каждое решение по сообщениям в этом разделе всплывает как несущая строка, что ты читаешь прямо из кода. Позиция ack задаёт семантику доставки — ack-первым это at-most-once (с потерями), ack-последним это at-least-once (дубли), а платёж хочет последнее плюс идемпотентность. Visibility timeout должен сидеть выше реального p99 времени обработки и heartbeat’иться, иначе медленные задачи переотправляются и бегут дважды параллельно. Outbox работает, лишь если обработчик делает только локальную транзакцию, а отдельный relay публикует — inline-публикация тихо возвращает двойную запись (фантомные события плюс дубли relay). А внутрипамятная очередь без границы растёт без предела под устойчивой перегрузкой, пока процесс не упадёт по OOM, поэтому ты ограничиваешь её и быстро падаешь (сигнал backpressure), сбрасывая при заполнении и используя долговечный брокер, если буферизованная работа должна пережить падение. Senior-привычка — найти несущую строку, протрассировать, что делает с ней падение, и выбрать починку, которую требует режим отказа, — а не ту, что её прячет.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.