Системное мышление Разборы: инцидент, техдолг, найм и очередь как системы
0%

Разборы: инцидент, техдолг, найм и очередь как системы

Разборы: инцидент, техдолг, найм и очередь как системы

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

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

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

  1. Очередь — модель количественная, опровергается за час нагрузочного теста.
  2. Инцидент — проверяется наполовину: механику можно воспроизвести, организационную петлю вокруг неё — только косвенно.
  3. Техдолг — прямых измерений нет вообще, есть заместители разной степени честности.
  4. Найм — данных мало, конфаундеров много, часть выводов честнее оставить гипотезой.

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

Одна структура, четыре масштаба времени

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

Обнаружение, действие и эффект для четырёх разборов на логарифмической оси времени

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

Протокол разбора: семь шагов и стоп-правило

Расшифровка шагов — с типичным плохим ответом рядом, потому что плохие ответы устойчивее хороших.

Шаг Хороший ответ Плохой ответ (и почему он плохой)
1. Референсный режим «p99 растёт с 40 до 300 мс за 3–4 дня после релиза, потом падает; так было в марте, мае и июле» «Всё тормозит» — нет ни величины, ни формы, ни повторяемости, а значит, нечего объяснять
2. Граница и горизонт «Внутри: сервис заказов, его пул к БД, клиентские ретраи. Снаружи: платёжка. Горизонт — квартал» «Система — это наш продукт». Причина всегда окажется внутри границы, поэтому неназванная граница = предопределённый вывод
3. Запасы «Очередь в пуле, шт.; неисправленные первопричины, шт.; эффективные инженеры, FTE» Список сущностей без единиц. Если единицы не пишутся, это не запас, а слово
4. Петли «R1: латентность → ретраи → нагрузка → латентность. B1: лимит конкурентности → нагрузка» Стрелки без знаков. Схема без полярностей не предсказывает ничего, ей нельзя противоречить
5. Задержки «Обнаружение 4 мин (окно алерта), действие 10 мин (раскатка), эффект 15 мин (прогрев кэша)» «Реагируем быстро». Ровно эта неопределённость и порождает колебания
6. Предсказание «Если это ретраи, отношение downstream/upstream RPS в инциденте будет > 1.5. Если ≈ 1.0 — гипотеза мертва» «Модель объясняет всё, что мы видели». Объяснение задним числом — не проверка
7. Вмешательство «Лимит конкурентности 60, флаг, откат одной командой, ретро 12 августа» «Повысить внимание к качеству» — не переменная, не проверяется, не откатывается

Стоп-правило. Разбор прекращают и переходят к обычной инженерии, если выполняется хотя бы одно:

  • событие единично — второго случая нет, форма поведения во времени не построена;
  • нет ни одной переменной, которую вы уже измеряете или сможете начать измерять за две недели;
  • известное локальное исправление дешевле любого структурного и не создаёт зависимости (индекс в БД, увеличенный пул, исправленный if).

Третий пункт особенно важен: системное мышление — не обязанность. Если у вас упал сервис из-за опечатки в конфиге, правильный ответ — починить опечатку, а не рисовать контуры. Схема нужна тогда, когда починка уже была и не помогла.

Разбор 1: очередь, которая растёт при неизменной нагрузке

Сцена. Сервис держит 85 rps при паспортной ёмкости 100 rps. Три месяца всё стабильно: p99 около 70 мс. Затем за две недели p99 уезжает до 900 мс, а на дашборде «Запросы в секунду» — те же 85. Первая реакция команды: «нагрузка не выросла, значит, что-то сломали в коде».

Шаги 1–3: режим, граница, запасы

  • Референсный режим: плавный рост p99 за 10–14 дней при плоском RPS, дважды за полгода, оба раза после роста объёма данных.
  • Граница: сервис + пул соединений + кэш + реплика БД; клиентские ретраи внутри (они влияют на приток). Горизонт — месяц. Таймер меряет от входа в балансировщик: если мерить внутри обработчика, очередь на входе окажется снаружи границы измерения, и вы будете смотреть на график, где всё в порядке (границы).
  • Запасы: длина очереди в пуле (шт.), занятые соединения (шт.), размер рабочего набора кэша (МБ).

Арифметика, которая закрывает половину гипотез

