Знакомая картина: в 3 часа ночи падает база данных, Alertmanager засыпает каналами, через 5 минут сеть восстанавливается, но… система остаётся «мёртвой». CPU на воркерах 100 %, Goodput на нуле, а перезапуск подов только усугубляет ситуацию. Все указывает на то, что вы столкнулись с метастабильным отказом — самым коварным типом аварий в распределённых системах.

Меня зовут Артём Баранов, в «Базисе» я занимаюсь автоматизацией и развитием инфраструктуры. В этой статье я хочу рассказать о математике метастабильного отказа и через теорию массового обслуживания — конкретно через модель G/G/1 и формулу Кингмана — проложить мост от математических абстракций к реальным логам, метрикам и алертам. Далее, предложить практикующему SRE-специалисту набор методик для анализа, диагностики и предотвращения катастроф, возникающих в информационных системах по тем или иным причинам (ошибки конфигурирования, сбои «железа» и т. д.). Это позволит инженеру распознать sustaining-эффект по логам и метрикам и осознанно выбрать рычаг (backoff, retry budget, shedding, circuit breaker), а не просто объяснить постфактум, почему возникла предыдущая авария.

Несколько замечаний, прежде чем мы начнем

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

Второе, эта статья — инженерная компиляция нескольких моделей (fluid-ODE, Линдли, Max-Plus, аппроксимация Кингмана), а не единое строгое доказательство. Каждая модель отвечает на свой вопрос и имеет жёсткую зону применимости; ниже мы явно фиксируем эти границы.

Третье, рассмотреть весь материал, в полной мере освещающий проблематику метаотказов, в рамках одной статьи невозможно. Формальные методы (TLA^+, model checking, алгебры процессов) сознательно оставлены за рамками: здесь они упомянуты как карта местности, но не разворачиваются.

1. Складка, гистерезис и два аттрактора

Классический пример бистабильности и аттракторов в метастабильных отказах — это система обработки интернет-трафика (микросервис или база данных) с механизмом автоматических повторов (retries). При временном всплеске нагрузки система из здорового состояния переходит в состояние «залипания» с нулевой пропускной способностью, где и остаётся даже после исчезновения исходной причины сбоя. Именно этот механизм и называется метастабильностью: внешний триггер уже исчез, но внутренняя петля (сустейнер) продолжает питать аттрактор отказа.

С точки зрения теории катастроф это — складка (fold): множество стационарных решений f(x,\lambda_0)=0 при изменении \lambda_0 образует на плоскости (\lambda_0,\,x^*) характерную ветвящуюся кривую — в классической картине с двумя устойчивыми ветвями и одной неустойчивой между ними (оговорки о числе корней — ниже и в разд. 2; рис. 1 построен по двухпороговой модели из того же раздела).

Рисунок 1. Ветви равновесий двухпороговой модели повторов (параметры — численный пример разд. 2): корни  при изменении . Зелёные точки — устойчивые (), красные — неустойчивые (). Вертикаль  — срез из таблицы:  (сепаратриса),  (сбойный аттрактор), . Здоровый аттрактор в этом примере — граница  (при  очередь дренируется). В однопороговой модели с одной сигмоидой классическая картина с тремя корнями не реализуется.
Рисунок 1. Ветви равновесий двухпороговой модели повторов (параметры — численный пример разд. 2): корни f(x;\lambda_0)=0 при изменении \lambda_0. Зелёные точки — устойчивые (f'<0), красные — неустойчивые (f'>0). Вертикаль \lambda_0=0{,}5 — срез из таблицы: x_a\approx 0{,}575 (сепаратриса), x_b\approx 1{,}385 (сбойный аттрактор), x_c\approx 2{,}715. Здоровый аттрактор в этом примере — граница x=0 (при x<x_a очередь дренируется). В однопороговой модели с одной сигмоидой классическая картина с тремя корнями не реализуется.

Точки складки и гистерезис

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

Что определяет форму складки

Амплитуда складки по \lambda_0 растёт с R — интенсивностью повторов. При R\to 0 складка вырождается в монотонную кривую, и бистабильность исчезает: без повторов метастабильного отказа не бывает. Крутизна перехода задаётся k (жёсткостью клиентского таймаута); сдвиг по оси x — параметром x_{\mathrm{crit}} (порог срабатывания повторов). Глубина N-образности функции f=\lambda_{\mathrm{total}}-\mu определяется параметром \gamma в аппроксимации \mu(x): при \gamma=0 функция монотонна, и складки нет вовсе (см. разд. 2).

Форма кривой на рис. 1 — ветви равновесий двухпороговой модели из численного примера разд. 2 (не схематический кубический fold). Для строгой S-складки с двумя устойчивыми внутренними ветвями нужна ещё и f(\infty)<0; наша модель даёт бистабильность нуля и x_b (см. замечание о классической S-складке в разд. 2). В обоих случаях — с одним или двумя порогами — бистабильность возникает при наличии петли повторов: это качественный механизм, а не конкретная форма кривой.

Однопараметрический срез

В общем случае поверхность равновесий живёт в пространстве параметров (R,\,k,\,x_{\mathrm{crit}},\,\mu_0,\,\alpha,\,\beta,\,\gamma) и устроена сложнее S-образной складки: при изменении нескольких параметров форма поверхности может меняться качественно. Для практических выводов статьи достаточно однопараметрической картины: мы рассматриваем складку как срез многомерного пространства по одному параметру \lambda_0 при фиксированных остальных. Именно поэтому защитные механизмы из разд. 8 формулируются как способы изменить саму форму складки (retry budget и backoff уменьшают R; shedding снижает эффективную \lambda_0 и возвращает рабочую точку в область, где складки нет), а не как способ её «обойти».

Фазовое пространство и два аттрактора

Система обладает бистабильностью, то есть имеет два устойчивых состояния равновесия (аттрактора), разделённых барьером:

  • Аттрактор 1 (здоровое состояние): низкая задержка, высокая пропускная способность, очередь запросов пуста.

  • Аттрактор 2 (отказ / livelock): задержка максимальна, полезный трафик равен нулю, очереди забиты до отказа.

Механизм перехода (как работает метастабильность)

  1. Всплеск (триггер): кратковременный рост внешнего трафика или сбой одного узла заставляет систему замедлиться.

  2. Петля обратной связи (сустейнер): клиенты начинают массово повторять таймаутнутые запросы. Повторы добавляются к реальному трафику.

  3. Смещение барьера: дополнительный объём повторов переводит систему через разделительный порог (сепаратрису) в «бассейн притяжения» второго аттрактора.

  4. Залипание: внутренний объём работы (повторы) полностью утилизирует ресурсы процессора и сети. Даже если внешний всплеск исчез, лавина внутренних повторов продолжает питать аттрактор отказа. Система не может вернуться в норму сама.

Сравнение состояний системы

Параметр

Аттрактор 1 (норма)

Аттрактор 2 (метастабильный отказ)

Нагрузка

Внешняя полезная

Внутренние повторы

Задержка (Latency)

Низкая (миллисекунды)

Стремится к бесконечности (таймауты)

Пропускная способность

Максимальная

Около нуля

Реакция на снятие триггера

Работает штатно

Система остаётся в отказе («залипла»)

Гистерезис проявляется в том, что при снятии триггера (возврате \lambda_0 к норме) система не возвращается на нижнюю ветвь: она заперта на верхней, потому что для перехода нужно либо пройти обратную точку складки (жёсткий сброс нагрузки), либо изменить топологию — разорвать петлю повторов.

2. Математическая модель метастабильного отказа

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

Базовое уравнение динамики очереди

Обозначим через x(t) текущую длину очереди запросов (или уровень нагрузки) в момент времени t. Изменение очереди во времени описывается уравнением

\frac{\mathrm{d}x}{\mathrm{d}t} = \lambda_{\mathrm{total}}(x) - \mu(x), где:

  • \lambda_{\mathrm{total}}(x) — суммарный входящий поток запросов (функция генерации работы);

  • \mu(x) — реальная скорость обработки запросов системой (функция мощности обслуживания).

Границы применимости

Fluid-ODE предполагает x \gg 1: очередь рассматривается как непрерывная величина. При x \to 0 модель теряет смысл — там работает дискретная рекурсия Линдли (разд. 3).

Повторы: функция генерации работы

При росте очереди x растёт задержка, и клиенты начинают генерировать повторные запросы (retries). Суммарный поток состоит из базового полезного трафика \lambda_0 и лавины повторов:

\lambda_{\mathrm{total}}(x) = \lambda_0 + R \cdot P_{\mathrm{timeout}}(x), \qquad P_{\mathrm{timeout}}(x) = \frac{1}{1 + e^{-k(x-x_{\mathrm{crit}})}}

