Project Management Kanban и управление потоком: WIP, cycle time, диаграммы
0%

Kanban и управление потоком: WIP, cycle time, диаграммы

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.

Стрелки 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 — сколько дней заняла, горизонтали — процентили.

Диаграмма разброса cycle time с процентилями

  1. Распределение правоскошенное — норма для интеллектуального труда, а не аномалия; поэтому среднее и σ бессмысленны: «среднее ± 2σ» даст отрицательные сроки слева и заниженный хвост справа.
  2. Обещание = процентиль: «85% задач завершаем за 8 дней или быстрее» — это SLE (Service Level Expectation), не дедлайн, а вероятностное обещание.
  3. Тренд разброса: облако сжимается вниз → система предсказуемее. Именно сужение хвоста, а не средняя скорость, обычно и есть настоящая цель.
  4. Выбросы именные: каждую красную точку можно открыть и спросить «что здесь произошло». Три-четыре разобранных выброса дают больше, чем месяц абстрактных ретроспектив.

Часть 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 раз, и консервативный ответ — это нижний процентиль объёма.

Три предостережения, без которых метод превращается в театр:

  1. История должна быть репрезентативной: сменился состав или характер работы — старые данные не про эту систему (практическое правило: 8–15 последних недель).
  2. Учитывайте дробление скоупа: «сделать личный кабинет» при уточнении станет пятью карточками. Магеннис предлагает измерять исторический коэффициент дробления и подставлять его явно — см. FocusedObjective.
  3. Не выдавайте 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, с поправкой на дробление скоупа, а классы обслуживания превращают приоритизацию в политику, а не в переговоры по громкости голоса.

Источники

Что дальше

Мы научились измерять поток и строить вероятностные прогнозы по историческим данным. Но что делать, когда истории ещё нет — новый продукт, новая команда, новая предметная область? Об этом следующая статья: Оценка и планирование: story points, PERT, #NoEstimates. Карта трека целиком — в обзоре.

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

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

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

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