Загрузка $\rho = \lambda / \mu$ — отношение притока к ёмкости. Для простейшей модели M/M/1 среднее время ожидания в очереди равно $W_q = \dfrac{\rho}{1 - \rho} \cdot \dfrac{1}{\mu}$. При $\lambda = 85$, $\mu = 100$: $\rho = 0{,}85$, $W_q = 57$ мс. Теперь ключевой ход разбора: RPS не изменился, но изменилась ёмкость. Кэш попадал в 92 % случаев, стал попадать в 85 % — рабочий набор перерос память. Если промах стоит примерно в 8 раз дороже попадания, средняя стоимость запроса выросла с $0{,}92 + 0{,}08 \cdot 8 = 1{,}56$ до $0{,}85 + 0{,}15 \cdot 8 = 2{,}05$, то есть на 31 %. Ёмкость падает пропорционально: $\mu’ = 100 \cdot 1{,}56/2{,}05 = 76$ rps.

И тогда $\rho’ = 85/76 = 1{,}12 > 1$. Очередь растёт неограниченно, пока не упрётся в лимит пула. Приток не изменился ни на процент — изменился знаменатель. Семь процентных пунктов hit rate превратились в отказ обслуживания; это ровно та нелинейность из главы 6, где «чуть больше» меняет режим.

Вторая поправка — вариабельность. Приближение Кингмана для G/G/1 даёт $W_q \approx \dfrac{\rho}{1 - \rho} \cdot \dfrac{c_a^2 + c_s^2}{2} \cdot \dfrac{1}{\mu}$, где $c_a$ и $c_s$ — коэффициенты вариации интервалов прихода и времени обслуживания. Пуассоновский поток даёт $c_a^2 = 1$; реальный трафик с батчами от планировщиков легко даёт 2–3. При $c_a^2 = 2$, $c_s^2 = 1$ ожидание умножается на 1,5 при той же загрузке. Отсюда практический вывод: сглаживание входа (джиттер в крон-задачах, размазывание ретраев) снижает латентность, не трогая ни код, ни железо.

Третий инструмент — закон Литтла $L = \lambda W$: он связывает три величины и потому работает проверкой приборов. Если дашборд показывает 300 запросов в полёте при 85 rps, то среднее время в системе — $300/85 = 3{,}5$ с. Видите на соседнем графике 200 мс? Один из двух графиков врёт, и это надо выяснить до всякого моделирования.

# Где на самом деле узкое место. Вход: (имя, ёмкость rps, ca2, cs2).
# Сложность: O(n) по времени, O(1) по дополнительной памяти.
def bottleneck(stages, lam):
    out = []
    for name, mu, ca2, cs2 in stages:
        rho = lam / mu
        wq = float("inf") if rho >= 1 else (rho / (1 - rho)) * ((ca2 + cs2) / 2) / mu
        out.append((name, rho, wq))
    total = sum(w for _, _, w in out if w < float("inf"))
    for name, rho, wq in out:
        print(f"{name:<9} rho={rho:.2f}  Wq={wq * 1000:7.1f} мс  доля={100 * wq / total:.0f}%")
    return out


bottleneck([("gateway", 400, 1.0, 1.0), ("app", 120, 2.0, 1.5),
            ("db-read", 95, 1.0, 4.0),  # тяжёлый хвост запросов: cs2 велик
            ("cache", 2000, 1.0, 1.0)], lam=85)

Результат разоблачает типичную локальную оптимизацию: app при $\rho = 0{,}71$ выглядит «нагруженным» и его идут оптимизировать, но 86 % ожидания сидит в db-read, где $\rho = 0{,}89$ и огромный $c_s^2$. Ускорение app вдвое снимает 35 мс из 260 — заметный труд ради 12 % результата, тогда как сокращение хвоста тяжёлых запросов к базе даёт кратный эффект. Это локальная оптимизация в чистом виде: выбирают стадию по ощущению «нагружена», а не по вкладу в сквозное время.

Шаг 4: где здесь петля

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

Три усиливающие петли и две уравновешивающие, которые появляются только если вы их построили. R3 — самая коварная: очередь занимает память, память давит на GC, GC отъедает CPU, ёмкость падает, очередь растёт. Ёмкость перестаёт быть константой и становится функцией состояния — а вся арифметика выше молча считала её постоянной.

Шаг 6: четыре предсказания и их опровержения

Гипотеза Предсказание Что её опровергает
Дело в загрузке ($\rho \to 1$) Латентность и глубина очереди растут вместе, $\rho$ по узкой стадии > 0,85 $\rho < 0{,}7$ на всех стадиях при высокой латентности → дело в блокировках или вариабельности, а не в загрузке
Дело в вариабельности Гистограмма интервалов прихода имеет всплески; $c_a^2 > 2$ $c_a^2 \approx 1$ и ровный поток → ищите ёмкость
Дело в падении ёмкости Hit rate / план запроса / размер рабочего набора менялись до роста p99 Ёмкость на синтетике не изменилась → причина во входе
Есть петля усиления Отношение downstream RPS к upstream RPS растёт вместе с латентностью Отношение стабильно ≈ 1,0 → петли нет, модель массового обслуживания достаточна