Здесь R — предельная надбавка к базовому потоку от повторов (при полном насыщении сигмоиды). P_{\mathrm{timeout}}(x) — вероятность того, что запрос превысит таймаут: удобная модель поведения клиентов с фиксированным таймаутом. Вероятность превышения порога резко растёт, как только очередь переваливает через критический размер x_{\mathrm{crit}} (возможны и другие формы — ступенчатая, кусочно-линейная — с тем же качественным эффектом). При x \gg x_{\mathrm{crit}} вероятность стремится к 1, поток повторов насыщается на уровне \lambda_0 + R.

Деградация: функция обслуживания

В идеальной системе скорость обработки постоянна. Но при перегрузке (рост x) ресурсы процессора тратятся на накладные расходы: контекст-свитчинг, сборку мусора, lock contention, очистку буферов. Производительность падает. Мы используем феноменологическую аппроксимацию функции обслуживания \mu (x):

\mu(x) = \mu_0 \cdot \frac{1 + \gamma x}{1 + \alpha x^{\beta}}, \qquad \beta > 1, \gamma \ge 0

Это не универсальный физический закон планировщика ОС, а удобная кривая, которая ловит конкуренцию параллелизма и thrashing. Параметры: \mu_0 — предельная скорость обслуживания при x \to 0; \alpha — масштаб нелинейности; \beta > 1 — степень ускорения деградации с ростом x.

Параметр \gamma > 0 играет ключевую роль: он даёт слабый начальный рост \mu(x) (параллелизм), а дальше — падение. Функция f = \lambda_{\mathrm{total}} - \mu при этом становится немонотонной.

Важное уточнение о числе корней

С одной сигмоидой P(x) и одним горбом \mu(x) функция f имеет не более одного экстремума: производная f' = R\,k\,P(1-P) - \mu' представляет собой разность колокола и функции с единственной сменой знака, поэтому f' обнуляется не более одного раза. Это даёт не более двух корней f = 0 — но не классическую S-складку с тремя.

Полная S-складка с тремя корнями (два устойчивых равновесия и одно неустойчивое между ними) возникает при двух порогах срабатывания повторов: например, две группы клиентов с разными \tau, дающие две сигмоиды в \lambda_{\mathrm{total}}(x). Схематически ветви равновесий такой модели показаны на рис. 1; явный численный пример — ниже. В обоих случаях — с одним или двумя порогами — сама бистабильность возникает из-за петли повторов, а не из-за конкретной формы кривой: это ключевой качественный механизм.

Численный пример: три корня при двух сигмоидах

Утверждение о трёх корнях при двух порогах срабатывания повторов неочевидно; приведём явный пример. Возьмём \mu_0=1,\quad \gamma=1,\quad \alpha=0{,}1,\quad \beta=2, \lambda_0=0{,}5,\quad     R_1=1{,}5,\ k_1=10,\ x_{\mathrm{crit},1}=0{,}5,\quad     R_2=2{,}5,\ k_2=10,\ x_{\mathrm{crit},2}=3{,}0.

Тогда \lambda_{\mathrm{total}}(x)     = 0{,}5     + \frac{1{,}5}{1+e^{-10(x-0{,}5)}}     + \frac{2{,}5}{1+e^{-10(x-3{,}0)}},     \qquad     \mu(x)=\frac{1+x}{1+0{,}1\,x^{2}}.

Значения f(x)=\lambda_{\mathrm{total}}(x)-\mu(x):

x

\lambda_{\mathrm{total}}(x)

\mu(x)

f(x)

0{,}0

0{,}510

1{,}000

-0{,}490

0{,}5

1{,}250

1{,}463

-0{,}213

0{,}6

1{,}597

1{,}544

+0{,}053

1{,}0

1{,}990

1{,}818

+0{,}172

1{,}4

2{,}000

2{,}007

-0{,}007

2{,}0

2{,}000

2{,}143

-0{,}143

2{,}5

2{,}017

2{,}154

-0{,}137

2{,}75

2{,}190

2{,}135

+0{,}055

3{,}0

3{,}250

2{,}105

+1{,}145

Последовательность знаков f: -,\,+,\,-,\,+, то есть три смены знака и, по теореме о промежуточном значении, три корня f(x)=0 на интервалах (0{,}5;\,0{,}6), (1{,}0;\,1{,}4), (2{,}5;\,2{,}75); численно x_a\approx 0{,}575, x_b\approx 1{,}385, x_c\approx 2{,}715.

Об устойчивости трёх корней

Знак f' в окрестности корня определяет устойчивость равновесия \mathrm{d}x/\mathrm{d}t=f(x): f'<0 — устойчивое, f'>0 — неустойчивое. В нашем примере

f’(x_a)>0,\qquad f’(x_b)<0,\qquad f’(x_c)>0,

то есть картина «неустойчивое — устойчивое — неустойчивое». Это надо прочесть так. Устойчивое равновесие одно — x_b: это и есть «сбойный» аттрактор (аналог x_2 из разд. 1). Роль здорового аттрактора x_1 играет граница x=0: при x<x_a f<0, и очередь дренируется к нулю. Точка x_a — сепаратриса x_{\mathrm{sep}}: если триггер вытолкнул систему правее x_a, она сваливается в x_b. Третий корень x_c — дополнительное неустойчивое равновесие: выше него очередь растёт без ограничения.

При \gamma = 0 функция обслуживания \mu(x)монотонно убывает. Поскольку производная сигмоиды всегда положительна, функция fостается строго монотонной, и бистабильности не существует. При \gamma > 0 картина меняется: за счет немонотонности \mu(x) даже одна сигмоида при определенных параметрах может дать три корня. В этом случае график "колокола" производной повторов трижды пересекается с графиком производной обслуживания \mu'(x), что порождает три чередующихся знака функции f^{\prime }. Использование двухпороговой модели с двумя сигмоидами в численном примере ниже — это не обязательное условие для бистабильности, а лишь способ сделать аналитические границы бассейнов притяжения более выраженными и наглядными для инженера.

Замечание о «классической» S-складке

В нашем примере картина не полностью совпадает с классической кубической складкой, у которой два устойчивых внутренних равновесия разделены неустойчивым: для этого нужно f(\infty)<0, а в нашей модели f(\infty)=\lambda_0+R_1+R_2>0. Качественный механизм — наличие порога (сепаратрисы) и залипание в устойчивом равновесии — сохраняется; в терминах аттракторов система по-прежнему бистабильна (нуль и x_b), только первый аттрактор лежит на границе области, а не во внутренней точке. Буквальная S-складка с двумя устойчивыми внутренними равновесиями требует дополнительной нелинейности — например, насыщения повторов при x\to\infty или полки в \mu(x); это выходит за рамки статьи.

Инженерное примечание

В реальных ИТ-системах классическая бистабильность с двумя устойчивыми внутренними состояниями возникает даже при одной сигмоиде P_{\mathrm{timeout}}(x), если:

  • ёмкость очереди жёстко ограничена сверху (Q_{\max}): за этой границей поток обрезается, и f(x) приобретает разрыв;

  • функция деградации \mu(x) падает до нуля быстрее, чем насыщаются повторы: тогда у f появляется второй горб и, соответственно, два экстремума.

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

Математически при \gamma=0 имеем \mu'(x)<0 для всех x>0. Поскольку для сигмоиды \lambda_{\mathrm{total}}'(x)=R\,k\,P(1-P)\ge 0 всегда (сигмоида монотонно возрастает), получаем f'(x)=\lambda_{\mathrm{total}}'(x)-\mu'(x)>0 всюду: даже на плато, где \lambda_{\mathrm{total}}'\to 0, вклад -\mu'>0 сохраняет строгую монотонность f. Следовательно, больше одного корня f=0 быть не может: бистабильности в этой fluid-модели при \gamma=0 не существует. Немонотонность из-за конкуренции «параллелизм \leftrightarrow деградация» — необходимое условие складки; для полной S-складки с тремя корнями нужны ещё два порога повторов (см. выше).

Аттракторы: точки равновесия

Стационарные состояния системы — корни уравнения \lambda_{\mathrm{total}}(x) = \mu(x) (точки пересечения кривой притока и кривой оттока). Число корней зависит от формы кривых: в однопороговой модели (одна сигмоида P(x)) равновесие, как правило, единственное; для трёх равновесий нужны два порога срабатывания повторов (см. обсуждение выше).

