Kanban и управление потоком: WIP, cycle time, диаграммы
Большинство команд, которые «перешли на Kanban», просто повесили доску «To Do → In Progress → Done» и перестали делать спринты. Через полгода задачи всё так же тянутся неделями, а на вопрос «когда будет готово» отвечает интуиция самого уверенного человека в комнате.
Kanban — это не доска. Это набор идей из теории очередей и бережливого производства о том, что скорость системы определяется не тем, как быстро работают люди, а тем, сколько работы одновременно находится в системе и как она движется между этапами. Статья — про то, как измерять поток, управлять им и получать из измерений прогнозы, которым можно верить. Контраст с предыдущей статьёй трека про Agile и Scrum: Scrum задаёт ритм (итерация как единица планирования), Kanban — непрерывность (поток как единица управления); это не конкуренты, а разные точки приложения силы.
Часть 1. Интуиция: почему поток важнее загрузки
Представьте шоссе: пропускная способность полосы максимальна не когда машины стоят бампер к бамперу, а когда они едут с дистанцией. При загрузке 100% скорость потока — ноль: все машины «в работе», ни одна не двигается. То же в разработке. Если каждый инженер занят на 100%, у системы нет запаса среагировать на ревью, инцидент, вопрос аналитика: любая задача, требующая чужого времени, встаёт в очередь. Очереди — невидимый склад незавершённого производства: их нет ни в бэклоге, ни в трудозатратах, но именно они съедают сроки.
Формально это формула Кингмана (VUT-уравнение) для очереди G/G/1: $W_q \approx \bigl(\rho/(1-\rho)\bigr)\cdot\bigl((c_a^2+c_s^2)/2\bigr)\cdot\tau$, где $\rho$ — загрузка, $c_a,c_s$ — коэффициенты вариации интервалов поступления и времени обслуживания, $\tau$ — среднее время обслуживания. Первый множитель растёт нелинейно: при загрузке 60% ожидание в 1,5 раза больше времени работы, при 80% — в 4 раза, при 90% — в 9 раз. Второй множитель не менее важен: чем разнороднее задачи, тем длиннее очереди при той же загрузке.
Отсюда два вывода, противоречащих управленческой интуиции: догружать занятую команду — способ её замедлить, а разбивать задачи на однородные куски — способ сократить сроки без роста скорости работы. Экономика очередей подробно разобрана у Дональда Рейнертсена в «The Principles of Product Development Flow».
«Канбан» (看板) — сигнальная карточка Toyota: предыдущий этап не производит ничего, пока следующий не подал сигнал «мне нужно». Это вытягивающая (pull) система вместо выталкивающей (push). Разница не терминологическая: в push объём работы задают амбиции плана, в pull — явный лимит.
Часть 2. Шесть практик и доска как модель процесса
Андерсон сформулировал метод как набор практик, а не как процесс: Kanban применяют поверх существующего процесса, поэтому внедрять его можно, ничего не ломая в понедельник.
| Практика | Что означает | Что ломается без неё |
|---|---|---|
| Визуализировать | Видна каждая единица работы, включая скрытую (баги, поддержка, «просто спросить») | Управляют не тем, что реально происходит |
| Ограничить WIP | У колонок явные числовые лимиты | Очереди растут, сроки размазываются |
| Управлять потоком | Смотрят на метрики потока, а не на занятость | Локальная оптимизация вместо глобальной |
| Сделать политики явными | Правила перехода между колонками записаны | Каждый понимает «Done» по-своему |
| Внедрить обратную связь | Регулярные встречи по потоку, а не по статусам | Проблемы не всплывают |
| Улучшать эволюционно | Изменения — эксперименты с гипотезой и метрикой | Реорганизация раз в год вместо подстройки |
Минимум зафиксирован в Kanban Guide — четыре страницы: три обязательные практики
(визуализация, ограничение WIP, активное управление элементами) и три обязательные метрики (WIP, throughput, cycle
time). Плохая доска повторяет оргструктуру; хорошая показывает, где работа ждёт — это 80–95% времени жизни задачи.
Отсюда правило: рабочий этап делят на doing и буфер done.
(без лимита)"] R["Ready (4)"] subgraph Dev["Разработка (3)"] D1["Doing"] --> D2["Done ✔
буфер"] end subgraph Rev["Ревью (2)"] V1["Doing"] --> V2["Done ✔
буфер"] end T["Тест (2)"] Rel["Релиз"] B -.->|"replenishment:
раз в неделю"| R R -->|pull| D1 D2 -->|pull| V1 V2 -->|pull| T T -->|pull| Rel style B fill:#e8833a,fill-opacity:0.2 style Rel fill:#46a758,fill-opacity:0.2
Стрелки pull по смыслу направлены назад: тестировщик не получает задачу — он её забирает, когда освободился
слот; нечего забирать — идёт помогать там, где затор («swarming»), что возможно только при явных лимитах. Буферы
done отличают «человек работает» от «работа лежит», без чего не посчитать flow efficiency — долю времени, когда
над задачей реально работали (типичные значения 5–15%, и первое измерение впечатляет команду сильнее презентации про
Lean).
Часть 3. WIP-лимиты: главный рычаг
Как выбрать число:
- Стартовая точка: лимит колонки ≈ число людей, способных её обслуживать, минус один — этот «минус один» и есть запас против $\rho \to 1$.
- Правило «меньше, чем удобно»: лимит должен вызывать лёгкий дискомфорт; если за неделю в него ни разу не упёрлись, он ничего не сигнализирует и потому бесполезен.
- Персональный WIP: не больше двух карточек на человека (одна активная + одна заблокированная); для маленьких команд рабочий вариант — один общий лимит на всю доску вместо поколоночных.
Самый важный момент метода — что делать, упёршись в лимит: обычно именно здесь всё разваливается, команда «на один разок» превышает лимит, и через месяц лимитов нет.
Когда слотов нет, легальных ходов четыре, по приоритету: (1) помочь протолкнуть то, что уже в работе; (2) разобрать блокировку — сходить к смежникам, эскалировать; (3) улучшить систему — дописать автотест, починить флаки; (4) и только потом вытянуть новую карточку. Чего в списке нет — «начать ещё одну задачу, потому что скучно»: многозадачность удлиняет все задачи в системе.
на_событие(слот освободился в колонке C):
если заблокированные карточки в C или правее: помочь разблокировать
иначе если WIP(C) < LIMIT(C) и очередь(C-1) не пуста: тянуть по политике
иначе: улучшать систему
политика выбора = expedite (макс. 1) → просроченный fixed-date
→ максимум «стоимость задержки / размер» (CD3) → FIFO
Сложность — O(1) на событие (O(log n) на вставку, если держать очередь в куче по приоритету), но на доске десятки карточек: важна не сложность, а то, что правило записано и одинаково понимается.
Часть 4. Закон Литтла: строгое основание
Единственная формула, обязательная для всех, кто управляет потоком: $L = \lambda W$, то есть
$$\text{среднее время прохождения} = \frac{\text{среднее WIP}}{\text{средний throughput}}$$
В системе 20 карточек, команда завершает 5 в неделю → среднее время прохождения 4 недели. Хотите 2 недели: либо удвойте throughput (дорого, долго, найм или автоматизация), либо вдвое сократите WIP (бесплатно, сегодня).
Теорема Литтла (John D. C. Little, 1961) — утверждение о долгосрочных средних в стационарной системе. Условия, которые почти всегда нарушают: длинный интервал наблюдения; все элементы в итоге покидают систему (ничего не висит вечно и не удаляется); средние устойчивы, а не растут; единицы согласованы (карточки и карточки, а не story points и карточки).
Ваканти в «Actionable Agile Metrics for Predictability» подчёркивает: закон Литтла — диагностический инструмент, а не прогнозный. Нельзя рассуждать «нам нужно 30 задач за квартал, значит поднимем WIP до N»; используют наоборот — если независимо посчитанные WIP, throughput и cycle time не сходятся по формуле, то либо данные врут (карточки, зависшие с прошлого года), либо система нестационарна.
Половину бессмысленных споров порождает путаница в терминах: customer lead time — от просьбы клиента до получения
результата (это чувствует заказчик); system cycle time — от точки обязательства (карточка вышла из «опций») до
Done (этим управляет команда); time in process — время в конкретной колонке. На доске обязательно обозначают
точку обязательства и точку завершения: всё левее обязательства — опции, а не работа, их можно выбросить бесплатно.
Поэтому бэклог не имеет WIP-лимита, а «Ready» — имеет.
Часть 5. Метрики потока и код для их расчёта
| Метрика | Определение | Как читать |
|---|---|---|
| WIP | Число начатых, но не завершённых элементов | Растёт → сроки поедут |
| Throughput | Число завершённых элементов за период | Основа прогнозов |
| Cycle time | Календарное время от старта до финиша | Смотреть распределение, не среднее |
| Work Item Age | Сколько уже живёт незавершённая карточка | Единственная метрика для действий сегодня |
Про Work Item Age отдельно: cycle time — метрика прошлого (на завершённые задачи не повлиять), Age — метрика настоящего. Карточка живёт 9 дней при 85-м процентиле в 8 → вмешиваться нужно сейчас, а не на ретро через две недели.
"""Метрики потока из журнала переходов: у карточки есть {колонка: дата входа} —
ровно то, что отдаёт changelog Jira / Linear / GitLab."""
from dataclasses import dataclass
from datetime import date
from statistics import mean
from typing import Sequence
ACTIVE = {"Dev", "Review", "Test"} # колонки, где реально работают
COMMITMENT, COMPLETION = "Dev", "Done"
@dataclass
class Card:
key: str
entered: dict[str, date] # колонка -> дата входа
@property
def cycle_time(self) -> int | None:
"""Календарные дни от обязательства до завершения, минимум 1."""
start, end = self.entered.get(COMMITMENT), self.entered.get(COMPLETION)
return None if not (start and end) else max((end - start).days, 1)
def age(self, today: date) -> int | None:
"""Возраст незавершённой карточки — метрика для ежедневных решений."""
start = self.entered.get(COMMITMENT)
if not start or COMPLETION in self.entered:
return None
return max((today - start).days, 1)
def flow_efficiency(self) -> float | None:
"""Доля времени в активных колонках (остальное — очереди). Обычно 5–15%."""
if self.cycle_time is None:
return None
stamps = sorted(self.entered.items(), key=lambda kv: kv[1])
active = sum((nxt - cur).days
for (col, cur), (_, nxt) in zip(stamps, stamps[1:]) if col in ACTIVE)
return active / self.cycle_time
def percentile(values: Sequence[float], p: float) -> float:
"""Ближайший ранг (nearest-rank): реально наблюдавшееся значение,
а не интерполяция между точками. O(n log n) на сортировку."""
ordered = sorted(values)
return ordered[min(len(ordered), max(1, -(-int(p * len(ordered) * 100) // 100))) - 1]
def report(cards: Sequence[Card], today: date) -> None:
cts = [c.cycle_time for c in cards if c.cycle_time is not None]
sle = percentile(cts, 0.85) # это и есть обещание команды
wip = sum(1 for c in cards if c.age(today)) # начали, но не закончили — O(n)
print(f"среднее {mean(cts):.1f} дн. <- бесполезно; p50/p85/p95: "
f"{percentile(cts, .5):.0f}/{sle:.0f}/{percentile(cts, .95):.0f} дн.; WIP {wip}; "
f"flow eff. {mean([e for c in cards if (e := c.flow_efficiency())]):.0%}")
# Стареющие карточки — единственное, на что можно повлиять сегодня
for card, days in sorted(((c, a) for c in cards if (a := c.age(today))),
key=lambda pair: -pair[1]):
print(f"{'!!' if days > sle else ' '} {card.key:<10} в работе {days:>3} дн.")
Сложность — O(n log n) по времени (доминирует сортировка для процентилей) и O(n) по памяти; узкое место в проде — выгрузка из трекера, а не арифметика. Тот же расчёт над репликой трекера:
-- Cycle time: первый переход в 'Dev' -> первый переход в 'Done'
WITH t AS (
SELECT issue_key,
MIN(changed_at) FILTER (WHERE to_value = 'Dev')::date AS started_on,
MIN(changed_at) FILTER (WHERE to_value = 'Done')::date AS finished_on
FROM issue_changelog WHERE field = 'status' GROUP BY issue_key
)
SELECT date_trunc('week', finished_on) AS week, COUNT(*) AS throughput,
percentile_disc(0.50) WITHIN GROUP (ORDER BY finished_on - started_on) AS p50,
percentile_disc(0.85) WITHIN GROUP (ORDER BY finished_on - started_on) AS p85,
percentile_disc(0.95) WITHIN GROUP (ORDER BY finished_on - started_on) AS p95
FROM t WHERE started_on IS NOT NULL AND finished_on >= started_on
GROUP BY 1 ORDER BY 1;
percentile_disc, а не percentile_cont — по той же причине, что и nearest-rank выше: нужно реально наблюдавшееся
значение.
Часть 6. Две диаграммы, которые заменяют статус-отчёт
CFD (cumulative flow diagram) — накопительный график: по X время, по Y накопленное число карточек, достигших этапа. Читается геометрически.
- Наклон верхней кривой — темп поступления, нижней — throughput. Верхняя круче нижней → вы копите долг, и никакие спринты этого не изменят.
- Вертикаль между кривыми — WIP этапа на эту дату; горизонталь — приблизительное среднее время прохождения (со всеми оговорками закона Литтла: только для стационарной системы).
- Расширяющаяся полоса — бутылочное горлышко; плоский участок — этап встал (отпуск единственного тестировщика, внешнее согласование); ступеньки на нижней кривой — работа завершается пачками: релиз раз в две недели или тестирование батчем.
Scatterplot cycle time — главный график Kanban-команды. Точка = завершённая карточка: X — дата завершения, Y — сколько дней заняла, горизонтали — процентили.
- Распределение правоскошенное — норма для интеллектуального труда, а не аномалия; поэтому среднее и σ бессмысленны: «среднее ± 2σ» даст отрицательные сроки слева и заниженный хвост справа.
- Обещание = процентиль: «85% задач завершаем за 8 дней или быстрее» — это SLE (Service Level Expectation), не дедлайн, а вероятностное обещание.
- Тренд разброса: облако сжимается вниз → система предсказуемее. Именно сужение хвоста, а не средняя скорость, обычно и есть настоящая цель.
- Выбросы именные: каждую красную точку можно открыть и спросить «что здесь произошло». Три-четыре разобранных выброса дают больше, чем месяц абстрактных ретроспектив.
Часть 7. Прогнозирование без оценок
Имея распределение throughput, можно отвечать бизнесу вероятностно, не оценивая каждую задачу. Метод — Monte Carlo: многократно разыгрываем будущее, подставляя случайные недели из истории.
"""Monte Carlo: «когда будут готовы N карточек?»"""
import random
def forecast_weeks(history: list[int], backlog: int, trials: int = 10_000,
split_factor: float = 1.0, seed: int | None = 42) -> dict[float, int]:
"""history — throughput по неделям за ~12 недель;
split_factor — поправка на дробление (1.3 = карточка в среднем станет 1.3).
Сложность: O(trials * недель) по времени, O(trials) по памяти."""
rng = random.Random(seed)
target = int(backlog * split_factor)
results = []
for _ in range(trials):
done = weeks = 0
while done < target and weeks < 500: # страховка от нулевой истории
done += rng.choice(history)
weeks += 1
results.append(weeks)
results.sort()
return {p: results[min(len(results) - 1, int(p * len(results)))]
for p in (0.50, 0.70, 0.85, 0.95)}
if __name__ == "__main__":
history = [7, 5, 9, 4, 8, 6, 11, 3, 7, 6, 8, 5] # разброс 3..11 — это норма
print(forecast_weeks(history, backlog=40, split_factor=1.3)) # {0.5: 8, ..., 0.95: 10}
Вывод: «40 карточек — 8 недель с вероятностью 50%, 9 недель с вероятностью 85%, 10 недель с вероятностью 95%». Это честнее любой оценки в часах, потому что включает всю наблюдавшуюся вариабельность — отпуска, инциденты, неожиданные баги — без необходимости их перечислять. Обратный вопрос («сколько успеем за 8 недель?») решается тем же ресемплингом: суммируем восемь случайных недель, повторяем 10 000 раз, и консервативный ответ — это нижний процентиль объёма.
Три предостережения, без которых метод превращается в театр:
- История должна быть репрезентативной: сменился состав или характер работы — старые данные не про эту систему (практическое правило: 8–15 последних недель).
- Учитывайте дробление скоупа: «сделать личный кабинет» при уточнении станет пятью карточками. Магеннис предлагает измерять исторический коэффициент дробления и подставлять его явно — см. FocusedObjective.
- Не выдавайте 50-й процентиль за план: половина прогонов в него не уложилась — это та самая ситуация, где «мы почти успели» повторяется каждый квартал.
Переход от оценок к вероятностям разобран в «When Will It Be Done?» и в следующей статье трека про оценку и планирование.
Часть 8. Классы обслуживания
Не всякая работа одинаково срочна. Kanban формализует это через классы обслуживания — явные политики обращения с типами карточек; класс определяется формой стоимости задержки (cost of delay): как меняется ценность, если сделать позже.
- Expedite — «стоп-кран»: можно превысить WIP-лимит, но не более одной карточки одновременно, и каждый случай разбирается. Если expedite больше 10% потока — это не класс обслуживания, а сломанный процесс.
- Fixed date — планируется обратным отсчётом от 95-го процентиля: p95 = 17 дней и дата 1 марта → начинать не позже 12 февраля. Раньше — карточка зря занимает WIP, позже — обещание уже вероятностно невыполнимо.
- Standard — основной поток: FIFO или CD3 (cost of delay divided by duration).
- Intangible — техдолг и обучение; без явной квоты (скажем, 20% слотов) класс вытесняется полностью, и через год система становится неуправляемой. Связь качества с потоком — в статье про качество и релизы.
CD3 обоснован у Рейнертсена; в SAFe та же идея называется WSJF — её ограничения разобраны в статье про масштабирование.
Часть 9. Ритмы: Kanban не значит «без встреч»
Kanban заменяет календарь спринта набором независимых каденций, у каждой своя частота.
Отличия от привычных ритуалов: дейлик идёт справа налево (от карточек ближе к «Done») и обсуждает карточки, а не людей — единственное изменение формата, которое само по себе обычно сокращает cycle time. Пополнение отвязано от завершения: освободился слот — пополнили, конца спринта ждать не нужно. Улучшения формулируются как эксперименты: «уменьшим лимит Review с 4 до 2, ждём падения p85 с 12 до 9 дней, проверим через две недели» — это управление, а «давайте лучше коммуницировать» — нет. Как такие встречи не превращаются в театр — в статье про команду и коммуникации.
Часть 10. Типичные ошибки
- Доска есть, лимитов нет. Самая частая ошибка. Проверка: было ли за последний месяц хоть одно решение, принятое из-за упора в лимит? Если нет — лимитов фактически нет.
- Лимиты обходят. Заводят колонки «на паузе», «ждёт заказчика», «в фоне», куда карточки переезжают и перестают считаться WIP. Правило: если работа не завершена и не выброшена, она в WIP; честных способов уменьшить его два — доделать или отменить.
- Cycle time в рабочих часах с вычетом блокировок. Соблазн понятен («мы не виноваты, что интеграция ждала неделю»), но клиент ждал календарные дни. Вычитание ожиданий уничтожает смысл метрики: вы перестанете видеть ровно ту проблему, которой должны управлять. Для активного времени есть отдельная метрика — flow efficiency.
- Средние вместо процентилей. «Средний cycle time 5 дней» при p85 = 12 — обещание, которое будет нарушено в трети случаев.
- Оптимизация занятости. «У Пети нет задач» — не проблема, «задача три дня ждёт ревью» — проблема; пока метрикой руководителя остаётся загрузка людей, WIP-лимиты саботируются сверху.
- Kanban как отмена планирования. Наоборот: без итераций планирование становится непрерывным и требует дисциплины пополнения и явных SLE. Иначе выходит поток без обещаний, и бизнес справедливо перестаёт доверять команде.
- Метрики как KPI людей. Как только throughput попадает в оценку сотрудников, он перестаёт быть измерением (закон Гудхарта): карточки дробятся, «Done» смещается влево. Метрики потока — для системы, не для людей; та же логика и для DORA — см. статью про инженерные метрики.
Часть 11. Как это выглядит в проде
Инструменты. Jira с плагином (ActionableAgile, Nave, Screenful), Linear, GitHub Projects с выгрузками, Azure DevOps. Жёсткое требование одно: инструмент должен отдавать changelog переходов, а не только текущий статус — иначе историю метрик не восстановить.
Реалистичный сетап команды из шести человек. Колонки: Options (без лимита) → Ready (4) → Dev (3, с буфером Done) → Review (2) → Test (2) → Done. Классы: Expedite (макс. 1), Fixed date, Standard, Intangible (квота 20%). SLE «85% стандартных карточек — за 9 дней», пополнение по вторникам, дейлик справа налево, обзор потока раз в две недели; карточка старше 9 дней подсвечивается автоматически и обсуждается первой.
Scrumban — самая частая реальная конфигурация: спринты остаются ритмом планирования и демо, но внутри работают WIP-лимиты, вытягивание и метрики потока вместо burndown (тот показывает лишь отклонение от плана, scatterplot — поведение системы). Метод хорошо ложится и на поддержку, SRE, DevOps и найм — там, где работа приходит непредсказуемо и итерации искусственны.
Порядок внедрения. Не «внедряем Kanban», а: (1) визуализируйте всё, включая скрытую работу; (2) месяц собирайте данные, ничего не меняя; (3) постройте scatterplot и покажите команде хвост; (4) введите лимит на одну колонку — ту, перед которой на CFD копится очередь; (5) замерьте через две недели; (6) повторите. Эволюционность тут не идеология, а способ не получить сопротивление.
Мини-итог
- Скорость системы определяется потоком, а не занятостью: загрузка выше ~80% нелинейно удлиняет сроки.
- WIP-лимит — главный и почти бесплатный рычаг: вдвое меньше WIP при том же throughput = вдвое короче время прохождения. Но закон Литтла диагностирует систему, а не обосновывает планы.
- Cycle time смотрят распределением: обещания — процентилями (SLE), а не средними. CFD читают геометрически (наклон = скорость, вертикаль = WIP, горизонталь = время), scatterplot — по хвосту и выбросам; Work Item Age — единственная метрика для действий сегодня.
- Прогноз даёт Monte Carlo по историческому throughput, с поправкой на дробление скоупа, а классы обслуживания превращают приоритизацию в политику, а не в переговоры по громкости голоса.
Источники
- David J. Anderson. Kanban: Successful Evolutionary Change for Your Technology Business, 2010; Daniel Vacanti, John Coleman, Kanban Guide.
- Daniel Vacanti. Actionable Agile Metrics for Predictability, When Will It Be Done?
- Donald Reinertsen. The Principles of Product Development Flow, 2009 — экономика очередей, CD3.
- John D. C. Little. A Proof for the Queuing Formula: L = λW, Operations Research, 1961; формула Кингмана для G/G/1.
- Martin Fowler, KanbanBoard; Atlassian, Kanban guide.
- Troy Magennis. FocusedObjective Resources — открытые модели Monte Carlo.
Что дальше
Мы научились измерять поток и строить вероятностные прогнозы по историческим данным. Но что делать, когда истории ещё нет — новый продукт, новая команда, новая предметная область? Об этом следующая статья: Оценка и планирование: story points, PERT, #NoEstimates. Карта трека целиком — в обзоре.