Ловушка измерения, которая ломает четвёртую проверку. Закрытый нагрузочный генератор (фиксированное число виртуальных пользователей, каждый ждёт ответа) физически не может воспроизвести R1 и R2: он не отправит следующий запрос, пока не получит предыдущий, поэтому приток автоматически падает вместе с ёмкостью. На закрытой модели система выглядит устойчивой, а в проде опрокидывается. Проверять петли усиления можно только открытой моделью с заданным rps и клиентской логикой ретраев (нагрузочное тестирование, тестирование производительности). Это, пожалуй, самый частый способ получить «зелёный» тест и красный прод.

Шаг 7: вмешательства по возрастанию рычага

Вмешательство Что меняет Сила и цена
Добавить реплики Параметр $\mu$ Низкая: $\rho$ вернётся к прежнему через квартал роста; стоит денег линейно
Сгладить входной поток $c_a^2$ Средняя, эффект мгновенный, почти бесплатно
Лимит конкурентности Добавляет петлю B1 Высокая: структурно ограничивает R1; часть запросов получает быстрый отказ — это фича
Приоритетный сброс Добавляет B2 с целью Высокая, переживает рост нагрузки; нужна классификация трафика
Убрать зависимость ёмкости от очереди Разрывает R3 Наивысшая; дорого — пересмотр модели памяти и батчинга

Сверху вниз — это движение по шкале рычагов от параметра к структуре петли. Подробности реализации — устойчивость и деградация, кэширование и Handling Overload в Google SRE Book.

Отдельно про масштабирование числом воркеров. Универсальный закон масштабируемости Гантера (Neil Gunther) даёт $C(N) = N / (1 + \alpha (N-1) + \beta N (N-1))$, где $\alpha$ — доля несмасштабируемой работы, $\beta$ — цена взаимных согласований. Максимум достигается при $N^{\ast} = \sqrt{(1 - \alpha)/\beta}$: при $\alpha = 0{,}03$ и $\beta = 0{,}0005$ это 44 воркера, после чего пропускная способность падает. Модель эмпирическая, коэффициенты подгоняются под измерения, но она даёт то, чего не даёт словесная схема: точку, после которой «добавим ещё инстансов» становится вредом.

Разбор 2: инцидент, который «починили» и который вернулся

Сцена. 14:02 — алерт на 5xx. 14:09 — дежурный перезапускает поды по раннбуку. 14:17 — метрики в норме. Постмортем: «причина — всплеск трафика от партнёра, добавили алерт на 5xx раньше». Через три недели то же самое, ещё через две — снова. Раннбук за квартал выполнили одиннадцать раз, каждый раз успешно. Формально всё хорошо: MTTR 15 минут, инциденты закрываются. Фактически команда построила процесс, зависящий от костыля, и структура, порождающая инциденты, не тронута.

Шаг 1: референсный режим важнее последнего случая

Один инцидент — событие. Одиннадцать инцидентов с одинаковой сигнатурой — режим, и разбирать надо его. Признаки одной сигнатуры: совпадает форма графика (резкий рост 5xx с плато, а не пик), совпадает эффективное лечение (перезапуск помогает, откат — нет), совпадает предшествующий сигнал (короткий всплеск трафика за 30–90 секунд до). Именно здесь ломается «пять почему»: метод строит одну причинную цепь для одного случая и не имеет средств отличить причину от совпадения. Про его слабости прямо пишет Postmortem Culture в SRE Book и подробно — Сидни Деккер в «The Field Guide to Understanding Human Error». Для повторяющихся инцидентов нужна не цепь, а контур.

Шаги 2–5: границы, запасы, петли

Ключевая деталь на диаграмме — не техническая. Успешная митигация снимает давление, которое одно только и могло привести к структурному исправлению. Тикет P3 — не халатность, а рациональный выбор внутри границы «квартальные цели команды».

B1 работает быстро и надёжно, петля CAP работает медленно и через приоритеты. Классический перенос проблемы из каталога архетипов: быстрое решение вытесняет медленное, а способность к медленному атрофируется. Разница с главой 9 здесь в том, что мы не распознаём архетип, а измеряем его — см. предсказания ниже.

Механика: почему система не вернулась сама