Там, где три корня существуют, система качественно бистабильна: неустойчивый корень x_{\mathrm{sep}} (сепаратриса) разделяет бассейны притяжения здорового и сбойного режимов. Ниже — каноническая карта x_1 / x_{\mathrm{sep}} / x_2 (та же, что в разд. 1); как она ложится на численный пример с граничным аттрактором x=0 — сразу после списка.

  • Здоровый аттрактор x_1: низкая нагрузка. Если систему немного качнуть (небольшой всплеск), она вернётся в x_1.

  • Сепаратриса x_{\mathrm{sep}}: критическая точка перелома. Если триггер выталкивает нагрузку выше x_{\mathrm{sep}}, система необратимо сваливается в отказ.

  • Метастабильный отказ x_2: высокая нагрузка, лавина повторов, производительность упала. Даже если убрать внешний триггер (вернуть \lambda_0 к норме), система останется запертой в точке x_2.

В численном примере выше роль x_1 играет граница x=0 (аттрактор на краю области), роль x_2 — x_b; сепаратриса x_{\mathrm{sep}} совпадает с x_a. Классическая картина с внутренним x_1>0 требует дополнительной нелинейности (см. замечание о классической S-складке выше).

Сепаратриса x_{\mathrm{sep}} — не то же самое, что порог таймаута x_{\mathrm{crit}}: первая разделяет бассейны притяжения складки, вторая — порог срабатывания повторов.

Load shedding: математика выхода из аттрактора

Чтобы вернуть систему в здоровое состояние x_1, нужно временно изменить топологию фазового пространства — уничтожить аттрактор x_2. Это делается через сброс нагрузки (load shedding). Администраторы или алгоритмы принудительно режут входящий трафик до значения \lambda_{\mathrm{drop}}, при котором \lambda_{\mathrm{total}}(x)<\mu(x) для всех x от текущего значения очереди до x_1. Тогда \mathrm{d}x/\mathrm{d}t < 0 на всём пути к здоровому равновесию: очередь начинает разжиматься, и система «стекает» обратно. Как только очередь упала ниже x_{\mathrm{sep}}, жёсткое ограничение трафика можно снимать — система вернётся в здоровый аттрактор x_1.

Почему система остаётся в сбойном состоянии после снятия триггера

Система остаётся в сбойном состоянии из-за внутренней регенеративной петли обратной связи. Когда внешний триггер (всплеск \lambda_0) исчезает, накопленная внутри системы «энергия» (очередь запросов) продолжает сама себя воспроизводить. В уравнениях этот эффект удобно разложить на три аспекта одного механизма.

1. Повторы от «прошлого» трафика (память системы)

Даже если новый внешний трафик \lambda_0 вернулся к норме, в очереди x уже находится огромное количество запросов. Клиенты, чьи запросы встали в очередь во время всплеска, не дождались ответа и получили таймаут. Вероятность P_{\mathrm{timeout}}(x) для них близка к 1, потому что очередь физически превышает x_{\mathrm{crit}}. Слагаемое R\cdot P_{\mathrm{timeout}}(x) выдаёт свой максимальный вклад: потока R самого по себе достаточно, чтобы удерживать точку пересечения графиков в районе x_2.

2. Падение полезной мощности (феномен деградации)

Из-за гигантской очереди x реальная скорость \mu(x) упала до минимума. Сервер тратит почти все ресурсы CPU не на полезную работу, а на обслуживание самой перегрузки: прерывания от сети, переключение контекста между тысячами потоков, парсинг заголовков повторных запросов, которые он тут же откидывает по таймауту. Замкнутый круг: система обрабатывает слишком медленно, потому что очередь большая — очередь остаётся большой, потому что система обрабатывает слишком медленно.

3. «Бассейн притяжения» аттрактора

Исчезновение триггера лишь слегка смещает положение точки x_2, но не уничтожает сам аттрактор. Текущая координата x(t) уже находится глубоко внутри бассейна притяжения сбойного аттрактора — правее сепаратрисы x_{\mathrm{sep}}. Для возврата нужно преодолеть энергетический барьер, но у системы нет для этого внутренних ресурсов.

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

Что это даёт на практике

Три «кита» этого раздела — память системы, падение мощности и «бассейн притяжения» — это три разных повода для аварии, и лечатся они по-разному. Память разрывается клиентскими рычагами (backoff, retry budget); деградация — серверными (shedding: снять нагрузку, пока \mu(x) не восстановится); бассейн притяжения — только жёстким сбросом ниже сепаратрисы x_{\mathrm{sep}}. Если на инциденте вы видите только один из трёх признаков, этого недостаточно: sustaining-эффект держится одновременно на всех трёх, и защита должна закрывать каждый.

3. Переход к рекурсии Линдли

Если дифференциальное уравнение описывало систему непрерывно и макроскопически (fluid approximation), то рекурсия Линдли (Lindley’s recurrence) позволяет заглянуть на уровень дискретных запросов и связать время ожидания конкретного клиента с поведением системы в условиях очередей типа G/G/1.

Что такое рекурсия Линдли в базе

В классической теории очередей время ожидания W_n для n-го запроса (до начала его обработки) вычисляется исходя из времени ожидания предыдущего запроса:

W_n = \max(0, W_{n-1} + S_{n-1} - T_n),

где S_{n-1} — время обслуживания предыдущего запроса, T_n — интервал между приходом (n-1)-го и n-го запросов.

Оператор \max(0,\ldots) отражает нелинейность: если сервер простаивал (W_{n-1} + S_{n-1} < T_n), новый запрос обрабатывается мгновенно, а время ожидания равно нулю.

Как Линдли объясняет метастабильный отказ

В контексте метастабильности и повторов параметры S и T перестают быть независимыми случайными величинами. Они становятся эндогенными функциями состояния очереди W_{n-1}: зависят от бэклога и больше не являются независимыми внешними величинами.

Эффект повторов: изменение интервала прихода

Когда система здорова, T_n определяется только внешним трафиком. Но если W_{n-1} растёт и превышает клиентский таймаут \tau, клиент шлёт повтор. Интервал между входящими пакетами на сервере резко сокращается: T_n \to T_{\min}. Плотность входящего потока растёт, среднее \mathbb{E}[T] уменьшается.

Эффект деградации: изменение времени обслуживания

Из-за огромного количества параллельных запросов и накладных расходов (обработка повторов, которые в итоге будут отброшены) эффективное время полезного обслуживания S_{n-1} увеличивается. Каждому запросу достаётся меньше квантов CPU.

Фазовый переход в терминах рекурсии

Математическое условие стабильности очереди по Линдли: среднее время обслуживания должно быть меньше среднего интервала между запросами\mathbb{E}[S] < \mathbb{E}[T]
(плюс (S-T)_+=\max(0,\,S-T), где (S-T)_+=\max(0,\,S-T)).

Классическое условие \mathbb{E}[S]<\mathbb{E}[T] справедливо для изолированных систем с i.i.d. интервалами S_n и T_n; в условиях эндогенной зависимости (когда S и T становятся функциями бэклога W_{n-1}, см. выше) оно применяется локально как квазистационарное приближение, а не как строгий критерий стационарности марковской цепи.

В метастабильной системе из-за нелинейных петель обратной связи возникают две зоны стабильности:

  • Режим здорового аттрактора: в здоровом режиме \mathbb{E}[S] < \mathbb{E}[T] (нормальное обслуживание и внешний трафик). Случайное блуждание W_n постоянно прижимается оператором \max(0,\ldots) к нулю. Очередь стабильна и пуста.

  • Режим метастабильного отказа: система переходит порог, где \mathbb{E}[S] > \mathbb{E}[T] уже для деградированного обслуживания и потока с повторами. Теперь выражение под знаком максимума почти всегда положительно: W_n = W_{n-1} + (S_{n-1} - T_n). Поскольку математическое ожидание шага (S_{n-1} - T_n) > 0, случайное блуждание превращается в прямой детерминированный тренд вверх. Время ожидания W_n линейно устремляется к бесконечности (если буфер конечен — см. разд. 4).

Почему система не возвращается сама?

Даже если внешний источник уменьшает генерацию исходных задач (мы убрали триггер), накопленное гигантское значение W_{n-1} в рекурсии определяет поведение системы на много шагов вперёд. Пока бэклог настолько велик, что t_{k-1}+B_{k-1}-a_k>\tau для всех новых прибытий (обозначения t_k, B_k, a_k введены в разд. 4), каждый новый запрос таймаутит: сервер физически не успевает начать его обработку до a_k+\tau, клиенты генерируют T_n \to T_{\min}, и условие \mathbb{E}[S] > \mathbb{E}[T] поддерживается. Система «заперта» внутри рекурсивного роста.

Что это даёт на практике

Рекурсия Линдли превращает качественную картину «система залипла» в два числа, которые считаются из логов до аварии: среднее время обслуживания \mathbb{E}[S] и минимальный интервал прихода T_{\min}, который возникает при шторме повторов. Пока \mathbb{E}[S] < \mathbb{E}[T] с запасом — система в здоровом аттракторе. Как только T_{\min} уходит ниже \mathbb{E}[S], рекурсия превращается в линейный рост, и это и есть переход в сбойный режим. Оба числа измеряются на живом трафике; методика — ниже.

