open atlas
↑ К треку
Очереди, потоки, события QUE · 01 · 02

Три ножки сбоя — где реально происходят дубликаты и потери

Как сбои producer-to-broker, durability брокера и broker-to-consumer каждый производят дубликаты или потери, прослеженные через реальный инцидент двойного списания.

QUE Middle ◷ 10 min
Уровень
ОсновыJuniorMiddleSenior

On-call инженер смотрит на график с 1200 Stripe-списаниями за 900 заказов. Очередь работала шесть месяцев без проблем. Сегодня ночью она списала с клиентов дважды. В коде ничего не изменилось. Сбой был всегда — ждал нужной комбинации медленной Lambda и SQS visibility timeout.

Три ножки сбоя

Если хочешь понять, где именно проскальзывает дубликат — не в теории, а на какой конкретной границе — вот три места, которые нужно проверить. Зная, какая ножка его породила, сразу понимаешь, какой фикс применять.

Сообщение проходит через три отдельные коммуникационные границы. Каждая граница может упасть так, что производит потерю или дубликат.

Ножка 1 — Producer к брокеру. Producer отправляет сообщение и ждёт ack от брокера. Если сеть роняет send — сообщение не дошло. Если сеть роняет ack — producer никогда не узнает, что сообщение сохранено, и ретраит. Брокер получает то же логическое сообщение второй раз. At-least-once producers ретраят на timeout — это feature, не баг, но это значит, что брокер может видеть одно и то же сообщение несколько раз.

Ножка 2 — Durability брокера. Брокер ack-нул producer, но должен сохранить сообщение до краша. Если существует replication lag при краше брокера, followers могут не иметь сообщения. Правильная конфигурация (Kafka acks=all, SQS надёжность по умолчанию) закрывает эту ножку, но misconfiguration тихо роняет сообщения здесь.

Ножка 3 — Брокер к consumer. Именно здесь происходит большинство production-дубликатов. Брокер доставляет сообщение consumer. Consumer обрабатывает (вызывает Stripe, апдейтит БД), потом отправляет ack. Если consumer падает после обработки, но до ack — у брокера нет записи об успехе. Он передоставляет сообщение следующему consumer. Side effect выполняется дважды.

Три ножки сбоя
Producer → BrokerAck потерян → producer ретраит → брокер видит сообщение дважды
Durability брокераКраш до репликации → потеря (config-сбой)
Broker → ConsumerConsumer обрабатывает, потом падает до ack → брокер передоставляет

Все три ножки производят дубликаты, но только ножка 3 делает это после того, как side effect уже выполнился — именно поэтому она приводит к двойным списаниям. Ножки 1 и 2 создают дубликаты или потери ещё до выполнения бизнес-логики, и их проще поглотить конфигурацией брокера. Без идемпотентного consumer ножку 3 не поймать со стороны брокера.

Ножку 3 стоит проследить на уровень глубже, потому что порядок обработки и ack решает, станет ли сбой дубликатом или потерей. Диаграмма ниже идёт по жизненному циклу consume→process→ack и отмечает, где бьёт каждый сбой.

Трасса инцидента двойного списания

Вот точный SQS visibility-timeout сценарий, который ловит инженеров каждые полгода.

  1. T=0с: Lambda получает msg-7a3f («списать $50 за заказ O-123»). SQS прячет сообщение от других consumer-ов на visibility timeout (дефолт 30с).
  2. T=0.5с: Lambda вызывает Stripe. Stripe списывает карту. charge_id=ch_abc123 возвращён.
  3. T=29с: Lambda всё ещё работает (медленная downstream DB-запись). DeleteMessage ещё не вызван.
  4. T=30с: SQS visibility timeout срабатывает. msg-7a3f становится visible для всех consumer-ов снова — у SQS нет информации, что первая Lambda преуспела.
  5. T=30.1с: Вторая Lambda поднимает msg-7a3f. Вызывает Stripe. Второе списание: ch_xyz789.
  6. T=31с: Первая Lambda наконец вызывает DeleteMessage. Сообщение удалено. Но ущерб нанесён: клиент списан $100 за заказ $50.

Сбой — не баг. Это определённое поведение at-least-once delivery, когда visibility timeout короче времени обработки.

Викторина

Consumer читает сообщение, успешно обрабатывает, но падает до ack. Что делает at-least-once delivery?

Викторина

SQS consumer visibility timeout 10с, но среднее processing 30с. Какой режим сбоя?

Расставь шаги по порядку

Поставь события инцидента двойного списания:

  1. 1 Lambda получает msg-7a3f; SQS устанавливает visibility timeout 30с
  2. 2 Lambda успешно вызывает Stripe — создан ch_abc123
  3. 3 Visibility timeout истекает; msg-7a3f становится visible для всех consumer-ов
  4. 4 Вторая Lambda получает msg-7a3f; снова вызывает Stripe — создан ch_xyz789
  5. 5 Первая Lambda наконец вызывает DeleteMessage — сообщение удалено, но два списания существуют
Вспомните перед уходом
  1. 01
    Что происходит на ножке 3, когда consumer падает после обработки, но до ack?
  2. 02
    Каково rule of thumb для SQS visibility timeout и почему?
  3. 03
    Почему потеря ack на ножке 1 вызывает дубликаты, а не потери?
Итог

У каждой message queue три ножки сбоя: producer-to-broker (потерянный ack вызывает producer retry), broker durability (replication misconfiguration вызывает потерю) и broker-to-consumer (обработка успешна, но ack потерян вызывает redelivery). Самый распространённый production-дубликат на ножке 3: consumer обрабатывает Stripe-charge, падает до SQS ack, visibility timeout (таймер, на время которого SQS прячет сообщение от других consumer-ов) срабатывает, второй consumer поднимает то же сообщение и списывает снова. Структурный фикс — идемпотентный consumer с dedup-стором плюс visibility timeout не менее 6x среднего processing time. Теперь, когда встретишь двойное списание или дублирующее событие в данных, сразу спроси: ack был потерян после успешной обработки — или visibility timeout сработал в процессе? Этот вопрос укажет на нужную ножку и нужный фикс.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки
встречается в204

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.