Всплеск длился 20 секунд, а деградация — 15 минут. Это признак метастабильного отказа: система имеет два устойчивых состояния при одной и той же нагрузке, и после толчка остаётся в плохом (Bronson et al., HotOS 2021; полевое исследование — Metastable Failures in the Wild, OSDI 2022).

Условие бистабильности при ретраях с коэффициентом $r$ формулируется в одну строку: $\lambda < \mu < \lambda (1 + r)$. Слева — «в норме справляемся», справа — «при полном шторме ретраев не справляемся». При $\lambda = 70$, $\mu = 100$, $r = 2$ оба неравенства выполнены: 70 < 100 < 210. Значит, ловушка существует при 70 % загрузки и трёхкратном запасе по ретраям — то, что интуиция считает безопасным.

import math

LAM, MU, R = 70.0, 100.0, 2.0   # приток rps, ёмкость rps, число повторов
T, W = 0.30, 0.05               # таймаут клиента, с; ширина перехода
QMAX, DT = 2000.0, 0.05         # предел очереди, шаг интегрирования


def step(q, lam):
    """Доля таймаутов растёт с ожиданием q/MU, каждый таймаут даёт R повторов."""
    p = 1.0 / (1.0 + math.exp(-((q / MU) - T) / W))
    return min(QMAX, max(0.0, q + (lam * (1.0 + R * p) - MU) * DT))


def run(shed, horizon=900.0):
    """Всплеск x3 на 20 с, затем сброс доли shed. Возвращает время выхода, с."""
    q, t = 0.0, 0.0
    while t < 20.0:                                  # триггер
        q = step(q, LAM * 3.0); t += DT
    t, recovered = 0.0, None
    while t < horizon:                               # триггера уже нет
        q = step(q, LAM * (1 - shed)); t += DT
        if recovered is None and q < 10.0:
            recovered = t
    return recovered


for s in (0.0, 0.3, 0.5, 0.52, 0.55, 0.6, 0.7):
    r = run(s)
    print(f"сброс {s:>4.0%}: {'не вышли за 15 мин' if r is None else f'выход за {r:.0f} с'}")
# 0 %, 30 %, 50 %, 52 % — не вышли за 15 минут
# 55 % — выход за 356 с; 60 % — за 123 с; 70 % — за 53 с

Сложность: $O(H/\Delta t)$ по времени на прогон, $O(1)$ по памяти; подбор минимального сброса бисекцией добавляет множитель $O(\log(1/\varepsilon))$.

Три вывода, которых нет в словесном описании инцидента:

  1. Порог выхода считается заранее: стационарное условие $\lambda (1 - s) (1 + r) < \mu$ даёт $s > 1 - \mu / ((1 + r)\lambda) = 52{,}4$ %. Сбросить 30 % — потратить время впустую.
  2. Стационарного порога мало. На границе 52 % очередь дренируется со скоростью около нуля. Чтобы уложиться в 15 минут, нужно 55 %, чтобы в две — 60 %. У плана восстановления должно быть число и срок, а не «снизим нагрузку».
  3. Асимметрия входа и выхода. Вошли при мгновенных 210 rps, выходим при 28 rps — гистерезис шириной почти в семь раз (нелинейность). Перезапуск подов, кстати, — это и есть сброс на 100 %: он работает не потому, что «поды протухли», а потому что обнуляет очередь.

Шаг 6: что проверяем

Техническая часть (проверяема за один нагрузочный тест):

  • отношение downstream/upstream RPS во время инцидента > 1,5 → ретраи участвуют; ≈ 1,0 → нет, ищите другую причину;
  • открытая нагрузочная модель воспроизводит незатухающее состояние после снятия всплеска → метастабильность подтверждена;
  • уменьшение клиентского таймаута увеличивает нагрузку на зависимость (больше отказов → больше повторов) — контринтуитивное предсказание, которое отличает петлю ретраев от простой перегрузки.

Организационная часть (проверяема за квартал, косвенно):

  • счётчик выполнений раннбука по месяцам растёт монотонно;
  • медианный возраст тикетов класса «структурное исправление» растёт;
  • доля инцидентов, закрытых ручной митигацией без изменения кода, растёт.

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

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

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

Разбор 3: техдолг как запас, а не как ругательство

Сцена. Команда третий квартал подряд не попадает в сроки. Оценки не завышены — просто каждая задача упирается в «сначала надо разобраться в модуле тарифов». Предложение выделить 20 % времени на рефакторинг откладывается формулировкой «сделаем, когда станет посвободнее».

Что именно считать переменной