Как измерить по логам среднее время обслуживания и минимальный интервал прихода

Обе величины берутся с конкретного ingress-шлюза приложения (не усреднённо по кластеру) за скользящее окно 5-15 мин.

  1. Время обслуживания S. Для каждого запроса — время от начала его обработки на сервере до отправки ответа, без времени ожидания в очереди. Источник: отдельная метрика обработки (например, http_request_duration без queue wait) либо разность timestamp в access-логе. Если метрика включает и wait, и service — разделить, иначе \mathbb{E}[S] будет расти автоматически, и причина не отличится от следствия.

  2. Интервалы прихода T_n. Для каждой пары соседних принятых запросов — разность их timestamp прихода.

  3. Три окна. Разбить анализируемый период на «до инцидента», «во время», «после снятия триггера».

  4. Три числа на окно. \mathbb{E}[S] (или медиана S, если хвосты тяжёлые), \mathbb{E}[T] и 5-й перцентиль T_n — последний и есть T_{\min} (компромисс: 1-й перцентиль слишком шумный в коротком окне, 10-й теряет хвост).

  5. Тренды. Смотреть на скользящее окно, а не на средние за час: инцидент живёт минуты.

Что искать в числах

\mathbb{E}[S]/\mathbb{E}[T] \ll 1 — здоровый режим; \mathbb{E}[S]/\mathbb{E}[T] \to 1 (на практике выше 0{,}85) — приближение к сепаратрисе; \mathbb{E}[S] > \mathbb{E}[T] — сбойный аттрактор. Признак активного sustaining-эффекта — T_{\min} < \mathbb{E}[S]: даже самые «быстрые» приходы не успевают за обслуживанием, и каждый повтор продлевает петлю.

В примере разд. 5: \mathbb{E}[S] = 100 мс в норме и  мс в сбое; \mathbb{E}[T] = 120 мс; при \Delta_{\mathrm{retry}} = 50 мс T_{\min} \approx 50 мс. В норме \rho \approx 0{,}83; в сбое \mathbb{E}[S] > \mathbb{E}[T] и T_{\min} < \mathbb{E}[S] — оба индикатора срабатывают.

4. Модифицированная рекурсия: таймаут, drop и повторы

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

  • \tau — жёсткий таймаут ожидания в очереди: при t_k - a_k > \tau клиент закрывает соединение;

  • p — вероятность повторного запроса после таймаута;

  • \Delta_{\mathrm{retry}} — задержка перед повторной отправкой;

  • c_{\mathrm{drop}} — цена выброса: время CPU на приём и отбрасывание просроченной заявки.

Drop-on-timeout

Под временем ожидания здесь понимается время до начала обработки (t_k - a_k), а не до её завершения: таймаут отсекает заявку ещё в очереди. Мы моделируем сервер, который определяет просрочку при извлечении из очереди. c_{\mathrm{drop}} — это стоимость обработки просроченной заявки: чтение заголовков, запись в лог, инкремент метрик, отдача ответа, освобождение сокета. Для сервиса с логированием и метриками это порядка 10-20 мс, а не 1-3 мс: даже минимальный лог и одна запись в метрику съедают больше миллисекунды. В симуляции мы используем 20 мс как реалистичную оценку для сервиса с активным логированием. Если заявка k ждала начала обработки дольше \tau, клиент уже ушёл, а сервер всё равно платит c_{\mathrm{drop}}.

B_k =  \begin{cases}  d(t_k),  & t_k - a_k \le \tau \\        c_{\mathrm{drop}}, & t_k - a_k > \tau    \end{cases}

Здесь d(t_k) \in \{d,\, d_{\mathrm{bad}}\} — длительность полезной работы, зависящая от состояния зависимости в момент начала. Рекурсия на момент начала: \begin{equation}     t_k = \max(a_k,\, t_{k-1} + B_{k-1}). \end{equation}

Ключевой момент

Если c_{\mathrm{drop}} = 0 — модель восстанавливается сама: сервер не тратит CPU на мёртвые заявки, очередь дренируется. Именно c_{\mathrm{drop}} > 0 замыкает петлю: сервер платит за каждую просроченную заявку, и лавина повторов поддерживает себя. Сама по себе очистка по таймауту спасает систему при отсутствии повторных запросов; как только появляется шторм повторов, одной очистки уже недостаточно — петля снова замыкается.

Связь с fluid-моделью

Инженерный смысл c_{\mathrm{drop}}>0 в микро-модели: это стоимость бесполезной работы, которая в fluid-модели (разд. 2) соответствует снижению эффективной производительности \mu(x) — сервер тратит ресурсы на просроченные заявки, а не на полезные. Дискретная петля в Линдли и непрерывная деградация в ODE — две стороны одного механизма.

Шторм повторов (sustaining effect)

Клиентский таймаут срабатывает по часам клиента в момент a_k + \tau, независимо от того, когда сервер извлечёт просроченную заявку. С вероятностью p повтор планируется на a_k + \tau + \Delta_{\mathrm{retry}}. Поток прибытий перестаёт быть экзогенным: интервал до следующей заявки зависит от состояния очереди. Именно эта обратная связь превращает оператор \max в ловушку метастабильности.

5. Симуляция

Параметры штатного режима

Здесь d обозначает время обслуживания — то же, что S в разд. 4; индексы d и d_{\mathrm{bad}} — норма и режим сбоя.

Параметр

Значение

d / d_{\mathrm{bad}}

100/200 мс

\mathbb{E}[T]

120 мс (\rho \approx 0{,}83)

\tau

150 мс

c_{\mathrm{drop}}

20 мс

p (вероятность повтора)

0,9

\Delta_{\mathrm{retry}}

50 мс

Q_{\max}

40 заявок

Число прогонов

100

Порог по

Стоит отметить, что c_{\mathrm{drop}} не просто «немного больше нуля» замыкает петлю. При \rho=0{,}83, p=0{,}9, \Delta_{\mathrm{retry}}=50 мс устойчивая петля требует c_{\mathrm{drop}}\gtrsim 10 мс. Это прямо согласуется с оценкой «единицы–десятки мс»: если бы c_{\mathrm{drop}}=2 мс, сервер успевал бы дренировать буфер, и метастабильный отказ не наступал бы. Значение 20 мс в таблице — не «подгонка ради красивого графика», а реалистичный порядок величины выше этого порога. Порог проверен численно: скрипт lindley_sweep_cdrop.py прогоняет baseline-сценарий по 100 прогонов при c_{\mathrm{drop}} от 1 до 20 мс.

Результат свипа резкий: при c_{\mathrm{drop}} \le 10 мс система восстанавливается во всех 100 прогонах; при c_{\mathrm{drop}} = 12 мс восстанавливается 79/100; при c_{\mathrm{drop}} = 12 мс восстановление практически отсутствует. Переход сосредоточен в узкой полосе 10-15 мс, что подтверждает бистабильный характер отказа: граница между аттракторами резкая, а не размытая. Значение 20 мс выбрано с запасом над серединой перехода (\approx 12 мс), чтобы результат не зависел от джиттера.

Эта оценка согласуется с грубым аналитическим порогом. При \lambda_0 = 1/\mathbb{E}[T] \approx 8{,}3 rps и p=0{,}9 в полном коллапсе (все заявки таймаутят) суммарный поток на входе равен \lambda_{\mathrm{total}} = \lambda_0/(1-p) = \lambda_0\bigl(1 + p/(1-p)\bigr) \approx 83{,}3 rps: базовый поток и лавина повторов оба платят c_{\mathrm{drop}}. Сервер обрабатывает просроченные заявки со скоростью \approx 1/c_{\mathrm{drop}} заявок в секунду, поэтому дренирование возможно лишь при c_{\mathrm{drop}} \lesssim 1/83{,}3 \approx 12 мс (при c_{\mathrm{drop}} \gtrsim 12 мс система коллапсирует). Это совпадает с эмпирикой свипа: именно при c_{\mathrm{drop}}=12 мс восстанавливается 79/100 прогонов — на границе аналитического порога. Аналитическая оценка — нижняя граница: она не учитывает джиттер задержек повтора и переходные процессы, поэтому эмпирическая полоса перехода шире и сдвинута в сторону меньших c_{\mathrm{drop}}.

