Суть Читай реальный конфиг и код со всего трека — размер кворума, проверка fencing, компенсация саги, backoff — предскажи провал и выбери фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги композиции ловятся чтением конфига и кода в швах, а не прозы. Прочитай каждый сниппет, предскажи его поведение под сбоем и выбери фикс, который сеньор делает первым.
Цель
Отработай цикл, который гоняешь в каждом распределённом инциденте: прочитай размер кворума, проверку fencing, компенсацию и политику ретраев, затем найди одно изменение, делающее шов безопасным.
Сниппет 1 — конфиг кворума
# реплицированное хранилище заказовreplication: N: 3 # реплик на ключ W: 1 # ack-ов для подтверждения записи R: 1 # реплик читается на get
Викторина
Completed
При N=3, W=1, R=1 что чтение гарантирует о только что подтверждённой записи и как починить это для read-your-writes?
Heads-up W=1 подтверждает после принятия записи одной репликой. Две другие могут её ещё не иметь, а R=1 чтение может лечь на одну из них — подтверждение не значит репликацию на пересекающий чтение кворум.
Heads-up W=1 ещё и рискует потерять запись, если та одна реплика упадёт до распространения. R + W > N — ровно свойство корректности: оно даёт пересечение read-your-write и долговечность, а не только цену латентности.
Heads-up N=1 убирает отказоустойчивость целиком — потеря одной ноды теряет данные. Цель — пересечение с избыточностью: держи N=3 и ставь W=2, R=2.
Сниппет 2 — проверка fencing
# ресурс, охраняющий шаг саги; вызывается тем, кто считает себя лидеромhighest_token_seen = 0def apply_step(step, token): global highest_token_seen # принять запись, затем запомнить токен write(step) highest_token_seen = max(highest_token_seen, token) return "ok"
Викторина
Completed
Приостановленный старый лидер просыпается с token=7, пока новый уже применил шаги при token=12. Что делает этот код и каков фикс?
Heads-up max() обновляет учёт, но лишь после того, как write(step) уже выполнился. Повреждающая запись происходит в любом случае; токен надо проверить и отвергнуть до записи, а не записывать после неё.
Heads-up Монотонные целые — канонический fencing-токен. Дефект в порядке — запись до валидации — а не в типе токена. UUID вообще нельзя было бы сравнить на монотонность.
Heads-up Полагаться на позднюю запись для починки повреждения — это гонка, а не гарантия: устаревший шаг может быть прочитан или вызвать эффекты до любой перезаписи. Fencing должен отвергнуть устаревшую запись напрямую.
Сниппет 3 — компенсация саги
# компенсация для отменённого заказа; вызывается слоем ретраев по таймаутуdef refund(order_id, amount): # ключ идемпотентности не протянут charge_id = payment_api.create_refund(order_id=order_id, amount=amount) return charge_id
Викторина
Completed
Возврат первого вызова прошёл, но его ответ потерян; слой ретраев вызывает refund() снова в рамках бюджета. Что происходит и каков единственный фикс с наибольшим рычагом?
Heads-up Сага выпускает одну компенсацию, но слой ретраев заново вызывает её по таймауту — в этом вся суть. Одна логическая компенсация становится двумя физическими вызовами, и без ключа второй возвращает деньги снова.
Heads-up Передача order_id — не ключ идемпотентности: у API нет записи, связывающей этот вызов с первой попыткой, так что он создаёт свежий возврат. Делает повтор no-op именно выделенный, записанный ключ идемпотентности.
Heads-up Тогда реально потерянный запрос возврата никогда не завершится, и отменённый заказ молча оставит деньги клиента — неверное число в другую сторону. Ретраи нужны; им нужен ключ, а не удаление.
Сниппет 4 — политика ретраев
def call_with_retry(fn, attempts=6): for i in range(attempts): try: return fn() except Timeout: # фиксированная задержка, без jitter, без бюджета sleep(1.0) raise
Викторина
Completed
Сервис вниз по стеку мигает на две секунды под нагрузкой. Тысячи вызывающих гоняют ровно эту политику. Что даёт фиксированный sleep 1.0s и что меняет сеньор?
Heads-up Дело не в длине задержки, а в синхронизации. Фиксированная задержка заставляет всех повторять в ногу, так что восстанавливающийся сервис бьют выровненные волны. Jitter их рассинхронизирует.
Heads-up Больше попыток ещё сильнее усиливают herd и съедают больше нагрузки во время сбоя. Фикс — форма ретраев (backoff, jitter и бюджет), а не их количество.
Heads-up Более длинная фиксированная задержка всё равно синхронизирует всех вызывающих; она лишь сдвигает волну позже. Только случайный jitter ломает выравнивание, и только бюджет ограничивает суммарную нагрузку ретраев.
Итог
Каждый распределённый инцидент читается в шве: кворум с R + W не больше N молча отдаёт устаревшие чтения; fencing, который пишет до проверки токена, даёт устаревшему лидеру повредить состояние; компенсация, вызванная слоем ретраев без общего ключа, дважды возвращает деньги; а ретрай с фиксированной задержкой без jitter и бюджета превращает миг в herd. Прочитай конфиг и код, почини шов — добавь пересечение, отвергай до записи, протяни ключ, добавь jitter и бюджет — затем проверь под инъекцией сбоев.