Уорд Каннингем, придумавший метафору в отчёте OOPSLA'92, имел в виду не грязный код, а расхождение между структурой кода и текущим пониманием предметной области: долг создаётся не только спешкой, но и обучением — вы узнали про домен больше, и вчерашняя правильная структура стала неправильной. Рабочее определение для разбора: техдолг — запас, единица измерения которого — дополнительные часы на внесение типового изменения. Такое определение сразу даёт притоки и оттоки и позволяет спросить «сколько», а не «плохо ли».

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

Петли и почему «когда посвободнее» не наступает

R1: долг → медленнее изменения → меньше времени → больше обходов → долг. Усиливающая петля, у которой нет естественного ограничителя, кроме полной остановки поставки. B1: явно выделенная доля времени → отток долга. Уравновешивающая петля, которая включается только решением и потому первой отключается под давлением.

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

def run(months=24, share=0.20, hurry=0.0, cap=100.0, d0=400.0, k=0.25, repay=1.5):
    """share — доля мощности на долг; hurry — надбавка к притоку от спешки;
    cap — часы в месяц; d0 — долг, при котором эффективность падает вдвое.
    Сложность: O(months) по времени и по памяти."""
    debt, shipped, hist = 400.0, 0.0, []
    for m in range(1, months + 1):
        eff = 1.0 / (1.0 + debt / d0)             # эффективность падает с долгом
        feature_hours = cap * (1.0 - share)
        shipped += feature_hours * eff            # реально поставленная ценность
        debt = max(0.0, debt + k * feature_hours * (1.0 + hurry) - repay * cap * share)
        hist.append((m, round(debt), round(shipped)))
    return hist


a = run(share=0.20, hurry=0.0)   # дисциплина: 20 % всегда
b = run(share=0.02, hurry=0.5)   # «когда посвободнее»: 2 % и спешка
for m in (1, 5, 10, 24):
    print(f"мес {m:>2}: A долг={a[m-1][1]:>4} итог={a[m-1][2]:>4} | "
          f"B долг={b[m-1][1]:>4} итог={b[m-1][2]:>4}")
# мес  1: A долг= 390 итог=  40 | B долг= 434 итог=  49
# мес  5: A долг= 350 итог= 205 | B долг= 569 итог= 227
# мес 10: A долг= 300 итог= 425 | B долг= 738 итог= 416
# мес 24: A долг= 160 итог=1133 | B долг=1210 итог= 825

Форма результата устойчива к разумным изменениям коэффициентов: политика B выигрывает первые месяцы, по месячной выработке проигрывает с 5-го месяца, по накопленному итогу — с 10-го. Отсюда единственный честный вывод: спор «чинить ли долг» — это спор о горизонте, и пока горизонт не назван вслух, обе стороны правы внутри своих границ. Проверять надо не убеждения, а коэффициенты $k$, repay и $d_0$ — и они измеримы.

Приоритизация: где чинить

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

Данные для горизонтальной оси берутся из git одной командой, вертикальная — любой метрикой сложности (цикломатическая, глубина вложенности, длина файла):

# Топ-20 файлов по числу коммитов за год — кандидаты в «горячие точки»
git log --since="1 year ago" --name-only --pretty=format: \
  | grep -E '\.(py|go|ts|java|cs)$' \
  | sort | uniq -c | sort -rn | head -20

Подход известен как hotspot-анализ (Адам Торнхилл, «Your Code as a Crime Scene»). Статус доказательности: это эвристика приоритизации с корреляционной поддержкой, а не закон. В работе Code Red: The Business Impact of Code Quality (Tornhill, Borg, 2022) на 39 проприетарных кодовых базах в «красном» коде обнаружено в разы больше дефектов и существенно большее время закрытия задач — но это наблюдательное исследование: код с высокой сложностью систематически отличается и другими свойствами (возраст, критичность, число авторов), поэтому причинность из него не следует.

Шаг 6: что проверяем, и что опровергает

Гипотеза Предсказание Что опровергает
Долг — узкое место потока Lead time в горячих модулях систематически выше при сопоставимом размере диффа Разницы нет → узкое место в другом месте (согласования, тесты, ревью)
Спешка порождает долг Доля срочных обходов коррелирует с ростом lead time с лагом 1–3 месяца Кросс-корреляция плоская → приток идёт не от спешки, а от дрейфа домена
Ремонт даёт эффект После квартала работы по горячей точке lead time именно в ней падает Не сдвинулось → чинили не то, либо долг не был ограничением
Эффективность падает с долгом Время «разобраться перед изменением» растёт быстрее объёма кода Растёт пропорционально объёму → это масштаб, а не долг