Рисунок 2. Свип по  в baseline-сценарии (100 прогонов на точку). Резкий переход между 10 и 15 мс подтверждает бистабильный характер метастабильного отказа: граница между «восстанавливается» и «не восстанавливается» не размазана. Значение 20 мс из таблицы параметров выше выбрано с запасом над серединой перехода.
Рисунок 2. Свип по c_{\mathrm{drop}} в baseline-сценарии (100 прогонов на точку). Резкий переход между 10 и 15 мс подтверждает бистабильный характер метастабильного отказа: граница между «восстанавливается» и «не восстанавливается» не размазана. Значение 20 мс из таблицы параметров выше выбрано с запасом над серединой перехода.

Сценарий

Окно сбоя — wall-clock 36\ldots48 с: функция обслуживания переключается на d_{\mathrm{bad}} = 200 мс. Пока очередь ещё пуста, первая заявка действительно обслуживается за d_{\mathrm{bad}} (таймаут \tau отсекает ожидание в очереди, t_k-a_k, а не полное время ответа). Но d_{\mathrm{bad}} > \mathbb{E}[T]=120 мс даёт \rho>1: бэклог растёт, и уже через доли секунды ожидание превышает \tau=150 мс. С этого момента почти все заявки уходят в ветку drop-on-timeout и платят B_k=c_{\mathrm{drop}}=20 мс вместо полезной работы, а клиенты взрывают поток повторами. То есть d_{\mathrm{bad}} — триггер набора бэклога; фаза сбоя на графиках поддерживается уже связкой c_{\mathrm{drop}}+повторы. После 48 с корень проблемы исчезает (d снова 100 мс), но клиенты агрессивно шлют повторы. Сценарий baseline в этой статье — drop-on-timeout с c_{\mathrm{drop}}>0 и фиксированной задержкой повтора \Delta_{\mathrm{retry}}, без backoff, retry budget и shedding (см. разд. 8). Ось X на рис. 3 — время в секундах (0\ldots120).

Почему drop-on-timeout остаётся в baseline

Drop-on-timeout сам по себе — простейший механизм защиты от перегрузки: сервер не тратит CPU на просроченные заявки, и очередь дренируется. Мы оставляем его в baseline сознательно, чтобы изолировать эффект клиентской политики повторов: при c_{\mathrm{drop}} = 0 модель восстанавливается сама (см. разд. 4), и роль повторов была бы не видна. Такой выбор отделяет клиентский рычаг (backoff/jitter/budget) от серверного (shedding) и делает сравнение защит в разд. 8 честным.

Результаты

На основе моделирования получается классическая картина метастабильного отказа:

  • до сбоя ожидание \approx 0 мс (очереди нет);

  • во время сбоя очередь быстро растёт: триггер d_{\mathrm{bad}}=200 мс > \mathbb{E}[T]=120 мс даёт \rho>1; как только ожидание превышает \tau=150 мс, заявки уходят в drop-on-timeout (c_{\mathrm{drop}}=20 мс), а лавина повторов раздувает входящий поток далеко за \lambda_0;

  • после снятия триггера система не возвращается к нулю: кривые остаются у верхней границы окна до конца симуляции. Хотя сервер снова готов работать за 100 мс, таймауты продолжают порождать повторные, поток раздувается далеко за число честных заявок, опустошения до нуля нет — потому что каждый повтор порождает c_{\mathrm{drop}} и подпитывает петлю.

Рисунок 3. Baseline: drop-on-timeout и фиксированная задержка повтора без backoff/budget/shedding. После устранения сбоя (синяя пунктирная линия,  с) очередь не рассасывается — sustaining effect шторма запросов. Ось ординат обрезана на  мс (та же шкала, что на рис. 5 и 6); в baseline очередь уходит за эту границу и не возвращается. Сравнивать следует поведение после снятия триггера (вертикаль ) и сводку в табл. 1.
Рисунок 3. Baseline: drop-on-timeout и фиксированная задержка повтора без backoff/budget/shedding. После устранения сбоя (синяя пунктирная линия, \approx 48 с) очередь не рассасывается — sustaining effect шторма запросов. Ось ординат обрезана на  мс (та же шкала, что на рис. 5 и 6); в baseline очередь уходит за эту границу и не возвращается. Сравнивать следует поведение после снятия триггера (вертикаль T_{\mathrm{rec}}) и сводку в табл. 1.

Итог фазы без защиты. Внешний триггер исчез, сервер снова готов работать за 100 мс, но перегруженная очередь сама генерирует избыточный входящий поток. Нужны либо клиентские (задержка / джиттер), либо серверные (сброс нагрузки / предохранитель) разрывы контура.

Воспроизводимость

Скрипты симуляции (генератор Линдли с drop-on-timeout, свип по c_{\mathrm{drop}}, ансамбли по 100 прогонов для baseline, backoff+jitter+budget и shedding) и исходные данные прогонов доступны в репозитории https://github.com/redbeardster/metastable.

Фрагмент: оценка виртуального ожидания (FIFO + drop).

def _service_at(start):
    return D_BAD if FAULT_START <= start <= FAULT_END else D_OK

def offer_wait(now, server_free, buffer, tau):
    t_free = server_free
    for arr, _rcnt in buffer:
        start = max(t_free, arr)
        t_free = start + (C_DROP if start - arr > tau
                          else _service_at(start))
    return max(0.0, t_free - now)

6. Тропическая математика: без мистики

Раздел можно пропустить при первом чтении: практический вывод сведён к процедуре в конце этого раздела («оставить не больше p_{\mathrm{crit}} трафика на самом жёстком цикле»). Аппарат ({\max},+) приведён здесь, потому что он даёт язык для сетевого анализа петель повторов и объясняет, почему одна лишь маскировка связей не спасает.

Рекурсия Линдли содержит оператор \max. В тропической алгебре (\max,+) (idempotent / tropical semirings) он становится линейным: нелинейная рекурсия превращается в линейную систему, к которой применим аппарат собственных значений и собственных векторов. Этот аппарат позволяет описывать метастабильные отказы через спектр матрицы переходов: знак тропического собственного значения задаёт, растёт очередь или релаксирует к нулю.

Переход к тропическому полукольцу

В тропическом полукольце (\max,+) операции a\oplus b=\max(a,b) и a\otimes b=a+b превращают рекурсию Линдли в линейное уравнение

W_n = (A_{n-1}\otimes W_{n-1})\oplus e,

где A_{n-1}=S_{n-1}-T_n, e=0 — тропическая единица. Нейтральный элемент по \oplus — -\infty (в терминах топологии сети это отсутствие ребра между узлами); для одного сервера критерий роста сводится к знаку S-T, содержательный спектр появляется только для сети узлов.

Обозначения

Ниже \lambda — тропическое собственное значение (maximum cycle mean), а не интенсивность входящего потока из ODE/Kingman. Коэффициент загрузки сервера по-прежнему \rho. Тропический спектральный радиус матрицы обозначаем \rho_{\max}; в одномерном случае \rho_{\max}=\lambda.

Сеть сервисов: где Max-Plus уместен

Аппарат матриц A и \rho_{\max} осмыслен для сети дискретно-событийных узлов (тандем, синхронизации) в детерминированном худшем случае. Пример 2\times 2 ниже — именно сеть, а не замена модели G/G/1. Он не доказывает стохастическую метастабильность; он оценивает, когда детерминированный цикл перегрузок растёт (\rho_{\max}>0) и какую долю трафика нужно срезать, чтобы \rho_{\max}<0.

Представим два связанных микросервиса в состоянии метастабильного отказа. Пусть узел 1 имеет время обслуживания S_1=5{,}5 мс при собственном интервале прихода T_1=1{,}5 мс, а узел 2 — S_2=6{,}5 мс и T_2=0{,}5 мс. В обоих случаях S_i > T_i: каждый узел сам себя перегружает.

Элемент A_{ij} = S_i - T_j — чистое превышение обслуживания над приходом на ребре j \to i: положительное значение означает, что бэклог узла i растёт, когда триггер приходит от узла j. Диагональные элементы — само-петли узлов, внедиагональные — перекрёстные потоки: узел 1 вызывает узел 2 и наоборот. Подставляя числа, получаем матрицу тропических переходов

A_{\mathrm{fault}} = \begin{pmatrix} S_1 - T_1 & S_1 - T_2 \\ S_2 - T_1 & S_2 - T_2 \end{pmatrix} = \begin{pmatrix} 4 & 5 \\ 5 & 6 \end{pmatrix}

Тождество A_{11}+A_{22}=A_{12}+A_{21}=10 — это просто суммарное S_1+S_2 - T_1 - T_2; оно выполняется автоматически для любой матрицы вида A_{ij}=S_i - T_j. Циклы:

  • цикл 1–1: вес 4, средний вес 4;

  • цикл 2–2: вес 6, средний вес 6;

  • цикл 1–2–1 (взаимное влияние): вес 5+5=10, средний вес 5.

