Архитектура бэкенда
Когда сбои складываются: каскад, что ни один юнит не мог показать
Прошлый урок проследил один запрос вниз по здоровому стеку, и каждый слой вёл себя. Теперь сделай одну вещь медленной — не сломанной, просто медленной — и смотри, как те же семь механизмов оборачиваются друг против друга. Платёжный провайдер начинает отвечать за 4 секунды вместо 40 миллисекунд. Ещё ничего не упало. Но обработчик, держащий тот вызов провайдера, держит и своё пуловое соединение, и теперь держит его в сто раз дольше, так что пул осушается; с пустым пулом новые запросы блокируются в ожидании получения, так что их латентность тоже лезет; клиент, видя медленные ответы, ретраит — и каждый ретрай это ещё один запрос, что хватает соединение и ждёт, так что ретраи не облегчение, они топливо. Breaker, наблюдая латентность провайдера, наконец размыкается — это система пытается спасти себя — но деплой, что ты стартовал две минуты назад, теперь дренирует под, и джоба в полёте, что он пытается закончить, — то же списание, что таймаутится. Ни один из этих семи механизмов не сломан. Каждый делает ровно то, чему учил его юнит. Авария — это взаимодействие, и это тот род сбоя, что нельзя увидеть, читая код какого-то одного механизма, потому что он не существует, пока все они не запустятся вместе под нагрузкой. Это урок, что чистые комнаты никогда не могли преподать: сбои складываются, и композиция хуже суммы.
Канонический каскад, звено за звеном
Самая частая продовая авария — не краш. Это рост латентности, что система усиливает в коллапс. Проследи цепочку:
- Даунстрим замедляется. p99 платёжного провайдера идёт с 40ms на 4s. Он не возвращает ошибки — просто ответы, поздно. Это вход, и он коварен ровно потому, что ничего не «упало».
- Пул осушается. Каждый медленный вызов держит своё соединение все 4 секунды вместо миллисекунд. С фиксированным пулом соединения-в-использовании лезут вверх, пока пул не пуст. Это пул, ведущий себя корректно — он ограничен, как настаивал юнит пулинга.
- Латентность расходится на несвязанные запросы. Теперь любой запрос, что нуждается в соединении — даже тот, что никогда не трогает платёжного провайдера — блокируется на получении из пула. Медлительность обобщилась с одной зависимости на весь сервис. Один медленный даунстрим теперь проблема всех.
- Ретраи усиливают нагрузку. Клиенты и middleware видят медленные или упавшие запросы и ретраят. Но сервис уже насыщен, так что каждый ретрай — ещё один захват соединения, ещё один поставленный в очередь запрос. Ретраи, призванные восстановить, теперь добавляют нагрузку перегруженной системе — шторм ретраев.
- Breaker размыкается. Circuit breaker, наблюдая латентность и долю ошибок провайдера, открывается. Это система защищает себя: она коротит вызовы провайдера, освобождая соединения и сбрасывая обречённую работу. Хорошо — но это и значит, что каждый платёж теперь падает быстро, меняя нагрузку и долю ошибок, что наблюдают все остальные.
- Дренаж деплоя гонится с работой в полёте. И конечно это когда rolling-деплой, что ты стартовал, дренирует под. Его обработчик graceful shutdown пытается закончить списания в полёте — ровно те списания, что таймаутятся против медленного провайдера — так что дренаж не может завершиться внутри grace period, и на дедлайне SIGKILL забирает полузаконченную джобу.
Каждое звено — механизм, делающий свою работу. Катастрофа — это порядок и обратная связь, не какой-то один сбой.
Почему композиция хуже суммы
Три свойства делают сложенные сбои уникально опасными, и ни одно не видно в одном механизме:
- Обобщение. Сбой в одной зависимости становится медлительностью во всех запросах, потому что они делят ресурс — пул, event loop. Радиус поражения задан тем, что разделено, не тем, что упало.
- Усиление. Ретраи и таймауты, корректные в изоляции, увеличивают нагрузку ровно когда система меньше всего может это позволить. Механизм восстановления становится источником нагрузки. Это сердце шторма ретраев.
- Метастабильность. Раз каскад запущен, удаление исходного триггера не останавливает его. Даже если провайдер восстановился, бэклог ретраев и запросов в очереди держит систему насыщенной — она застряла в плохом стабильном состоянии и нуждается в активном вмешательстве (сбросить нагрузку, дренировать очереди, сбросить breakers), чтобы выбраться. У системы два стабильных состояния, здоровое и схлопнутое, и нагрузка может перекинуть её из одного в другое.
Сеньорный навык: читать кросс-произведение
Джуниор дебажит каскад, спрашивая «какой компонент сломан?» — и не находит ни одного, потому что ни один не сломан. Сеньорный навык — перестать искать сломанную часть и начать читать граф взаимодействий: какие механизмы делят ресурс (так сбой в одном обобщается), какие добавляют нагрузку под стрессом (так усиливают) и какие имеют петли обратной связи (так уходят в метастабильность). Ты рассуждаешь о парах и циклах, не о частях. Изменение бюджета таймаута не локально — оно рябью идёт в пул (как долго держатся соединения), breaker (что считается медленным), слой ретраев (как быстро клиенты сдаются) и дедлайн shutdown (как долго может идти дренаж) разом. Держать весь этот граф в голове — и предсказывать кросс-произведения до каскада — вот работа, к которой ранние юниты тебя строили.
Почему это работает
Почему удаление исходной причины не чинит каскадный сбой — ведь если медленный провайдер восстановится, всё должно вернуться в норму? Потому что к моменту, как каскад запущен, исходный триггер больше не то, что держит его живым; система стала своим собственным источником нагрузки. Представь миг, когда провайдер исцелился: латентность обратно на 40ms. Но пул всё ещё пуст, потому что он полон соединений, держимых запросами, что сами ждут за очередью, что выросла в медленный период; клиенты всё ещё ретраят, потому что всё ещё видят таймауты, вызванные той очередью; и каждый ретрай добавляет ещё один запрос к тому самому бэклогу, что вызывает таймауты. Петля обратной связи, что медленный провайдер запустил, теперь питается целиком очередью и ретраями, что она создала — причина заменена своими собственными следствиями. Это метастабильность: у системы два стабильных равновесия, здоровое (короткая очередь, быстрые ответы, мало ретраев) и схлопнутое (полная очередь, медленные ответы, много ретраев), и достаточно большой шок толкает её из первого во второе, где она остаётся даже после ухода шока. Практическое следствие жестоко и контринтуитивно: нельзя переждать метастабильный сбой, и нельзя починить его, починив зависимость, потому что зависимость больше не проблема. Надо атаковать петлю — сбросить нагрузку, чтобы очередь дренировалась быстрее, чем ретраи её наполняют, ограничить или отключить ретраи, чтобы срезать усиление, сбросить breakers контролируемо, иногда сжать конкуренцию, чтобы меньше работы накапливалось. Это ровно почему инструменты контроля нагрузки и наблюдаемости из следующих двух уроков существуют: нельзя выбраться из метастабильного состояния, что не видишь, и нельзя выбраться из него механизмами, что лишь добавляют нагрузку. Более глубокая мысль в том, что устойчивость — не отсутствие сбоя, а отсутствие усиливающей обратной связи под сбоем — система выживает не потому, что её никогда не шокируют, а потому, что у неё нет схлопнутого равновесия, в которое шок мог бы её толкнуть.
| Звено | Механизм | Корректен в изоляции | Что делает в каскаде |
|---|---|---|---|
| Даунстрим замедляется | — | n/a | Коварный вход: поздно, не упал |
| Пул осушается | Пулинг | Ограничить соединения | Держит соединение 100× дольше → пустой пул |
| Латентность расходится | Async / loop | Делят один loop | Несвязанные запросы блокируются на получении |
| Ретраи наваливают | Идемпотентность / ретраи | Восстановить работу | Добавляют нагрузку насыщенной системе (шторм) |
| Breaker размыкается | Circuit breaker | Перестать долбить | Сбрасывает работу, меняет долю ошибок для всех |
| Дренаж гонится с джобой | Graceful shutdown | Закончить в полёте | Не дренировать джобу, застрявшую на медленном вызове |
p99 платёжного провайдера лезет с 40ms на 4s — он не возвращает ошибки, лишь медленные ответы. Через минуты весь сервис медленный, включая эндпоинты, что никогда не вызывают провайдера. Почему одна медленная зависимость становится проблемой всех?
Во время каскада медленный провайдер полностью восстановился — его латентность вернулась на 40ms — но сервис остаётся схлопнутым. Что это раскрывает?
Расставь канонический каскад латентности от триггера до коллапса:
- 1 Даунстрим-зависимость замедляется (поздние ответы, не ошибки)
- 2 Медленные вызовы держат пуловые соединения дольше, пока пул не осушится
- 3 Несвязанные запросы блокируются на получении, так что латентность обобщается
- 4 Клиенты ретраят медленные запросы, усиливая нагрузку в шторм
- 5 Breaker размыкается, и дренаж деплоя гонится с застрявшей джобой в полёте
- 01Пройди канонический каскад латентности звено за звеном, называя механизм на каждом шаге.
- 02Какие три свойства делают сложенные сбои хуже суммы и что такое метастабильность конкретно?
Прошлый урок проследил здоровый запрос; этот делает один даунстрим медленным — не сломанным, медленным — и смотрит, как семь механизмов оборачиваются друг против друга. Медленные вызовы держат пуловые соединения куда дольше, ограниченный пул осушается, несвязанные запросы блокируются на получении, так что сбой обобщается, ретраи усиливают нагрузку в шторм, breaker размыкается, защищая систему, а дренаж деплоя гонится с джобой в полёте, застрявшей на том же медленном вызове. Каждый механизм корректен; авария — взаимодействие. Сложенные сбои хуже суммы из-за обобщения (общие ресурсы расходят один сбой на всех), усиления (механизмы восстановления добавляют нагрузку) и метастабильности (каскад поддерживает себя своей же очередью и ретраями, так что починка причины не чинит систему). Сеньорный ход — перестать охотиться за сломанной частью и читать граф взаимодействий — общие ресурсы, добавители нагрузки, петли обратной связи — и знать, что изменение бюджета таймаута рябью идёт в пул, breaker, ретраи и shutdown разом. Но нельзя рассуждать о каскаде, что не видишь, или выбраться из него — ровно поэтому следующий урок обращается к наблюдаемости: делая всю систему видимой как одно целое.