Заместители нужно выбирать осознанно: lead time, доля переделок, доля инцидентов, локализованных в топ-5 % файлов по частоте изменений. Каждый из них — прокси, и каждый портится, если сделать его целью (локальная оптимизация, метрики). Управленческая сторона темы — техдолг в треке «Инженерное лидерство».

Ловушка: костыль, который стал процессом

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

Разбор 4: найм — мощность как запас с длинной задержкой

Сцена. Бэклог растёт четыре месяца, команда из 20 инженеров не справляется, принято решение нанять десять. Через год в команде 26 человек, а ощущение перегрузки то же. Это самый слабый по проверяемости разбор, и с этого надо начинать разговор, а не заканчивать.

Запасы, потоки, задержки

Запас — эффективные FTE, не головы. Притоки: выход новых людей (после задержки «решение → оффер → выход» в 4–6 месяцев). Оттоки: увольнения. Плюс два вычета, которые обычно не считают: неполный вклад новичка в период рампапа и время наставника.

RAMP = [0.0, 0.25, 0.5, 0.75]   # вклад новичка по месяцам 1..4, дальше 1.0
MENTOR, MENTOR_M = 0.3, 2       # доля времени наставника и её длительность


def plan(n0, schedule, attrition_m, months=12):
    """schedule: {месяц: сколько человек вышло}; attrition_m — отток в месяц.
    Возвращает (месяц, головы, эффективные FTE). Сложность O(months * hires)."""
    n, ages, out = float(n0), [], []
    for m in range(1, months + 1):
        n -= n * attrition_m
        ages = [a + 1 for a in ages]
        for _ in range(schedule.get(m, 0)):
            ages.append(0); n += 1
        deficit = sum(RAMP[a] - 1.0 for a in ages if a < len(RAMP))   # рампап
        mentoring = -MENTOR * sum(1 for a in ages if a < MENTOR_M)    # наставничество
        out.append((m, round(n, 1), round(n + deficit + mentoring, 1)))
    return out


for m, heads, fte in plan(20, {i: 1 for i in range(1, 11)}, attrition_m=0.0125):
    print(f"мес {m:>2}: голов {heads:>5}  эффективных FTE {fte:>5}")
# мес  1: голов  20.8  эффективных FTE  19.5
# мес  2: голов  21.5  эффективных FTE  19.1
# мес  4: голов  22.9  эффективных FTE  19.8
# мес  5: голов  23.7  эффективных FTE  20.6
# мес 12: голов  26.4  эффективных FTE  25.7

Первые четыре месяца интенсивного найма мощность ниже стартовой: 19,1 FTE против 20 в худшей точке. Десять нанятых превращаются в 6,4 прироста голов, потому что отток 15 % годовых съедает 3,6 человека. И это модель без координационных издержек: если добавить рост числа связей (закон Брукса и число пар $n(n-1)/2$), картина ухудшится ещё.

Петли

R1 — усиливающая петля перегрузки: чем тяжелее, тем больше уходят, тем тяжелее. B1 — найм, но с задержкой в полгода и с отрицательной начальной фазой. R2 — координационные издержки. R3 — трагедия общего ресурса в чистом инженерном виде: интервьюеры общие, каждая команда рационально просит больше слотов, суммарная нагрузка на интервьюеров растёт, качество отбора падает у всех, а плохие наймы возвращаются в R1. Ресурс общий, выигрыш — внутри границы команды, издержка — снаружи.

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

Что здесь можно измерить, а что нет

Измеримо и полезно:

  • кривая рампапа: время до первого самостоятельного изменения в проде, до первого самостоятельного дежурства, до закрытия первого инцидента без помощи. Три точки — уже кривая, и по ней виден эффект изменений в онбординге (онбординг);
  • нагрузка дежурства на человека (страниц в неделю, доля ночных) и её кросс-корреляция с увольнениями через 2–4 месяца: если R1 доминирует, лаговая корреляция будет видна;
  • доля синхронного времени в календаре: если растёт быстрее численности, R2 работает;
  • распределение оценок интервьюеров и число интервью на человека в неделю — прямая проверка R3.

Не измеримо в вашей команде и остаётся гипотезой: величина «закона Брукса» вообще. Сам закон — обобщение опыта OS/360 из «Мифического человеко-месяца» Брукса (1975), а не измеренная закономерность; он сильно зависит от связности задач и разваливается там, где работа хорошо разделяется. Правильная формулировка для разбора: «в нашей структуре задачи связны, поэтому ожидаем эффект; проверим по lead time потока сеньора, у которого забрали время на наставничество». Если lead time этого потока не изменился — гипотеза о налоге на онбординг у вас не подтверждается.

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

