awesome-everything EN
↑ Обратно к восхождению

Распределённые системы

Собираем всё вместе: пайплайн заказа, где протекают швы

Суть Каждый примитив — кворум, выборы лидера, часы, саги, ретраи — корректен по отдельности. Капстоун собирает их в один пайплайн заказа/оплаты и показывает баг композиции двух корректных слоёв: компенсация в гонке с ретраем даёт двойной возврат.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Клиенту вернули деньги дважды за один отменённый заказ. Финансы помечают это; дежурный инженер вытягивает трейс. Платёжный сервис идемпотентен, оркестратор саги — образцовый, у политики ретраев есть backoff и jitter — каждый слой проходит свои юнит-тесты. Баг не живёт ни в одном из них. Оркестратор выпустил компенсацию refund, вызов отвалился по таймауту на 30s, хотя возврат на самом деле прошёл, в бюджете ретраев было место — и слой повторил вызов, а у шага возврата не было ключа идемпотентности. Два корректных слоя при композиции дали неверное число в выписке реального клиента.

Система: один пайплайн заказа, шесть примитивов

Всё в этом треке было примитивом по отдельности. Капстоун — одна реалистичная система, использующая их сразу: пайплайн заказа/оплаты на четыре сервиса — Order, Payment, Inventory, Shipping — координируемый сагой. Пройди по запросу, и каждый прошлый урок всплывает как слой:

  • Кворумная репликация (R + W > N): сервис Order пишет в реплицированное хранилище. При N=3, W=2, R=2 запись, легшая на 2 из 3 реплик, переживает потерю одной ноды, и любое последующее чтение её пересекает. Именно это делает «заказ создан» долговечным.
  • Выборы лидера + fencing-токены: оркестратор саги работает как единственный лидер, чтобы два координатора не вели один заказ. Когда лидер замирает (GC, сетевой раздел) и выбирается новый, старый может проснуться и всё ещё действовать — поэтому каждая запись вниз несёт монотонный fencing-токен, и ресурсы отвергают любой токен меньше наибольшего виденного.
  • Часы / упорядочивание: нельзя доверять wall-clock меткам для упорядочивания событий между сервисами. Логические часы (или TrueTime Spanner с ограниченной неопределённостью) упорядочивают шаги саги; «Payment произошёл до Shipping» должен быть причинным фактом, а не сравнением Date.now().
  • Саги + компенсации: распределённой ACID-транзакции на четыре сервиса не существует. Сага идёт прямыми шагами и при сбое запускает компенсации — семантические откаты (возврат, возврат на склад), а не rollback.
  • Дисциплина ретраев: каждый межсервисный вызов повторяется с экспоненциальным backoff и jitter, под бюджетом ретраев, за circuit breaker.

Каждый слой корректен. Интересные провалы не внутри слоёв — они в том, как слои соединяются.

Идемпотентность — несущий примитив

Доставка at-least-once и ретраи — дефолтная реальность распределённых систем: вызов, отвалившийся по таймауту, мог пройти, и со стороны вызывающего этого не отличить. Единственный безопасный ответ на «случилось ли это?», когда узнать нельзя, — сделать «сделать снова» безвредным. Это и есть идемпотентность, и именно она делает безопасными все остальные слои пайплайна.

Ключевое слово — ключ. Идемпотентность — это не «операция от природы повторяема»: decrement stock by 1 не идемпотентна. Это «операция несёт стабильный бизнес-ключ, а получатель записывает этот ключ, так что второе прибытие того же ключа — это no-op, возвращающий первый результат». Ловушка, за которой следит сеньор: ключ привязан не к тому. Привяжешь к id HTTP-запроса — и ретрай, сгенерированный другим слоем (сага vs SDK vs клиент), получит свежий id и проскользнёт. Ключ должен выводиться из бизнес-намерения — refund:order-8842:attempt-1 — и протягиваться через каждый слой, который может повторить вызов.

Почему это работает

«At-least-once» и «exactly-once» — это не два режима доставки, между которыми выбираешь. Доставка exactly-once по сети невозможна; то, что называют «exactly-once», на деле — доставка at-least-once плюс идемпотентная обработка по dedup-id. Сеть даёт дубликаты; ключ даёт exactly-once эффекты. Смешение этих двух — то, как команды выкатывают сагу в вере, что брокер гарантирует отсутствие дубликатов.

Провал композиции: компенсация в гонке с ретраем

Вот баг из Hook, механически. Сага решает отменить и выпускает компенсацию refund:

tСлой саги / ретраевПлатёжный сервисЭффект
0sвыпускает компенсацию refundполучает запрос, начинает возврат
28sждёт ответавозврат проходит, ответ задержан$50 возвращены (1-й раз)
30sтаймаут; бюджет OK → ретрайполучает 2-й запрос, без dedup-ключа$50 возвращены (2-й раз)
31sретрай возвращает 200двойной возврат