Максимум \lambda = 6 > 0 достигается на цикле 2–2: это доминирующая петля обратной связи, по которой узел 2 сам себя подпитывает. При каждой итерации (один полный круг по доминирующему циклу — «событие» в терминологии Max-Plus) вектор очередей в среднем прибавляет 6 единиц времени, то есть система находится в аттракторе отказа и сама себя перегружает. Именно эту петлю — или её эквивалент в реальной системе — и нужно разрывать: защиты из разд. 8 снижают долю сохраняемого трафика p настолько, чтобы максимум по всем циклам стал отрицательным.

Физический смысл

Тропическое собственное значение — это доминирующая петля обратной связи: цикл с наибольшим средним весом S_\Sigma-T_\Sigma на ребро. Знак \lambda задаёт асимптотику очереди: \lambda > 0 — бэклог растёт линейно со скоростью \lambda за событие, \lambda < 0 — дренируется, \lambda = 0 — критический режим. Единицы \lambda — те же, что у S и T (в примере — мс).

Тот же цикл длины  с S_1+S_2=12 мс и T_1+T_2=2 мс даёт mean cycle (12-2)/2=5 — один из порогов в разд. 6.3; финальный p_{\mathrm{crit}} берётся как минимум по всем циклам.

Критическая доля сброса

Критическую долю сброса p_{\mathrm{crit}} не стоит путать с загрузкой \rho и с тропическим \rho_{\max}: здесь p_{\mathrm{crit}} — доля сохраняемого трафика, при которой \rho_{\max}=0 (ниже — сброс 1-p).

Пусть p — доля сохраняемого трафика. Когда мы отбрасываем часть запросов, средний интервал между теми, которые сервер всё-таки берёт в обработку, растёт как T(p)=T_{\mathrm{fault}}/p. Для цикла C длины |C| с суммарным service S_\Sigma и суммарным interarrival T_\Sigma тропический радиус равен

\rho_C(p) = \frac{S_\Sigma - \bigl(T_\Sigma / p\bigr)}{|C|},

а условие \rho_C(p) < 0 равносильно p < T_\Sigma/S_\Sigma: каждому циклу соответствует свой порог дренирования.

Поскольку \rho_{\max} — это максимум по всем циклам (разд. 6), доля сохраняемого трафика должна удовлетворять самому жёсткому из этих порогов:

p_{\mathrm{crit}} = \min_C \frac{T_\Sigma(C)}{S_\Sigma(C)}.

Доминирующий цикл (в примере — цикл 2–2) задаёт нижнюю границу.

Пример

Для того же сценария, что в разделе выше:

  • цикл 1–1: порог T_1/S_1 = 1{,}5/5{,}5 \approx 27{,}3\%;

  • цикл 2–2: порог T_2/S_2 = 0{,}5/6{,}5 \approx 7{,}7\%;

  • цикл 1–2–1: порог (T_1+T_2)/(S_1+S_2) = 2/12 \approx 16{,}7\%.

Минимум — p_{\mathrm{crit}}\approx 7{,}7\%, задаваемый доминирующей петлёй 2–2. Это верхняя граница доли сохраняемого трафика: сбросить придётся \approx 92{,}3\%, то есть агрессивнее, чем требуется для перекрёстного цикла в одиночку.

Важно: если бы мы ориентировались только на цикл 1–2–1 (порог 16{,}7\%), при p=10\% доминирующая петля 2–2 оставалась бы положительной, и \rho_{\max} не стал бы отрицательным. Оценка должна идти по самому жёсткому циклу.

Физический смысл и подстановка чисел

Если p > 7{,}7\% (пропускаем слишком много трафика), \rho_{\max}>0 — очереди продолжат расти, сброс неэффективен. Если p < 7{,}7\% — все циклы, включая доминирующий 2–2, становятся отрицательными: уравнение Линдли превращается в сжимающее отображение, и вектор очередей «стекает» к нулю.

Подставим числа в рекурсию Линдли на доминирующем цикле 2–2. При S_2=6{,}5 мс и T_2=0{,}5 мс эффективный интервал между принятыми заявками равен T_2/p, а дрейф одного шага:

A(p) = S_2 - \frac{T_2}{p}.

Три режима:

p

T_2/p, мс

A(p), мс

Режим

10\%

5{,}0

+1{,}5

рост бэклога

7{,}7\%

6{,}5

\approx 0

критический режим

5\%

10{,}0

-3{,}5

дренирование

При p=5\% рекурсия принимает вид \begin{equation}     W_n = \max\bigl(0,\; W_{n-1} - 3{,}5~\text{мс}\bigr), \end{equation}то есть каждое событие срезает с бэклога фиксированные 3{,}5 мс. Из насыщенного состояния W_0=800 мс очередь дренируется за \lceil 800/3{,}5 \rceil = 229 событий. При p=10\% знак A противоположен, и та же рекурсия даёт линейный рост W_n = W_{n-1} + 1{,}5 мс.

Как только очереди опустели, можно безопасно вернуть p=1.

Реальная доля сброса обычно выше этой нижней границы: джиттер (разброс интервалов прихода/обслуживания) и тяжёлые хвосты требуют запаса, а не точного равенства p=p_{\mathrm{crit}}.

Важно: разрыв одного ребра не спасает (элемент в матрице — отсутствие дуги). После удаления дуги матрица принимает вид

A’ = \begin{pmatrix} 4 & -\infty \\ 5 & 6 \end{pmatrix}, \quad \lambda = \max(4,6) = 6 > 0

— очередь продолжает расти. Нужен реальный спад локальных (сброс нагрузки на узлах), а не только маскирование связей.

Рисунок 4. Спектральный радиус  для трёх циклов из разд. 6 при , , ,  мс. Красная кривая (цикл 2–2) пересекает ноль раньше всех и задаёт . При  все три цикла отрицательны, и очередь дренируется.
Рисунок 4. Спектральный радиус \rho_C(p) для трёх циклов из разд. 6 при S_1=5{,}5, T_1=1{,}5, S_2=6{,}5, T_2=0{,}5 мс. Красная кривая (цикл 2–2) пересекает ноль раньше всех и задаёт p_{\mathrm{crit}}\approx 7{,}7\%. При p<p_{\mathrm{crit}} все три цикла отрицательны, и очередь дренируется.

Инженерное резюме

Тропическая алгебра даёт инженеру три вещи, которых нет ни в fluid-ODE (разд. 2), ни в дискретно-событийной симуляции (разд. 5). Во-первых, узкое место в сети сервисов — это цикл обратной связи, а не отдельный узел: два микросервиса, вызывающих друг друга с повторами, образуют петлю, которая живёт своей жизнью и не видна при анализе по одному узлу. Во-вторых, знак доминирующего собственного значения \lambda (максимум по средним весам циклов) за одну операцию отвечает на вопрос «система растёт или дренируется». Отличие от классического спектра: там \|x(n)\|\sim\rho^n (экспоненциальный рост), здесь x(n)\sim\lambda n (линейный) — для очередей естественна именно линейная метрика. В-третьих, критическая доля сброса p_{\mathrm{crit}} вычисляется по логам, без запуска симуляции.

Практическая процедура

  1. Построить граф вызовов между сервисами с рёбрами A_{ij}=S_i-T_j: S_i — среднее время обслуживания узла i, T_j — средний интервал между принятыми заявками от узла j.

  2. Найти простые циклы (на практике — длины 2-3; длиннее уже редкость).

  3. Для каждого цикла вычислить p_C = T_\Sigma(C)/S_\Sigma(C).

  4. p_{\mathrm{crit}} = \min_C p_C; сбросить не менее 1-p_{\mathrm{crit}} трафика.

  5. Вернуть нагрузку только после того, как очереди опустеют.

Главный вывод на инженерном языке: если узел сам себя перегружает через собственную петлю повторов, он задаёт порог сброса жёстче, чем перекрёстные циклы, — сбрасывать надо по самому жёсткому циклу, а не «по среднему по сети». В нашем примере это \approx 92{,}3\%, а не \approx 83{,}3\%, как подсказала бы наивная оценка по перекрёстному циклу 1–2–1. Именно поэтому adaptive concurrency control в проде держит нагрузку существенно ниже половины входного трафика: порог p_{\mathrm{crit}}\approx 7{,}7\% из этого примера задаёт нижнюю границу, а с запасом на джиттер и тяжёлые хвосты практический ориентир оказывается в районе 10-20%.

Оценка p_{\mathrm{crit}} — детерминированная, worst-case, и даёт нижнюю границу доли сброса; реальная доля выше из-за джиттера и тяжёлых хвостов. Стохастический аналог — формула Кингмана: она даёт среднюю картину в открытой системе без обратной связи. Обе модели дополняют друг друга: тропический анализ отвечает на вопрос «что и на сколько рвать», Кингман — «где мы стоим по загрузке прямо сейчас».