Что общего у четырёх разборов

Очередь Инцидент Техдолг Найм
Запас Заявки в очереди, шт. Неисправленные первопричины, шт. Доп. часы на изменение Эффективные FTE
Доминирующая петля R: ретраи и деградация ёмкости R: зависимость от костыля R: спешка кормит спешку R: перегрузка кормит отток
Длина контура Секунды Часы → недели Кварталы Год
Типовая локальная оптимизация Ускорили не то звено Улучшили MTTR, не трогая причину Переписали красивый, но холодный модуль Наняли в самый громкий отдел
Главная проверка $\rho$ по стадиям, открытый нагрузочный тест Отношение downstream/upstream RPS Lead time в горячих точках Кривая рампапа и лаговая корреляция оттока
Рычаг Лимит конкурентности и сброс Убрать зависимость от ручной митигации Явная доля мощности + приоритет по горячим точкам Снизить связность задач и защитить наставничество
Проверяемость Высокая Средняя Низкая, только заместители Очень низкая

Три инварианта, которые видны только при сравнении:

  1. Симптом всегда там, где смотрят; причина — там, где не смотрят. Очередь видна на графике латентности, а её причина — в hit rate кэша. Инцидент виден в 5xx, а его причина — в приоритете тикета P3.
  2. Быстрая петля вытесняет медленную. Перезапуск вытесняет лимит конкурентности, обход вытесняет рефакторинг, найм вытесняет упрощение работы. Это не слабость людей, а свойство структуры: быстрая петля успевает замкнуться раньше.
  3. Рычаг почти всегда меняет структуру или информацию, а не усилие. Ни в одном из четырёх разборов сильнейшее вмешательство не имеет вида «делать то же самое, но старательнее».

Пределы метода: где эти разборы перестают работать

Честный список того, чего вы не получили:

  • Схема не даёт величин. Полярности говорят «вырастет», модель говорит «вырастет на столько-то при таких-то коэффициентах». Коэффициенты берутся из измерений, и если их нет, диаграмма предсказывает только знак — иногда этого достаточно, чаще нет.
  • Границу выбрали вы. Любой из четырёх разборов можно расширить (очередь → вся цепочка сервисов, найм → рынок труда) и получить другой вывод. Это не дефект метода, а его условие использования: граница должна быть записана и обоснована (глава 2).
  • Одна структура согласуется со многими данными. Совпадение формы графика с формой, которую даёт петля, — слабое свидетельство: тот же график порождается тремя-четырьмя разными структурами. Различать их можно только вмешательством, а не наблюдением (причинные ошибки).
  • Организационные петли почти невозможно опровергнуть чисто. Нельзя провести контрольную группу «команда без раннбука». Отсюда правило: организационные утверждения формулировать как гипотезы с указанием, каким наблюдением вы бы от них отказались, — и всё равно держать в голове, что вывод останется слабым.
  • Красивая схема — не результат. Диаграмма, из которой не следует ни одного числа, ни одного эксперимента и ни одного вмешательства с датой, — это иллюстрация к разговору. Её нормально нарисовать, ненормально — принимать по ней решения (глава 12 и глава 13).

Как записывать разбор, чтобы через полгода его можно было проверить

Разбор, который нельзя перепроверить, через два квартала превращается в фольклор. Минимальная запись — шесть полей, и два из них обязательны:

  • симптом и референсный режим — что наблюдаем, с каких пор, какой формы;
  • граница и горизонт — что внутри, что объявлено внешним, на каком сроке;
  • структура-гипотеза — петли, запасы, задержки, со знаками;
  • предсказание — метрика, направление и опровергается, если (обязательное поле);
  • срок проверки — дата, к которой наблюдение будет внесено (обязательное поле);
  • вмешательство — что меняем, какой это рычаг, обратимо ли, когда ретроспектива.

Без двух обязательных полей запись не отличается от протокола совещания. Формат неважен — вики или ADR рядом с архитектурными решениями; важно, что через квартал кто-то откроет и впишет наблюдение, в том числе опровергающее.

Честно про доказательную базу

Материал этой главы неоднороден по надёжности, и смешивать уровни нельзя.

Прочное (математика и воспроизводимые эксперименты). Закон Литтла (Little, 1961) — доказанная теорема при очень слабых предположениях. Формулы M/M/1 и приближение Кингмана — стандартная теория массового обслуживания с известными границами применимости. Метастабильные отказы воспроизводятся в лаборатории и описаны на полевых данных крупных операторов. Экспериментальные результаты Стермана о систематических ошибках людей в задачах с накоплением и задержкой (пивная игра, Management Science, 1989) многократно реплицированы.

