АрхитектураРаспределённые системыИнженер по распределённым системам

Сервис обрабатывает события одного заказа параллельно, из за чего отмена иногда применяется раньше оплаты. ...

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

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Используйте партиционирование по идентификатору заказа: все события одного заказа должны попадать в одну партицию, где они обрабатываются последовательно. Разные заказы при этом могут обрабатываться параллельно в разных партициях.

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

Исторический контекст

Глобальный порядок всех сообщений прост для понимания, но требует единой последовательной очереди или координатора. Это ограничивает пропускную способность и делает масштабирование потребителей малоэффективным.

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

Постановка проблемы

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

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

Подробное решение

Выберите ключ партиционирования, равный идентификатору заказа. Брокер детерминированно направляет одинаковые ключи в одну партицию, а порядок сообщений внутри неё сохраняется согласно порядку их принятия брокером.

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

Важно различать два порядка:

  • порядок публикации в партицию — его обычно гарантирует брокер;
  • бизнес-порядок событий — например, оплата произошла до отмены.

Если несколько производителей публикуют события одного заказа параллельно, брокер может получить их не в бизнес-порядке. В таком случае нужны последовательные номера версии, проверка допустимых переходов состояния или другой механизм координации. Нельзя надёжно восстановить причинный порядок только по времени получения.

При сбое сообщение может быть доставлено повторно. Поэтому обработчик должен быть идемпотентным: повторная обработка того же события не должна повторно списывать деньги или ломать состояние. Если раннее сообщение временно не обработано, строгий порядок может задержать последующие сообщения этой партиции — это плата за последовательность.

Главный компромисс — возможное снижение параллелизма для «горячего» ключа. Один заказ нельзя безопасно ускорить простым увеличением числа потребителей, если операции над ним должны оставаться упорядоченными. Дополнительно нужно учитывать стабильность хеширования ключа: изменение числа партиций может изменить маршрутизацию новых сообщений, поэтому миграцию схемы необходимо планировать.

Ситуация из практики

Платёжная система публиковала события оплата, доставка и отмена в общий пул обработчиков. Иногда обработчик отмены завершался первым, и заказ переходил в состояние, из которого последующая успешная оплата уже не могла корректно восстановить бизнес-логику.

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

Выбрали партиционирование по идентификатору заказа. События одного заказа стали обрабатываться последовательно внутри одной партиции, а разные заказы — параллельно; дополнительно добавили идемпотентность и проверку допустимых переходов состояния.

Это устранило перестановку событий внутри потока. При этом система не стала полагаться только на порядок брокера: некорректные или запоздалые события отбрасывались либо переводились на повторную обработку по правилам версии состояния.

Что кандидаты часто упускают

  1. Что произойдёт, если количество партиций изменить после начала обработки?

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

  2. Гарантирует ли одна партиция отсутствие дублей?

    Нет. Порядок и уникальность — разные свойства. После обработки сообщения, но до фиксации подтверждения потребитель может упасть, и брокер доставит сообщение повторно. Поэтому обработчик должен использовать идемпотентный ключ события, уникальную запись о применении или условное изменение версии состояния.

  3. Можно ли сохранить порядок, если один заказ временно требует долгой операции?

    Строгая последовательность означает, что последующие события этого заказа могут ждать завершения предыдущего. Перенос долгой работы во внутренний параллельный процесс способен нарушить порядок побочных эффектов. Обычно используют короткий последовательный этап изменения состояния, отдельное отслеживание незавершенной операции и повторное продолжение потока только после подтверждения результата; если это невозможно, модель состояния должна явно поддерживать асинхронные переходы.