7. Формула Кингмана: количественный компас

До сих пор мы двигались в мире абстрактной математики: дифференциальные уравнения, рекурсия Линдли, тропические полукольца, спектральные радиусы. Это дало качественное понимание метастабильности — как возникает бистабильность и гистерезис, что сепаратриса x_{\mathrm{sep}} — та точка, после которой система не возвращается сама, как оценивать критическую долю сброса p_{\mathrm{crit}}.

Но всё это — качественные или полуколичественные модели. Они показывают механизм, но не дают простого способа ежедневно мониторить реальную систему и отвечать на вопросы: насколько я близок к сепаратрисе прямо сейчас? Какие у меня C_a^2 и C_s^2 в часы пик? При каком росте трафика я пересеку опасную зону?

Для ответа нужна теория массового обслуживания — модель G/G/1 и формула Кингмана:

W_q \approx \frac{\rho}{1-\rho} \cdot \frac{C_a^2 + C_s^2}{2} \cdot \mathbb{E}[S],

где \rho — загрузка, C_a^2, C_s^2 — квадраты коэффициентов вариации интервалов прихода и времени обслуживания, \mathbb{E}[S] — среднее время обслуживания. Именно эта формула становится компасом, который превращает абстрактную теорию в повседневный инструмент SRE-инженера.

Что такое i.i.d. и почему это важно.

Формула Кингмана опирается на предположение, что интервалы прихода \Delta t_i = t_i - t_{i-1} — это i.i.d. (independent and identically distributed). Термин содержит два независимых требования.

  • Independent (независимые): величина \Delta t_i не зависит от предыдущих \Delta t_{i-1}, \Delta t_{i-2}, \ldots Знание истории не помогает предсказать следующий интервал.

  • Identically distributed (одинаково распределённые): все \Delta t_i имеют одно и то же распределение, не меняющееся во времени и не зависящее от состояния системы.

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

Повторы нарушают оба требования сразу: клиенты реагируют на задержку (при превышении \tau шлют повтор), поэтому \Delta t_i зависит от состояния очереди — нарушена независимость; интенсивность входного потока растёт вместе с деградацией — нарушена одинаковость распределения. Именно поэтому формула Кингмана не описывает sustaining-эффект даже при малой \rho: в ней нет обратной связи от бэклога к входящему потоку.

Область применимости

Формула Кингмана справедлива при \rho < 1, i.i.d. интервалах прихода (см. выше) и конечных вторых моментах T и S. Инженерно это означает: она корректна в открытой системе без обратной связи. Пока повторов нет, поток внешний, и W_q — это функция загрузки.

Она теряет применимость в двух случаях.

  • Загрузка на грани: при \rho \to 1 множитель \rho/(1-\rho) уходит в бесконечность, оценка W_q теряет физический смысл. На практике при \rho \gtrsim 0{,}85 формула уже даёт завышенные значения, а поведение системы описывается не стационарной аппроксимацией, а динамикой (см. разд. 2).

  • Эндогенный поток: как только появляются повторы, параметры S и T перестают быть независимыми случайными величинами (см. разд. 4), i.i.d. нарушено. Формула не описывает sustaining-эффект даже при малой \rho, потому что в ней нет обратной связи от бэклога к входящему потоку. Это не «немного неверная формула», а качественно не тот инструмент.

Именно поэтому в примере из разд. 7 (при \rho \geq 3{,}2) формула уже неприменима. Для таких сценариев работают рычаги из разд. 8: retry budget и shedding, а не расчёт W_q по Кингману.

Сравнение моделей

Модель

C_a^2

C_s^2

W_q

M/M/1

1

1

\dfrac{\rho}{1-\rho}\cdot\mathbb{E}[S]

M/G/1

1

произв.

\dfrac{\rho}{1-\rho}\cdot\dfrac{1+C_s^2}{2}\cdot\mathbb{E}[S]

G/G/1 (Кингман)

произв.

произв.

\dfrac{\rho}{1-\rho}\cdot\dfrac{C_a^2+C_s^2}{2}\cdot\mathbb{E}[S]

M/M/1 — частный случай M/G/1 при C_s^2=1 и одновременно частный случай аппроксимации Кингмана при C_a^2=C_s^2=1. Формула Поллачека–Хинчина для M/G/1 точна; формула Кингмана для G/G/1 — приближение.

Как читать по логам

Как увидеть угрозу в своих логах до того, как она превратится в аварию.

  1. Собрать интервалы \Delta t_i = t_i - t_{i-1}, посчитать C_a^2 = (\sigma_{\Delta t} / \mathbb{E}[\Delta t])^2. Если C_a^2\approx 1 — поток пуассоновский; если C_a^2\gg 1 — трафик идёт пачками (первый признак угрозы).

  2. Собрать времена обслуживания S_i, посчитать C_s^2 = (\sigma_S / \mathbb{E}[S])^2. Если C_s^2\approx 0 — все запросы одинаковы; если C_s^2>1 (а иногда 10 или 50) — «тяжёлые» запросы блокируют быстрые.

  3. Если C_a^2 + C_s^2 > 4 и \rho > 0{,}85 — система в красной зоне. Порог  — эмпирическое «правило большого пальца» SRE-команд, а не теорема. При такой вариативности и высокой загрузке любой сбой сети на  мс с высокой вероятностью запустит лавину повторов. Правило контекстно-зависимо: если клиентский таймаут \tau многократно превышает среднее время обслуживания \mathbb{E}[S] (\tau/\mathbb{E}[S] \gg 10), лавина повторов может не запуститься даже при высокой вариативности.

Важно для распределённых систем

C_a^2 нужно измерять на конкретном ingress-шлюзе приложения, а не усреднять по кластеру в Prometheus. При нескольких шлюзах потоки на каждом независимы, и общая C_a^2 окажется ниже, чем на любом отдельном: для оценки лавины повторов ориентироваться следует на худший (наиболее пачечный) из шлюзов, а не на усреднённый показатель. Балансировщик (round-robin, least-connections) искусственно сглаживает пачки трафика и маскирует thundering herd.

Что важно понимать: множитель растёт гиперболически, а не экспоненциально. При он равен 9, при — уже 49, то есть в 5,4 раза больше при росте нагрузки всего на 8 п.п. Это и есть «крутой обрыв»: любой микро-всплеск мгновенно превращает очередь в бесконечную.

Пример

Сервис заказов с нормальным потоком 80 rps и временем обработки 10 мс (\rho = 0{,}8). Из-за блокировки БД время обработки выросло до 13 мс (\rho = 1{,}04). Клиенты с 3 повторами (то есть до 4 попыток на запрос) дают до 320 rps — при номинальном S это \rho \ge 3{,}2; при актуальных 13 мс ещё выше. Система парализована, хотя база данных уже восстановилась. При \rho \geq 3{,}2 формула Кингмана неприменима дважды: \rho > 1 нарушает условие сходимости, а повторы делают поток эндогенным — теряется i.i.d. Именно поэтому здесь важны не расчёты W_q, а рычаги из разд. 8: retry budget и shedding.

8. Защитные механизмы

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

Клиентская защита: backoff, jitter, бюджет повторов

Фиксированная \Delta_{\mathrm{retry}} = 50 мс синхронизирует повторы в плотные пачки. Экспоненциальная задержка (exponential backoff) с full jitter (Brooker, AWS Builders’ Library) разносит повторные попытки по времени: \mathrm{Delay}_{\mathrm{base}} = \mathrm{Base} \cdot 2^n, \mathrm{Delay}_{\mathrm{final}} = \mathrm{Uniform}(0, \mathrm{Delay}_{\mathrm{base}})(с потолком MaxBackoff). Это уменьшает C_a^2 и возвращает \mathbb{E}[S] < \mathbb{E}[T].

Retry budget. Лимит попыток обязателен: 3 попытки = 1 исходная + 2 повтора. Без него вложенные повторы на разных уровнях стека перемножаются, и один пользовательский запрос порождает десятки внутренних.

Почему это спасает рекурсию: экспонента отодвигает повторные попытки далеко вперёд; джиттер уничтожает синхронизацию («пульсацию») и размазывает паразитный трафик; средняя задержка между повторными становится больше d = 100 мс, оператор \max перестаёт накапливать бэклог, буфер очищается.

Результат симуляции: медиана времени восстановления по 100 прогонам T_{\mathrm{rec}}\approx 0{,}9 с, goodput на хвосте \approx 100\%, ценой abandon \approx 50 транзакций на прогон. Drop-on-timeout с c_{\mathrm{drop}}>0 не отключается: меняется только клиентская политика повторов (backoff + jitter + budget).

Рисунок 5. Backoff + full jitter + budget. Ось X — wall-clock время  с; ось ординат — до 800 мс (общая шкала с рис. 3 и 6). Пурпурная вертикаль — медиана  после снятия триггера. Сводное сравнение по времени восстановления — в табл. 1.
Рисунок 5. Backoff + full jitter + budget. Ось X — wall-clock время 0\ldots120 с; ось ординат — до 800 мс (общая шкала с рис. 3 и 6). Пурпурная вертикаль — медиана T_{\mathrm{rec}} после снятия триггера. Сводное сравнение по времени восстановления — в табл. 1.

Серверная защита: shedding и circuit breaker

Клиентский backoff — «хороший тон», но чужих клиентов не контролируют. Сервер обязан защищать себя сам. Сброс нагрузки защищает сам сервис от перегрузки, а предохранитель — от каскадных сбоев при обращении к проблемным зависимостям (БД, кэш и т. п.): он изолирует внешний вызов, который всё равно упадёт, и даёт зависимости время восстановиться.

Load shedding. Порог W_{\max} < \tau (100 < 150 мс), чтобы сброс срабатывал раньше, чем клиент сдастся. Оценивает offer wait с учётом FIFO-бэклога. Отдаёт 503 без клиентского повтора — иначе плотность ошибок умножится на стоимость обработки, и gate запрётся сам. В терминах формулы Кингмана shedding снижает \rho и не даёт C_s^2 вырасти за счёт бесполезной работы. Затраты прокси на fail-fast (в коде — константа c_{\mathrm{fast}}\approx 2 мс) в модели не бронируются на тред-пуле приложения (server_free): gate сидит перед очередью сервиса и отвечает 503 без занятия воркера.

Ограничение модели (клиенты и 503)

Симуляция предполагает, что клиент различает таймаут (\tau) и отказ прокси (503): на 503 повтор не планируется, транзакция сразу уходит в abandon. На практике это идеализация: сторонние библиотеки и «глупые» клиенты нередко шлют повторы и на 503, и тогда shedding переносит петлю на уровень прокси. В проде это закрывается retry budget на клиенте и/или заголовком Retry-After; в модели такой сценарий не воспроизводится.

Circuit breaker. Следит за долей ошибок и таймаутов в скользящем окне. При превышении — Open, вызовы к проблемной зависимости превентивно отвергаются, не тратя локальные ресурсы на заведомо неуспешные попытки. Circuit breaker обнуляет \lambda_{\mathrm{retry}} и даёт время на восстановление.

Результат симуляции: медиана времени восстановления по 100 прогонам T_{\mathrm{rec}}\approx 0{,}1 с, goodput \approx 100\%, без abandon. Drop-on-timeout сохраняется; добавляется load shedding на прокси.

Про circuit breaker

В этой статье CB упоминается как дополнительный механизм изоляции проблемной зависимости — концептуально. В симуляции реализован только load shedding на прокси; для разрыва петли повторов этого достаточно, а CB в проде добавляется поверх того же gate.

Рисунок 5. Load shedding (прокси, 503 без повтора). Ось X — wall-clock время  с; ось ординат — до  мс (общая шкала с рис. 3 и 5). Пурпурная вертикаль — медиана  после снятия триггера. Сброс нагрузки ограничивает рост ожидания порогом ; после снятия триггера очередь за конечное число шагов возвращается к нулю. Сводное сравнение — в табл. 1.
Рисунок 5. Load shedding (прокси, 503 без повтора). Ось X — wall-clock время 0\ldots120 с; ось ординат — до  мс (общая шкала с рис. 3 и 5). Пурпурная вертикаль — медиана T_{\mathrm{rec}} после снятия триггера. Сброс нагрузки ограничивает рост ожидания порогом W_{\max}; после снятия триггера очередь за конечное число шагов возвращается к нулю. Сводное сравнение — в табл. 1.

Сравнение

Таблица 1 суммирует ансамбль из 100 прогонов. Колонка «Восстановление» — число прогонов с восстановлением (из 100). Колонка T_{\mathrm{rec}} — медиана времени восстановления по прогонам, в которых восстановление зафиксировано; для baseline значение не определено (восстановления нет).

Таблица 1. Сравнение сценариев защиты по 100 прогонам. Baseline — сценарий без backoff/budget/shedding; drop-on-timeout включён.

Сценарий

Восстановление

Goodput хвост

T_{\mathrm{rec}} (медиана)

Baseline (без backoff/budget/shedding)

0/100

\approx 0\%

—

Backoff + jitter + budget

100/100

\approx 100\%

\approx 0{,}9 с

Shedding (прокси, без повтора)

100/100

\approx 100\%

\approx 0{,}1 с

Итог: обе защиты возвращают систему в норму, но с разной ценой. Backoff сохраняет больше транзакций, shedding — быстрее восстанавливает сервер.

Почему  мс в профиле DET

Если генератор квазидетерминированный (120 \pm 2 мс), S = 100 мс, а T_{\min} = 118 мс > S, сервер гарантированно успевает «остыть». Формально W_q = 0, но микро-всплески на границе T_{\min} дают среднее \approx 0{,}023 мс — на два порядка меньше шага дискретизации.

При пуассоновском приходе (та же \rho) DES даёт \approx 240 мс — в согласии с аналитическим M/D/1 \approx 250 мс. Порог W_{\max} нужно калибровать по реальному закону прихода: если DET-настройки выкатить в пуассоновский прод, gate будет отдавать ложные 503 ещё до аварии.

Этот раздел подтверждает, что все эффекты разд. 5 вызваны именно сбоем, а не законом прихода: в отсутствие сбоя система ведёт себя штатно при обоих режимах генератора.

Полевые наблюдения Huang et al. (OSDI ’22) показывают, что большинство метастабильных отказов в проде развивается именно по сценарию «повторы после таймаута»: диагностика по goodput/badput и долям таймаутов в скользящем окне — первый практический шаг.

Промышленные рычаги

Эти механизмы уже реализованы в промышленных прокси и планировщиках; ниже — что именно искать в конфигурации.

  • Envoy / Istio: лимиты пула соединений, outlier ejection, retry budgets, circuit breaking — cool-down вместо бесконечного буфера.

  • Adaptive Concurrency / Adaptive Token Bucket — держать перегрузку на прокси до приложения, нащупывая p_{\mathrm{crit}} в реальном времени.

  • CoDel / fq_codel (RFC 8289) — на qdisc ядра; не путать с очередью приложения.

  • Resilience4j (наследник идей Hystrix) — CircuitBreaker, RateLimiter, Bulkhead, Retry в JVM-сервисах.

Задача инженера — не «изобрести backoff», а согласованно включить эти рычаги, измерить \rho и вариативность и в chaos-прогоне проверить, что удаление триггера действительно возвращает систему в норму.

9. Что осталось за рамками

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

Формальная верификация контракта восстановления (TLA^+, Timed CCS, model checking) — отдельная тема: на конечных моделях проверяют контракт \Phi_{\mathrm{rec}}(T) — «после снятия триггера система за время T возвращается в здоровый режим». Эти методы не заменяют эмпирическую проверку, но без них легко чинить симптомы вместо причины; с ними — целенаправленно устранять метастабильные контуры.

Без моделирования — или хотя бы без ответа на вопрос «что произойдёт с системой при изменении одного или нескольких параметров?» — инженер превращается в шамана.

Чек-лист архитектора

  • Собирайте логи и регулярно вычисляйте C_a^2 и C_s^2.

  • Мониторьте \rho и W_q через формулу Кингмана.

  • Внедряйте exponential backoff с full jitter на всех клиентах.

  • Ставьте лимит попыток (retry budget) — один, общий, на всех уровнях стека.

  • Ставьте circuit breaker на все межсервисные вызовы (Istio / Envoy / Resilience4j).

  • Настраивайте load shedding с W_{\max} < \tau.

  • Настраивайте CoDel (RFC 8289) для внутренних очередей.

  • Применяйте адаптивные лимиты (adaptive concurrency) на входе.

  • Проводите chaos-эксперименты: инъекция задержки \to снятие триггера \to проверка контракта восстановления.

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

Комментарии (3)


  1. wanderlustpork
    07.10.2026 09:53

    Если в численном примере раздела 2 положить R₂ = 0, у f(x) остаются три корня (≈0,577; 1,381; 3,618). Зачем тогда вторая сигмоида, и как это согласуется с утверждением, что f′ обнуляется не более одного раза?


  1. ElDark
    07.10.2026 09:53

    Хабр, который мы потеряли)
    P.S. Ссылка на репо не открывается.


    1. Basis_Habr
      07.10.2026 09:53

      Ссылку поправили, спасибо.