Среднее (наблюдательные данные, корреляции). Hotspot-анализ и связь сложности кода с дефектами: корреляции на реальных кодовых базах, причинность не установлена. Метрики DORA («Accelerate», Форсгрен, Хамбл, Ким) — опросные данные с самоотчётом; полезны как ориентир, но это не эксперименты, и переносимость выводов на конкретную команду не гарантирована.

Слабое (обобщение опыта, кейсы). Закон Брукса, закон Конвея, законы эволюции ПО Лемана (1980), системные архетипы Сенге («Пятая дисциплина», 1990), «правило 20 % на техдолг». Всё это — полезные структурирующие идеи без контролируемой проверки. Их место — генерировать гипотезы, а не закрывать споры.

Про системную динамику как дисциплину. Форрестер построил её на инженерной теории управления («Industrial Dynamics», 1961), и это дало работающий язык. Но «Пределы роста» (Meadows et al., 1972) — не пример подтверждённого предсказания. Модель World3 критиковали за агрегированность и за параметры, не выведенные из данных: наиболее известна рецензия Уильяма Нордхауса «World Dynamics: Measurement Without Data» (Economic Journal, 1973) и сборник «Models of Doom» (Cole et al., 1973). Позднейшие работы, сопоставлявшие сценарии с реальностью (Turner, 2008; Herrington, 2021), сообщают о согласии по ряду переменных, но методологически это сравнение сценариев с данными, а не проверка предсказаний вне выборки, и спор продолжается пятьдесят лет. Правильный вывод для инженера: системная динамика — хороший язык описания структуры и плохой оракул величин на длинных горизонтах. Сам Стерман писал об этом прямо: валидация SD-моделей опирается на тесты структуры и поведения, а не на точность прогноза (Barlas, 1996; Sterman, «All Models Are Wrong», System Dynamics Review, 2002). И отдельно: устойчивых доказательств того, что обучение «системному мышлению» улучшает решения на практике, мало. Что люди систематически ошибаются в задачах с запасами и задержками — проверено (Booth Sweeney, Sterman, 2000); что после обучения ошибаются меньше — гипотеза. Пользоваться методом стоит не потому, что он «доказан», а потому что он даёт проверяемые предсказания там, где интуиция не даёт никаких.

Типовые ловушки, собранные вместе

  • Оптимизация части в ущерб целому. Ускорили стадию, где $\rho$ мала. Признак: локальная метрика улучшилась, сквозная не изменилась.
  • Перенос проблемы. Быстрая митигация вытеснила структурное исправление. Признак: частота выполнения обхода растёт.
  • Костыль стал процессом. У обхода появились владелец, бюджет и метрика. Признак: его нельзя отключить без переговоров с тремя командами.
  • Эскалация. Цель одной стороны задаётся состоянием другой (штат, оценки, SLO). Признак: обоснование в терминах сравнения, а не задачи.
  • Трагедия общего ресурса. Общие раннеры, общая БД, общие интервьюеры. Признак: выигрыш внутри границы команды, издержка — снаружи. Лечится видимостью расхода и квотами, а не призывами.
  • Схема без опровержения. Красивый набор стрелок, из которого не следует ни одного наблюдения; признак — на вопрос «что мы увидим, если это неверно» нет ответа. Сюда же закрытый нагрузочный тест как доказательство устойчивости: тест зелёный, прод падает при той же нагрузке.

Мини-итог

  • Один протокол применим к задачам с разбросом масштабов в шесть порядков: режим → граница → запасы → петли → задержки → предсказание → вмешательство.
  • Проверяемость разная, и её надо объявлять. Очередь считается арифметикой; инцидент воспроизводится в тесте; техдолг измеряется заместителями; найм остаётся преимущественно языком согласования ожиданий.
  • Стандартный источник сюрприза — не сложность, а то, что менялся знаменатель: ёмкость, а не приток. Метастабильность, гистерезис и порог сброса при этом считаются заранее и превращаются в конкретные числа плана восстановления.
  • Быстрая петля всегда вытесняет медленную; поэтому «починили симптом» — это не финал, а начало новой структуры, у которой появятся владелец и бюджет.
  • Схема без наблюдения, которое её опровергает, — иллюстрация. Записывайте разборы так, чтобы через квартал их можно было проверить и признать неверными.

Источники

Что дальше

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

Практика и ресурсы: с чего начать и что читать дальше

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов