Разборы: инцидент, техдолг, найм и очередь как системы
Тринадцать предыдущих глав выдавали инструменты поштучно: границы, петли, запасы, задержки, пороги, архетипы, рычаги, проверка диаграмм и пределы моделирования. Эта глава не добавляет теории. Она берёт один протокол и прогоняет его четыре раза по задачам, которые в жизни выглядят как принципиально разные жанры: пятиминутный отказ сервиса, двухлетнее гниение кодовой базы, наём двух инженеров и медленно растущая очередь.
Читать нужно не истории, а метод: одинаковые семь шагов, одинаковые вопросы и одинаковая дисциплина — назови наблюдение, которое опровергнет твою схему, до того как схему нарисовал.
Порядок разборов выбран по убыванию проверяемости, и это принципиально:
- Очередь — модель количественная, опровергается за час нагрузочного теста.
- Инцидент — проверяется наполовину: механику можно воспроизвести, организационную петлю вокруг неё — только косвенно.
- Техдолг — прямых измерений нет вообще, есть заместители разной степени честности.
- Найм — данных мало, конфаундеров много, часть выводов честнее оставить гипотезой.
Если начинать с найма, легко поверить, что стрелочки на схеме сами по себе что-то доказывают. Начинать надо там, где схему бьёт арифметика.
Одна структура, четыре масштаба времени
Общее у четырёх разборов — форма контура: что-то накапливается, сигнал об этом приходит с опозданием, действие даёт эффект ещё позже. Отличается только длительность, и отличается на шесть порядков.
Практическое следствие важнее картинки. Число циклов обучения за квартал — это и есть ваш бюджет на ошибки. По очереди вы делаете десятки итераций в день и можете позволить себе метод проб. По найму — две-три попытки за карьеру в конкретной команде, и каждая ошибка обходится в год. Поэтому там, где циклов много, работает эксперимент; там, где мало, работает арифметика заранее — и признание, что часть выводов останется непроверенной.
Протокол разбора: семь шагов и стоп-правило
минимум два периода повтора"] S1 --> STOP{"Повторяется?
Есть хоть одна измеряемая переменная?"} STOP -->|нет| EXIT(["Стоп: это событие, а не система.
Чините локально, не рисуйте схему"]) STOP -->|да| S2["2. Граница и горизонт:
что внутри, что снаружи, на каком сроке"] S2 --> S3["3. Запасы: что накапливается
и в каких единицах"] S3 --> S4["4. Петли-кандидаты со знаками
и явными полярностями"] S4 --> S5["5. Задержки на дугах:
обнаружение, действие, эффект"] S5 --> S6["6. Предсказание:
что мы увидим, если схема верна,
и что увидим, если неверна"] S6 --> CHECK{"Предсказание можно
проверить за 2 недели?"} CHECK -->|нет| WEAK(["Пометить как гипотезу.
Действовать по обратимым вмешательствам"]) CHECK -->|да| S7["7. Вмешательство: рычаг, цена,
обратимость, дата ретроспективы"] S7 --> BACK["Наблюдение → пересмотр схемы"] BACK --> S1
Расшифровка шагов — с типичным плохим ответом рядом, потому что плохие ответы устойчивее хороших.
| Шаг | Хороший ответ | Плохой ответ (и почему он плохой) |
|---|---|---|
| 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 — не халатность, а рациональный выбор внутри границы «квартальные цели команды».
лимиты, сброс, бэкофф"] CAP -->|"−"| Q RS -->|"+"| FREQ[Частота выполнения раннбука] FREQ -->|"+ R2: зависимость от костыля"| RS
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))$.
Три вывода, которых нет в словесном описании инцидента:
- Порог выхода считается заранее: стационарное условие $\lambda (1 - s) (1 + r) < \mu$ даёт $s > 1 - \mu / ((1 + r)\lambda) = 52{,}4$ %. Сбросить 30 % — потратить время впустую.
- Стационарного порога мало. На границе 52 % очередь дренируется со скоростью около нуля. Чтобы уложиться в 15 минут, нужно 55 %, чтобы в две — 60 %. У плана восстановления должно быть число и срок, а не «снизим нагрузку».
- Асимметрия входа и выхода. Вошли при мгновенных 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$), картина ухудшится ещё.
Петли
общий ресурс"] INTV -->|"−"| QUAL[Качество отбора] QUAL -->|"+ R3: гонка за слоты бьёт по всем"| BURN
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 в горячих точках | Кривая рампапа и лаговая корреляция оттока |
| Рычаг | Лимит конкурентности и сброс | Убрать зависимость от ручной митигации | Явная доля мощности + приоритет по горячим точкам | Снизить связность задач и защитить наставничество |
| Проверяемость | Высокая | Средняя | Низкая, только заместители | Очень низкая |
Три инварианта, которые видны только при сравнении:
- Симптом всегда там, где смотрят; причина — там, где не смотрят. Очередь видна на графике латентности, а её причина — в hit rate кэша. Инцидент виден в 5xx, а его причина — в приоритете тикета P3.
- Быстрая петля вытесняет медленную. Перезапуск вытесняет лимит конкурентности, обход вытесняет рефакторинг, найм вытесняет упрощение работы. Это не слабость людей, а свойство структуры: быстрая петля успевает замкнуться раньше.
- Рычаг почти всегда меняет структуру или информацию, а не усилие. Ни в одном из четырёх разборов сильнейшее вмешательство не имеет вида «делать то же самое, но старательнее».
Пределы метода: где эти разборы перестают работать
Честный список того, чего вы не получили:
- Схема не даёт величин. Полярности говорят «вырастет», модель говорит «вырастет на столько-то при таких-то коэффициентах». Коэффициенты берутся из измерений, и если их нет, диаграмма предсказывает только знак — иногда этого достаточно, чаще нет.
- Границу выбрали вы. Любой из четырёх разборов можно расширить (очередь → вся цепочка сервисов, найм → рынок труда) и получить другой вывод. Это не дефект метода, а его условие использования: граница должна быть записана и обоснована (глава 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). Признак: обоснование в терминах сравнения, а не задачи.
- Трагедия общего ресурса. Общие раннеры, общая БД, общие интервьюеры. Признак: выигрыш внутри границы команды, издержка — снаружи. Лечится видимостью расхода и квотами, а не призывами.
- Схема без опровержения. Красивый набор стрелок, из которого не следует ни одного наблюдения; признак — на вопрос «что мы увидим, если это неверно» нет ответа. Сюда же закрытый нагрузочный тест как доказательство устойчивости: тест зелёный, прод падает при той же нагрузке.
Мини-итог
- Один протокол применим к задачам с разбросом масштабов в шесть порядков: режим → граница → запасы → петли → задержки → предсказание → вмешательство.
- Проверяемость разная, и её надо объявлять. Очередь считается арифметикой; инцидент воспроизводится в тесте; техдолг измеряется заместителями; найм остаётся преимущественно языком согласования ожиданий.
- Стандартный источник сюрприза — не сложность, а то, что менялся знаменатель: ёмкость, а не приток. Метастабильность, гистерезис и порог сброса при этом считаются заранее и превращаются в конкретные числа плана восстановления.
- Быстрая петля всегда вытесняет медленную; поэтому «починили симптом» — это не финал, а начало новой структуры, у которой появятся владелец и бюджет.
- Схема без наблюдения, которое её опровергает, — иллюстрация. Записывайте разборы так, чтобы через квартал их можно было проверить и признать неверными.
Источники
- J. D. C. Little. A Proof for the Queuing Formula // Operations Research, 1961. doi:10.1287/opre.9.3.383; J. F. C. Kingman. The Single Server Queue in Heavy Traffic, 1961 — приближение для G/G/1.
- N. Gunther. Universal Scalability Law — предел масштабирования по числу воркеров.
- N. Bronson, A. Aghayev, A. Charapko, T. Zhu. Metastable Failures in Distributed Systems // HotOS 2021; L. Huang et al. Metastable Failures in the Wild // OSDI 2022.
- Google SRE Book: Handling Overload, Addressing Cascading Failures, Postmortem Culture.
- AWS Builders’ Library: Timeouts, retries and backoff with jitter, Using load shedding to avoid overload.
- W. Cunningham. The WyCash Portfolio Management System // OOPSLA 1992 — исходная метафора долга; M. Lehman. Programs, Life Cycles, and Laws of Software Evolution // Proceedings of the IEEE, 1980.
- A. Tornhill, M. Borg. Code Red: The Business Impact of Code Quality, 2022; A. Tornhill. Your Code as a Crime Scene, 2015 — hotspot-анализ.
- F. Brooks. The Mythical Man-Month, 1975; M. Conway. How Do Committees Invent?, 1968; D. Reinertsen. The Principles of Product Development Flow, 2009; E. Goldratt. The Goal, 1984 — теория ограничений с кейсовой, а не экспериментальной базой.
- J. Sterman. Modeling Managerial Behavior // Management Science, 1989. doi:10.1287/mnsc.35.3.321; L. Booth Sweeney, J. Sterman. Bathtub Dynamics // System Dynamics Review, 2000. doi:10.1002/sdr.198
- Y. Barlas. Formal Aspects of Model Validity and Validation in System Dynamics, 1996; J. Sterman. All Models Are Wrong // System Dynamics Review, 2002. doi:10.1002/sdr.261
- D. Meadows et al. The Limits to Growth, 1972; W. Nordhaus. World Dynamics: Measurement Without Data // Economic Journal, 1973. doi:10.2307/2231475; G. Turner, 2008 doi:10.1016/j.gloenvcha.2008.05.001; G. Herrington, 2021 doi:10.1111/jiec.13084.
- N. Leveson. Engineering a Safer World — STAMP как альтернатива линейным моделям аварий; S. Dekker. The Field Guide to Understanding Human Error, 2014; R. Cook. How Complex Systems Fail; N. Forsgren, J. Humble, G. Kim. Accelerate, 2018 — метрики DORA и границы их доказательности.
Что дальше
Метод разобран и применён. Остаётся превратить его в привычку: выбрать первую систему для тренировки, найти данные, которые уже есть, и не утонуть в литературе, половина которой — пересказ одной книги.