Прогони каждый слой отдельно — он невиновен. Таймаут был разумным. В бюджете ретраев было место, так что ретрай был правильным решением политики. Логика компенсации верна — отменённый заказ должен возвращать деньги. Платёжный сервис был идемпотентен для пути списания. Дефект целиком в шве: ретрай (один корректный слой) заново вызвал компенсацию (другой корректный слой) через вызов без общего ключа идемпотентности. Чинится протягиванием одного ключа — refund:order-8842 — от саги через слой ретраев в платёжный сервис, где второе прибытие этого ключа возвращает результат первого возврата вместо выпуска нового. В самих слоях ничего не меняется; шов получает общий ключ.

Наблюдаемость — то, как этим реально управляют

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

  • Consumer lag — насколько отстали консьюмеры событий саги. Растущий lag значит, что пайплайн отстаёт от продюсеров; саги застревают на полпути, а компенсации в полёте копятся.
  • Latency кворумной записи/чтения — p99 записи W=2. Медленная третья реплика тянет хвостовую latency, даже когда ни одна нода технически не «упала».
  • Leader churn — выборы в минуту. Частые перевыборы значат, что fencing-токены постоянно растут и ты в одной паузе от попытки split-brain записи.
  • Исчерпание бюджета ретраев — процент потреблённого бюджета. Когда бюджет около 100%, ретраи вот-вот будут отброшены, что выходит наружу как видимые пользователю ошибки, хотя каждый сервис вниз здоров. Бюджеты обычно ставят так, чтобы ретраи добавляли максимум ~10% сверх обычного rate запросов — ровно чтобы шторм ретраев не усилил мелкий сбой до аварии.

Инстинкт сеньора: алертить на швы, а не только на ноды. Пайплайн из здоровых сервисов всё равно может подводить клиентов.

Выбери лучший вариант

Компенсацию `refund` твоей саги заказа слой ретраев может повторить после таймаута. Где поставить защиту, чтобы двойной возврат стал невозможен?

Викторина

Каждый сервис в пайплайне заказа проходит свои тесты, но клиенты иногда видят двойные списания. Где баг скорее всего?

Викторина

Оркестратор саги работает как выбранный лидер. После долгой паузы GC он просыпается и пытается записать шаг саги. Что мешает ему испортить состояние, которое новый лидер уже продвинул?

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

Расставь шаги диагностики провала композиции (периодический двойной возврат), где все сервисы зелёные:

  1. 1 Воспроизводи по трейсу, а не по логу одного сервиса — баги композиции пересекают слои
  2. 2 Найди шов: какие два корректных слоя взаимодействовали (здесь: слой ретраев, заново вызывающий компенсацию)
  3. 3 Проверь, пересекает ли этот шов стабильный ключ идемпотентности — обычно нет
  4. 4 Протяни один бизнес-ключ через каждый слой, который может повторить операцию
  5. 5 Делай dedup на получателе по этому ключу и добавь алерт уровня шва (например, счётчик дубль-эффектов)
Вспомните перед уходом
  1. 01
    Проведи коллегу по тому, как компенсация возврата и ретрай складываются в двойной возврат, хотя оба слоя по отдельности корректны, и как одно изменение это чинит.
  2. 02
    Почему идемпотентность называют несущим примитивом всего пайплайна, и что делает ключ корректным против ключа, который молча проваливается?
Итог

Капстоун — не новый примитив, а осознание, что каждый примитив в этом треке корректен по отдельности, а реальные провалы живут в швах, где они соединяются. Один пайплайн заказа/оплаты использует их все: кворумную репликацию (R + W > N) для долговечных записей заказа, выбранного лидера с fencing-токенами, чтобы паузнутый оркестратор не испортил состояние, продвинутое новым лидером, логические часы или TrueTime для причинного упорядочивания шагов, саги с компенсациями, потому что распределённой ACID-транзакции нет, и ретраи с backoff, jitter и бюджетом, ограниченным около 10% сверх обычного трафика. Идемпотентность по бизнес-ключу — несущий примитив: доставка at-least-once делает дубликаты неизбежными, поэтому единственный безопасный ответ на непознаваемое «случилось ли это?» — сделать «сделать снова» безвредным. Каноничный провал композиции — компенсация возврата в гонке с ретраем через вызов без общего ключа, дающая двойной возврат, пока тесты каждого слоя остаются зелёными — чинится протягиванием одного ключа через шов, а не изменением слоя. Всем этим управляют по сигналам швов — consumer lag, latency кворума, leader churn, исчерпание бюджета ретраев — потому что пайплайн из здоровых сервисов всё равно может подводить реальных клиентов.

Продолжить восхождение ↑Распределённый капстоун: проверка с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.