Качество, Definition of Done и релизный менеджмент
Есть два вопроса, на которые команда отвечает десятки раз в неделю, и от честности этих ответов зависит почти всё остальное в проекте:
- «Это готово?»
- «Это можно выкатывать?»
Обычно на первый отвечают словами «ну, в целом да, осталось дотестировать», а на второй — «давай в пятницу вечером, чтобы если что, в выходные починим». Обе фразы — симптом одного и того же: у команды нет операционального определения готовности и нет управляемого процесса доставки. Качество в такой системе — это не свойство продукта, а результат везения и героизма отдельных людей.
Эта статья — про то, как перевести оба вопроса из области мнений в область проверяемых фактов. Мы разберём экономику дефектов (почему поздняя проверка стоит на порядок дороже ранней), конструкцию Definition of Done, автоматические quality gates, стратегии релиза, фича-флаги, обратимые миграции и откаты. Всё это — механика, которая позже измеряется метриками из статьи про DORA и инженерные метрики; здесь мы строим то, что там будем мерить.
Часть 1. Что такое качество и почему его нельзя «добавить в конце»
Два разных смысла слова «качество»
Джозеф Джуран в «Quality Control Handbook» развёл два определения, которые в IT постоянно путают:
- Conformance to specification — соответствие спецификации. «Работает так, как написано в требованиях». Это проверяет тестирование.
- Fitness for use — пригодность к использованию. «Решает реальную задачу пользователя». Это проверяет только сам пользователь.
Продукт может быть безупречен по первому определению и бесполезен по второму. Обратное тоже бывает: кривой прототип, который решает боль, ценнее вылизанного продукта, который никому не нужен. Управление качеством — это управление обоими смыслами: pipeline отвечает за conformance, а обратная связь от пользователей и продуктовая аналитика — за fitness. Вторую половину подробнее разбирает трек продуктового управления.
Внутреннее и внешнее качество
Мартин Фаулер в статье «Is High Quality Software Worth the Cost?» проводит ещё одну важную границу:
- Внешнее качество — то, что видит пользователь: отсутствие багов, скорость, UX. За него можно осознанно платить временем: «выкатим с известным ограничением, потом починим».
- Внутреннее качество — архитектура, тесты, читаемость кода, наблюдаемость. Пользователь его не видит никогда. И именно поэтому его так легко резать под давлением сроков.
Ключевой тезис Фаулера: экономия на внутреннем качестве окупается только на очень коротком горизонте — недели, максимум пары месяцев. Дальше кривая переворачивается: скорость команды с плохим внутренним качеством падает быстрее, чем растёт от «сэкономленного» времени. То есть внутреннее качество — не «инвестиция в будущее ценой настоящего», а условие сохранения текущей скорости.
Практический вывод для менеджера: внутреннее качество нельзя выносить в бэклог как задачу, за которую конкурируют с фичами. Оно должно быть встроено в определение готовности любой задачи — иначе оно всегда проиграет приоритизации.
Экономика позднего обнаружения
Классические цифры Барри Боэма («Software Engineering Economics», 1981) и отчёта NIST 2002 года показывают: стоимость исправления дефекта растёт примерно экспоненциально по фазе обнаружения.
Точные множители спорны — их много раз критиковали за методологию (см. разбор «The Leprechauns of Software Engineering» Лорана Боссавита, где показано, что первоисточники цифр слабее, чем принято думать). Но форма кривой не спорна, и объясняется она просто, без всякой статистики:
- Когда дефект найден через 10 минут после написания кода, у автора весь контекст в голове. Правка стоит минуты.
- Через две недели контекст утерян. Нужно заново разобраться в коде, воспроизвести баг, написать тест. Стоимость — часы.
- В проде добавляется всё остальное: инцидент, коммуникация с пользователями, хотфикс-релиз, откат данных, постмортем, репутационный ущерб. Стоимость — дни и деньги.
Отсюда принцип shift-left: сдвигать обнаружение проблем как можно раньше по конвейеру. Не «тестировать больше», а «тестировать раньше и дешевле».
Built-in quality вместо фазы тестирования
Отсюда следует главное организационное решение: тестирование — не фаза после разработки, а свойство каждого шага. Модель «разработали → отдали в QA → QA нашли 200 багов → чинили месяц» плоха не тем, что там есть QA, а тем, что она создаёт длинную петлю обратной связи и большой батч непроверенной работы. Работа QA при этом смещается: от «искать баги руками» к «проектировать проверки, автоматизировать их и находить то, что автоматика найти не может» — исследовательское тестирование, тестирование удобства, edge-cases на реальных данных.
Часть 2. Definition of Done: контракт готовности
Определение
Definition of Done (DoD) — формальный, проверяемый список условий, которым должна удовлетворять любая единица работы, чтобы считаться завершённой. Ключевые слова: формальный (записан, а не в головах), проверяемый (можно ответить да/нет без спора) и любая (применяется ко всем задачам одинаково).
Scrum Guide 2020 формулирует это жёстко: DoD — это «формальное описание состояния Инкремента, когда он соответствует стандартам качества продукта». И дальше: работа, не удовлетворяющая DoD, не может быть представлена на Sprint Review и не входит в Инкремент. То есть DoD — это не пожелание, а условие существования результата.
DoD против Acceptance Criteria против DoR
Самая частая путаница в командах. Разведём:
| Definition of Ready | Acceptance Criteria | Definition of Done | |
|---|---|---|---|
| Область | вход в работу | конкретная история | все истории одинаково |
| Кто автор | команда + владелец продукта | владелец продукта | команда (инженерные стандарты) |
| Пример | «есть макет, зависимости выявлены» | «при неверном пароле показать ошибку и не блокировать аккаунт до 5 попытки» | «покрыто тестами, прошло ревью, задеплоено в staging» |
| Меняется | редко | у каждой истории свои | раз в несколько месяцев, осознанно |
| Проверяет | достаточно ли информации, чтобы начать | сделали ли то, что нужно | сделали ли как надо |
Мнемоника: AC — про «что», DoD — про «как». История считается завершённой, только когда выполнены и её acceptance criteria, и общий DoD.
К Definition of Ready есть обоснованная критика: жёсткий DoR легко превращается в мини-водопад («история не берётся в спринт, пока аналитик не оформит её по шаблону из 12 полей»), что убивает совместное уточнение. Разумный компромисс — DoR как эвристика для обсуждения, а не как ворота с охраной. Про то, как это связано с планированием, — в статье об оценке и планировании.
Многоуровневый DoD
В любой нетривиальной системе один список не работает: условия для отдельной истории и для пользовательского релиза различаются. Поэтому DoD строят слоями — каждый внешний слой добавляет условия к внутреннему.
Смысл слоёв — в честности. Если у вас один DoD уровня истории, а перед релизом всё равно нужны нагрузочное тестирование, release notes и проверка отката, то эта работа существует, но нигде не запланирована. В LeSS её называют undone work — и она всегда всплывает в самый неудачный момент, порождая ритуал «стабилизационного спринта». Подробнее про то, как это масштабируется, — в статье про фреймворки масштабирования.
Жизненный цикл единицы работы через призму DoD
Обратите внимание на две стрелки в Undone Work. Они существуют в любой реальной команде.
Задача менеджмента — не запретить их, а сделать так, чтобы переход туда был явным решением
с записанной ценой, а не тихим срезанием угла.
DoD как код
Устный DoD деградирует за месяц. Написанный на вики — за квартал. Работает только тот, который проверяется автоматически. Начните с YAML-описания, где каждый пункт помечен способом проверки:
# quality/dod.yml — единый источник правды о готовности
version: 3
levels:
story:
- id: code-review
text: "Изменения одобрены минимум одним инженером, не автором"
check: automated # проверяется правилом защиты ветки
- id: unit-tests
text: "Новая логика покрыта тестами; покрытие изменённых строк >= 80%"
check: automated # diff-coverage gate в CI
- id: static-analysis
text: "Линтер и статический анализатор без новых нарушений"
check: automated
- id: observability
text: "Для новых путей исполнения добавлены структурные логи и метрики"
check: manual # пункт чек-листа в шаблоне merge request
- id: ac-demo
text: "Acceptance criteria продемонстрированы владельцу продукта"
check: manual
feature:
- id: e2e
text: "Сквозной сценарий проходит на staging"
check: automated
- id: flag
text: "Фича-флаг создан, протестирован в обоих положениях, есть план его удаления"
check: manual
- id: docs
text: "Пользовательская документация и справка поддержки обновлены"
check: manual
release:
- id: load-test
text: "Нагрузочный тест на профиле прод-трафика без деградации p99"
check: automated
- id: rollback-drill
text: "План отката выполнен на стенде в этом релизном цикле"
check: manual
- id: slo-alerts
text: "Алерты на SLO новых компонентов заведены и протестированы"
check: automated
Дальше — маленький валидатор, который CI запускает на merge request. Он не заменяет людей, но убирает у пунктов статус «мы вроде бы это делаем»:
"""Проверка DoD уровня story для merge request.
Сложность: O(n) по числу пунктов DoD и O(m) по числу изменённых строк diff.
Память: O(n + m). На практике время работы определяется вызовами API, а не вычислениями.
"""
from dataclasses import dataclass
import sys
import yaml
@dataclass(frozen=True)
class Item:
id: str
text: str
check: str # "automated" | "manual"
@dataclass(frozen=True)
class MergeRequest:
approvals: int # число одобрений от не-авторов
diff_coverage: float # покрытие изменённых строк, 0..1
new_lint_violations: int
checked_manual: frozenset[str] # id пунктов, отмеченных галочкой в описании MR
def load_level(path: str, level: str) -> list[Item]:
with open(path, encoding="utf-8") as f:
data = yaml.safe_load(f)
return [Item(**it) for it in data["levels"][level]]
# Реестр автоматических проверок: id пункта -> предикат.
AUTOMATED = {
"code-review": lambda mr: mr.approvals >= 1,
"unit-tests": lambda mr: mr.diff_coverage >= 0.80,
"static-analysis": lambda mr: mr.new_lint_violations == 0,
}
def evaluate(items: list[Item], mr: MergeRequest) -> list[tuple[Item, bool]]:
"""Возвращает список (пункт, выполнен) в порядке объявления в DoD."""
result = []
for item in items:
if item.check == "automated":
predicate = AUTOMATED.get(item.id)
# Пункт объявлен автоматическим, но проверки нет — это ошибка конфигурации,
# а не повод считать его выполненным. Fail closed.
passed = predicate(mr) if predicate else False
else:
passed = item.id in mr.checked_manual
result.append((item, passed))
return result
def main() -> int:
mr = MergeRequest(
approvals=1,
diff_coverage=0.86,
new_lint_violations=0,
checked_manual=frozenset({"ac-demo"}), # observability забыли отметить
)
report = evaluate(load_level("quality/dod.yml", "story"), mr)
failed = [item for item, ok in report if not ok]
for item, ok in report:
print(f"{'PASS' if ok else 'FAIL'} [{item.check[:4]}] {item.id}: {item.text}")
if failed:
print(f"\nDoD не выполнен: {len(failed)} из {len(report)} пунктов.")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
Два принципиальных решения в этом коде:
- Fail closed. Если пункт помечен как автоматический, но проверки для него нет, он считается проваленным. Иначе DoD тихо разлагается: кто-то добавил пункт, забыл проверку, и год все думают, что он работает.
- Ручные пункты остаются в том же списке. Их нельзя автоматизировать сегодня, но их видно рядом с автоматическими — и на ретроспективе понятно, что стоит автоматизировать следующим.
Антипаттерны DoD
- DoD-фикция. Список из 15 пунктов, который никто не читал после воркшопа. Лечится сокращением до 5–7 пунктов, которые команда реально готова соблюдать всегда.
- DoD как дубина. Менеджер использует DoD, чтобы отчитывать команду. Результат — команда занижает DoD или врёт. DoD принадлежит команде; менеджер отвечает за то, чтобы у команды было время его соблюдать.
- Разный DoD у разных людей. «У Пети без тестов, потому что он быстрый» — гарантированный путь к тому, что через год модуль Пети нельзя трогать.
- DoD, зависящий от внешней команды. Пункт «отдел безопасности провёл аудит» превращает готовность в лотерею. Либо втягивайте компетенцию в команду, либо выносите пункт на уровень релиза с явным SLA от смежников. Про управление такими зависимостями — в статье о рисках.
- «Done» без деплоя. Если «готово» означает «код в ветке», между готовностью и ценностью остаётся невидимый разрыв, который никто не измеряет.
Часть 3. Quality gates: конвейер, который не пускает плохое дальше
DoD, который проверяется людьми, — это чек-лист. DoD, который проверяется конвейером, — это система. Схема типичного пайплайна с воротами:
цель: < 5 минут"] B -->|красный| R1["Автор чинит немедленно
контекст ещё в голове"] B -->|зелёный| C["Ревью человеком:
дизайн, читаемость, риски"] C --> D["Интеграционные тесты
и сборка артефакта"] D -->|красный| R1 D -->|зелёный| E["Слияние в trunk"] E --> F["Ворота релиза: E2E, нагрузка,
сканирование зависимостей"] F -->|красный| R2["Trunk сломан:
остановка слияний до починки"] F -->|зелёный| G["Артефакт готов к деплою
иммутабельный, с версией"] G --> H["Прогрессивная выкатка
canary → 5% → 50% → 100%"] H -->|SLO нарушен| R3["Автоматический откат
или выключение флага"] H -->|SLO в норме| I["Релиз завершён"] style R1 fill:#e0902f,fill-opacity:0.25 style R2 fill:#d8504a,fill-opacity:0.25 style R3 fill:#d8504a,fill-opacity:0.25 style I fill:#46a758,fill-opacity:0.25
Порядок ворот определяется стоимостью, а не важностью
Главный принцип проектирования конвейера: дешёвые и быстрые проверки идут первыми. Не потому что они важнее, а потому что вероятность потратить дорогой ресурс (30-минутный E2E-прогон, время ревьюера) на заведомо сломанный код должна быть минимальной.
Формально: если проверка $i$ стоит $c_i$ и отсеивает долю дефектов $p_i$, оптимальный порядок — по убыванию $p_i / c_i$. Это тот же жадный принцип, что в задаче о планировании с весами, и он же лежит в основе WSJF-приоритизации из оценки и планирования.
Пирамида тестов и её честная критика
Классическая пирамида (много юнит-тестов, меньше интеграционных, совсем мало E2E) — эвристика про стоимость обратной связи, а не догма. У неё есть два известных возражения:
- «Testing Trophy» Кента Доддса: в веб-разработке основная масса должна приходиться на интеграционные тесты, потому что юнит-тесты с моками проверяют в основном ваши моки.
- Тесты, привязанные к реализации, вредны. Тест, который ломается при каждом рефакторинге, не защищает поведение, а препятствует изменению. Хороший критерий: тест должен ломаться только тогда, когда меняется наблюдаемое поведение.
Практическая формулировка, которая работает вне зависимости от формы фигуры: чем выше уровень теста, тем медленнее и нестабильнее обратная связь — значит, тестов этого уровня должно быть ровно столько, сколько нужно, чтобы поймать классы дефектов, которые не ловятся ниже.
Flaky-тесты: почему они убивают ворота
Нестабильный тест — самый дорогой актив в CI, потому что он разрушает сам смысл ворот. Посчитаем. Пусть в наборе $n$ тестов, каждый независимо падает ложно с вероятностью $f$. Вероятность полностью зелёного прогона:
$$P(\text{зелёный}) = (1 - f)^n$$
def green_run_probability(n: int, flake_rate: float) -> float:
"""Вероятность, что прогон из n тестов зелёный при доле ложных падений flake_rate."""
return (1.0 - flake_rate) ** n
for n in (200, 1_000, 5_000):
for f in (0.0001, 0.001, 0.01):
p = green_run_probability(n, f)
print(f"n={n:5d} f={f:<7} P(зелёный)={p:6.1%} ожид. перезапусков={1 / p - 1:5.1f}")
Результат отрезвляющий:
| Тестов | f = 0,01 % | f = 0,1 % | f = 1 % |
|---|---|---|---|
| 200 | 98,0 % | 81,9 % | 13,4 % |
| 1 000 | 90,5 % | 36,8 % | 0,004 % |
| 5 000 | 60,7 % | 0,7 % | ≈ 0 % |
При 1000 тестов и всего 1 % нестабильности зелёный прогон — событие практически невозможное. Команда начинает перезапускать pipeline «на удачу», и в этот момент ворота перестают существовать: красный сигнал больше не означает поломку, значит, его перестают читать. Это классическая десенсибилизация к алертам, ровно как в мониторинге.
Что с этим делать:
- Измерять flake rate как продуктовую метрику: доля прогонов, где тест упал, а повторный запуск на том же коммите прошёл.
- Карантин: нестабильный тест автоматически выводится из блокирующего набора и заводится как дефект с владельцем и сроком. Не «удаляется навсегда» — иначе карантин станет свалкой.
- Бюджет: если в карантине больше N тестов, слияния новых фич останавливаются. Так делают в Google — подробно в главе про CI в «Software Engineering at Google» (глава 23, доступна бесплатно).
Часть 4. Дефекты: учёт, приоритизация, метрики
Severity против Priority
Две независимые оси, которые постоянно склеивают в одну:
- Severity — техническая тяжесть последствий. Свойство дефекта. Определяет инженер или QA.
- Priority — срочность исправления. Свойство бизнес-контекста. Определяет владелец продукта.
Опечатка на главной странице лендинга — низкая severity, но может быть высочайший priority. Падение сервиса для трёх внутренних пользователей раз в месяц — высокая severity, низкий priority. Склеивание осей приводит к вечному спору «это баг первого приоритета!» вместо разговора о рисках.
Метрики качества, которые не врут
"""Метрики качества по релизам. Все — O(n) по числу дефектов, память O(1) на метрику."""
from dataclasses import dataclass
from datetime import date
@dataclass(frozen=True)
class Defect:
id: str
found_in: str # "dev" | "review" | "ci" | "staging" | "production"
severity: int # 1 — критический, 4 — косметический
opened: date
closed: date | None
def escaped_defect_rate(defects: list[Defect]) -> float:
"""Доля дефектов, дошедших до прода. Главная метрика эффективности ворот.
Не путать с «числом багов в проде»: абсолютное число зависит от объёма релиза,
а доля — от качества конвейера.
"""
if not defects:
return 0.0
escaped = sum(1 for d in defects if d.found_in == "production")
return escaped / len(defects)
def defect_removal_efficiency(defects: list[Defect]) -> float:
"""DRE = доля дефектов, пойманных ДО прода. Классика Каперса Джонса.
Ориентир зрелой команды: 0.85–0.95. Значение 1.0 почти всегда означает,
что в проде баги просто не заводят как дефекты.
"""
return 1.0 - escaped_defect_rate(defects)
def phase_containment(defects: list[Defect]) -> dict[str, float]:
"""Распределение обнаружения по фазам: показывает, где именно течёт конвейер."""
total = len(defects) or 1
phases = ("dev", "review", "ci", "staging", "production")
return {p: sum(1 for d in defects if d.found_in == p) / total for p in phases}
def mttr_days(defects: list[Defect], severity: int = 1) -> float | None:
"""Среднее время до закрытия дефектов заданной severity, в днях.
Медиану считать полезнее среднего: распределение времени починки
почти всегда с тяжёлым правым хвостом.
"""
closed = [(d.closed - d.opened).days for d in defects
if d.severity == severity and d.closed is not None]
return sum(closed) / len(closed) if closed else None
Отдельно про опасные метрики, которые ломают поведение при попытке ими управлять (закон Гудхарта в чистом виде):
- Число найденных багов на тестировщика. Приводит к дроблению одного дефекта на пять карточек.
- Процент покрытия кода как цель. Приводит к тестам без ассертов. Покрытие полезно как диагностика непокрытых мест, вредно как KPI. Разумный компромисс — покрытие изменённых строк (diff coverage) как ворота, а не общее покрытие как цель.
- Ноль открытых багов. Приводит к массовому закрытию с резолюцией «не воспроизводится».
Подробнее про то, как метрики деформируют поведение, — в статье про DORA и в разделе о мотивации статьи про команду и коммуникации.
Часть 5. Релизный менеджмент
Главная идея: размер батча
Интуиция менеджера обычно такая: «релизы рискованные, значит, надо релизить реже и тщательнее готовиться». Это ровно наоборот. Разберём почему.
Пусть в релизе $n$ изменений, каждое независимо ломает прод с вероятностью $q$. Вероятность проблемного релиза: $1-(1-q)^n$ — растёт с размером батча. Но хуже другое: при отказе вы не знаете, какое из $n$ изменений виновато. Стоимость диагностики растёт примерно как $O(n)$ при линейном поиске и в лучшем случае как $O(\log n)$ при бисекции. А риск отката растёт ещё быстрее: откатить один релиз с 200 изменениями означает откатить 199 работающих.
| Релиз раз в квартал | Релиз ежедневно | |
|---|---|---|
| Изменений в батче | ~500 | ~5 |
| Вероятность инцидента | высокая | низкая |
| Время локализации причины | часы–дни | минуты |
| Стоимость отката | огромная (тянет всё) | почти нулевая |
| Стресс команды | пик перед релизом | ровный фон |
| Обратная связь от пользователей | через 3 месяца | завтра |
Это тот же аргумент про размер партии, что в статье про Kanban и поток: маленькие частые батчи снижают и время цикла, и риск одновременно. Данные из «Accelerate» (Форсгрен, Хамбл, Ким) это подтверждают эмпирически: элитные команды деплоят чаще И имеют меньшую долю неудачных изменений. Это не компромисс между скоростью и качеством — это одна и та же переменная.
Стратегии релиза: где вы находитесь
Ключ к правому верхнему квадранту — не «релизить смелее», а отделить деплой от релиза и построить автоматические механизмы обнаружения и отката. Без них рост частоты действительно двигает вас в правый нижний квадрант.
Ветвление: release branches против trunk-based
Что видно на схеме: долгоживущая релизная ветка порождает двойную работу — каждый фикс нужно применить и туда, и в main, а обратные слияния конфликтуют тем сильнее, чем дольше живёт ветка. Это цена, которую платят за возможность «заморозить» состояние.
Альтернатива — trunk-based development: короткоживущие ветки (часы, максимум день), всё сливается в trunk, релиз — это тег на trunk, а незавершённая функциональность скрыта фича-флагами. Каноническое описание — trunkbaseddevelopment.com Пола Хаммонда и Стива Смита; исследование в «Accelerate» показывает статистически значимую связь trunk-based практик с производительностью доставки.
Trunk-based не бесплатен. Он требует:
- быстрого и надёжного CI (иначе сломанный trunk блокирует всех);
- дисциплины разбиения работы на маленькие безопасные шаги;
- инфраструктуры фича-флагов и дисциплины их удаления;
- культуры, где сломать trunk — не преступление, а сигнал починить конвейер.
Если этого нет, release branches — честный промежуточный вариант. Плохо не иметь релизных веток, плохо иметь их и не двигаться в сторону сокращения их жизни.
Deploy ≠ Release: фича-флаги
Самый недооценённый рычаг в релизном менеджменте — разведение двух событий:
- Deploy — код оказался на проде. Техническое событие, риск управляется инфраструктурой.
- Release — функциональность стала доступна пользователям. Бизнес-событие, риск управляется продуктом.
Фича-флаг делает их независимыми. Следствия огромны: код едет в прод ежедневно маленькими кусками (низкий риск деплоя), а включение фичи — отдельное решение, обратимое за секунды без пересборки.
"""Минимальный, но продакшн-пригодный движок фича-флагов.
Ключевые свойства:
* детерминированность — один и тот же пользователь всегда получает одно решение;
* равномерность — хеш распределяет пользователей без перекоса;
* независимость флагов — соль флага в хеше, иначе одни и те же 5% пользователей
попадают в canary каждой фичи и получают весь риск системы.
Сложность: O(1) на проверку, O(1) памяти. Реально узкое место — доставка конфигурации.
"""
import hashlib
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Flag:
key: str
enabled: bool = False # глобальный рубильник (kill switch)
percentage: float = 0.0 # доля пользователей, 0..1
allowlist: frozenset[str] = field(default_factory=frozenset) # всегда включено
denylist: frozenset[str] = field(default_factory=frozenset) # всегда выключено
def _bucket(flag_key: str, user_id: str) -> float:
"""Стабильное отображение (флаг, пользователь) -> [0, 1)."""
digest = hashlib.sha256(f"{flag_key}:{user_id}".encode()).digest()
return int.from_bytes(digest[:8], "big") / 2 ** 64
def is_enabled(flag: Flag, user_id: str) -> bool:
if user_id in flag.denylist: # denylist сильнее всего: аварийное исключение
return False
if not flag.enabled: # kill switch — один вызов гасит фичу целиком
return False
if user_id in flag.allowlist: # внутренние пользователи, dogfooding
return True
return _bucket(flag.key, user_id) < flag.percentage
if __name__ == "__main__":
flag = Flag(key="new-checkout", enabled=True, percentage=0.05,
allowlist=frozenset({"qa-01"}))
users = [f"user-{i}" for i in range(100_000)]
on = sum(is_enabled(flag, u) for u in users)
print(f"включено у {on / len(users):.2%} пользователей") # ~5%
# Детерминированность: повторный вызов даёт тот же ответ
assert is_enabled(flag, "user-42") == is_enabled(flag, "user-42")
Главная опасность фича-флагов — их накопление. Каждый флаг удваивает число путей исполнения: $k$ независимых флагов дают $2^k$ комбинаций, из которых тестируется хорошо если две. Флаг, проживший год, — это неудалённый мёртвый код и мина в логике. Дисциплина:
- у каждого флага есть владелец и дата истечения прямо в конфигурации;
- CI ругается на флаги старше 60–90 дней;
- удаление флага — часть DoD фичи, а не «потом когда-нибудь»;
- флаги релизные (временные) и операционные (kill switch, живут долго) разводятся явно.
Каноническое описание типов флагов — у Пита Ходжсона: «Feature Toggles (aka Feature Flags)».
Прогрессивная выкатка и автоматический откат
окно 10 минут, статистический тест alt Отклонение в пределах порога Mon-->>CI: canary здоров CI->>LB: поднять до 5%, затем 25%, 50%, 100% CI->>Old: вывести старую версию else error rate выше базовой на 2 сигмы Mon-->>CI: ТРЕВОГА: деградация CI->>LB: вернуть 100% трафика на стабильную версию CI->>New: остановить canary Note over CI,Mon: время до отката — секунды,
пользователей задето ~1% end
Три свойства, без которых canary — театр:
- Сравнение с одновременно работающей базовой линией, а не с историческими данными. Иначе вы измеряете суточную сезонность, а не эффект релиза.
- Метрики включают бизнес-показатели. Технически здоровый релиз, обнуливший конверсию в оплату, — провал, который error rate не покажет.
- Откат автоматический. Если решение принимает дежурный инженер ночью, среднее время отката измеряется десятками минут вместо секунд.
Обзор стратегий — в главе про release engineering «Site Reliability Engineering» (Google, доступна бесплатно).
Сравнение основных стратегий:
| Стратегия | Стоимость инфраструктуры | Скорость отката | Ограничение |
|---|---|---|---|
| Rolling update | низкая | минуты | обе версии в проде одновременно — нужна совместимость |
| Blue-green | двойная (два полных окружения) | секунды | дорого; проблема с состоянием и БД |
| Canary | средняя | секунды | нужен зрелый мониторинг и статистика |
| Feature flag | почти нулевая | миллисекунды | только для того, что скрыто за флагом |
| Shadow traffic | высокая | не применимо | только чтение; побочные эффекты недопустимы |
Откат невозможен без обратимых миграций
Самое частое место, где «у нас есть план отката» разбивается о реальность, — база данных. Откатить код за 30 секунд легко. Откатить миграцию, удалившую колонку, — нельзя: данные уже потеряны.
Решение — паттерн expand / contract (он же parallel change): любое несовместимое изменение схемы разбивается на серию совместимых шагов, между которыми старая и новая версии кода работают одновременно.
-- Задача: переименовать users.email -> users.email_address
-- Наивный вариант ломает откат: ALTER TABLE users RENAME COLUMN email TO email_address;
-- Старая версия кода после отката упадёт на несуществующей колонке.
-- Шаг 1 (EXPAND). Релиз N. Добавляем новую колонку, ничего не ломая.
ALTER TABLE users ADD COLUMN email_address TEXT;
-- Шаг 2. Релиз N. Код пишет в обе колонки, читает пока из старой.
-- Обратная засыпка старых данных — батчами, чтобы не залочить таблицу.
UPDATE users SET email_address = email
WHERE email_address IS NULL AND id BETWEEN :lo AND :hi;
-- Шаг 3. Релиз N+1 (после того как засыпка завершена и проверена).
-- Код читает из новой колонки, продолжает писать в обе.
-- В этой точке откат к релизу N полностью безопасен.
-- Шаг 4 (CONTRACT). Релиз N+2, спустя период, гарантирующий,
-- что откат к N уже не понадобится (обычно 1-2 недели).
ALTER TABLE users DROP COLUMN email;
Правила, которые стоит зафиксировать в DoD уровня релиза:
- миграции только аддитивные в том же релизе, что и код, который их использует;
- удаление чего-либо — минимум через один релиз после того, как это перестали читать;
- каждая миграция сопровождается оценкой времени блокировки на объёме прод-данных;
- для больших таблиц — онлайн-инструменты (
pg_repack,gh-ost,pt-online-schema-change).
Подробный каталог таких приёмов — в «Refactoring Databases» Скотта Эмблера и Прамодкумара Садаладжа.
Rollback против roll-forward
Два способа реагировать на плохой релиз:
- Rollback — вернуть предыдущую версию. Быстро, предсказуемо, требует обратной совместимости данных и API. Стандарт по умолчанию.
- Roll-forward — выкатить исправление вперёд. Требует очень быстрого конвейера (минуты от коммита до прода) и уверенности в диагнозе.
Практическое правило: rollback — рефлекс, roll-forward — решение. Во время инцидента приоритет — остановить ущерб, а не понять причину. Диагностика после восстановления. Если откат в вашей системе технически невозможен, это не «особенность архитектуры», а незакрытый риск первого приоритета.
Версионирование и release notes
Semantic Versioning 2.0.0 даёт контракт MAJOR.MINOR.PATCH:
несовместимое изменение — обратно совместимая функциональность — исправление.
Для приложений (не библиотек) semver часто избыточен — там разумнее CalVer (2026.07.3)
или просто монотонный номер сборки. Важно другое: версия артефакта иммутабельна,
и по ней однозначно восстанавливается коммит, зависимости и конфигурация. Без этого
расследование инцидента превращается в археологию.
Release notes — не бюрократия, а входные данные для поддержки и для будущего вас. Минимум: что изменилось для пользователя, какие флаги включены, какие миграции применены, как откатить, кто дежурный. Хорошая практика — генерировать черновик из conventional commits, а редактировать руками.
Часть 6. Go / no-go: как принимать решение о релизе
Ритуал «релизного комитета», где восемь человек час обсуждают, готова ли сборка, — признак того, что готовность не определена операционально. Правильная конструкция: DoD релиза уже описал критерии, конвейер их проверил, а встреча (если она вообще нужна) занимает пять минут и обсуждает исключения, а не норму.
Что должно быть на руках к моменту решения:
- Отчёт конвейера: все автоматические ворота релизного уровня зелёные.
- Список известных дефектов, вошедших в релиз, с явным решением по каждому.
- Подтверждение, что план отката исполним (в идеале — исполнялся на стенде в этом цикле).
- Дежурный назначен и доступен, окно выкатки не упирается в его конец рабочего дня.
- Понятно, какие метрики смотреть первые 30 минут и какой порог означает откат.
Про code freeze стоит сказать отдельно. Заморозка кода — это не мера качества, а компенсация отсутствия автоматических ворот. Она создаёт очередь готовой работы (растёт размер батча первого релиза после разморозки — то есть риск), провоцирует «давайте успеем протолкнуть до заморозки» (то есть спешку в самый опасный момент) и ничего не проверяет по существу. Если заморозка нужна из-за регуляторных требований — это законно; если из-за того, что «иначе страшно» — это симптом, который лечится конвейером.
Аналогично релиз в пятницу: сама по себе пятница не опасна. Опасны релизы, которые нельзя быстро откатить, и отсутствие людей, способных отреагировать. Команда, у которой откат занимает 30 секунд и работает автоматика, релизит в пятницу спокойно. Запрет на пятничные релизы — разумный костыль ровно до тех пор, пока вы чините причину.
Часть 7. Типичные ошибки
- DoD не включает деплой. «Готово» = «код в ветке». Между готовностью и ценностью остаётся невидимый разрыв, который не попадает ни в одну метрику.
- Отдельная фаза стабилизации. Регулярный «спринт стабилизации» — это признание, что предыдущие спринты производили не готовый продукт, а его черновик.
- QA как ворота вместо конвейера. Один человек, который «пропускает» релиз, — узкое место и единственная точка отказа; при этом он физически не может проверить всё.
- Игнорирование flaky-тестов. Приводит к культуре «перезапусти, обычно проходит», после чего красный CI перестаёт что-либо значить.
- Покрытие как KPI. Порождает тесты без ассертов и ложное чувство защищённости.
- Долгоживущие фича-ветки. Чем дольше живёт ветка, тем дороже слияние — стоимость растёт нелинейно, потому что растёт и объём изменений, и вероятность конфликта.
- Свалка фича-флагов. Через два года никто не знает, что произойдёт при выключении флага
enable_new_billing_v2_final. - Необратимые миграции. План отката существует на бумаге и не работает в момент, когда нужен.
- Ручной чек-лист релиза на 40 пунктов. Человек, который делает это в 40-й раз, пропускает пункты автоматически. Всё, что в чек-листе повторяется, должно быть скриптом.
- Метрики качества, используемые для оценки людей. Гарантированно приводят к игре с цифрами вместо улучшения качества.
- Нет владельца релиза. «Все отвечают» означает «никто не отвечает» в момент инцидента.
- Релиз как событие, а не как рутина. Чем реже вы это делаете, тем хуже вы это умеете.
Часть 8. Как это выглядит в проде
Google. Релизная инженерия — отдельная дисциплина с выделенными инженерами (SRE-книга, глава Release Engineering). Принципы: герметичные (воспроизводимые) сборки, все изменения проходят через ревью, автоматизация всего, что повторяется. Отдельно — политика по flaky-тестам и огромные вложения в скорость CI, потому что скорость обратной связи считается стратегическим активом.
Facebook/Meta. Исторический путь от еженедельных «push» к квази-непрерывной доставке описан в статье «Continuous Deployment at Facebook and OANDA» (Savor et al., ICSE 2016). Главный вывод исследования: рост частоты деплоев на два порядка не привёл к росту доли неудачных изменений — потому что параллельно рос уровень автоматизации и прогрессивных выкаток (сначала на сотрудников, потом на 2 % пользователей, потом на всех).
Etsy. Каноническая история про «деплой десятки раз в день» и культуру безвинных постмортемов. Ключевая практика — каждый новый инженер деплоит в прод в первый день работы: это одновременно и обучение, и проверка того, что конвейер действительно безопасен.
Amazon. Многие тысячи деплоев в день достигаются не героизмом, а декомпозицией: маленькие независимо развёртываемые сервисы, у каждого своя pipeline, свои флаги и своя ответственность команды. Это архитектурное решение в поддержку релизного процесса — см. трек архитектурных паттернов.
Регулируемые домены (финтех, медицина, авиация). Здесь встречается искреннее возражение: «у нас нельзя релизить часто, у нас аудит». На практике аудит требует прослеживаемости и контроля, а не редкости. Автоматизированный конвейер с иммутабельными артефактами, подписанными сборками и полным логом одобрений даёт аудитору больше, чем письмо с темой «согласовано» от начальника отдела. Требование разделения обязанностей закрывается обязательным ревью другим человеком, а не ручным деплоем.
Мини-итог
- Качество бывает внешним (видно пользователю, можно осознанно занять в долг) и внутренним (не видно никогда, занимать нельзя — это условие сохранения скорости).
- Стоимость дефекта растёт нелинейно с задержкой обнаружения, поэтому проверки двигают влево, а не наращивают в конце.
- Definition of Done — формальный проверяемый контракт готовности, принадлежащий команде. Строится слоями (история → фича → релиз), максимально автоматизируется, fail closed. Всё, что не покрыто ни одним слоем, — undone work, то есть скрытый долг.
- Quality gates выстраиваются по убыванию отношения «пойманные дефекты / стоимость проверки». Flaky-тесты уничтожают смысл ворот и требуют карантина с бюджетом.
- Размер батча — главный рычаг релизного риска: маленькие частые релизы одновременно быстрее и безопаснее. Это не компромисс, а одна переменная.
- Deploy ≠ release. Фича-флаги разводят техническое и продуктовое события; за флаги надо платить дисциплиной удаления.
- Откат должен быть рефлексом. Он невозможен без обратимых миграций (expand/contract) и иммутабельных версионированных артефактов.
- Code freeze и запрет пятничных релизов — костыли, компенсирующие отсутствие конвейера; допустимы как временная мера, вредны как постоянная политика.
Источники
- Scrum Guide 2020 — определение Definition of Done и Инкремента.
- Jez Humble, David Farley. «Continuous Delivery» — базовая книга про deployment pipeline, обратимые релизы и автоматизацию.
- Nicole Forsgren, Jez Humble, Gene Kim. «Accelerate» — эмпирическая связь trunk-based, автоматизации тестов и производительности доставки.
- Google. «Site Reliability Engineering», глава Release Engineering — бесплатно онлайн.
- Titus Winters et al. «Software Engineering at Google» — главы про тестирование, CI и борьбу с нестабильными тестами.
- Martin Fowler. «Is High Quality Software Worth the Cost?»
- Pete Hodgson. «Feature Toggles» — таксономия флагов и их жизненный цикл.
- trunkbaseddevelopment.com — практики, ограничения и миграция на trunk-based.
- Scott Ambler, Pramod Sadalage. «Refactoring Databases» — expand/contract и эволюция схемы.
- Savor et al. «Continuous Deployment at Facebook and OANDA», ICSE 2016.
- Laurent Bossavit. «The Leprechauns of Software Engineering» — критический разбор «общеизвестных» цифр индустрии, включая кривую стоимости дефекта.
- Semantic Versioning 2.0.0
Что дальше
Мы построили механику: определили готовность, поставили ворота, научились выкатывать маленькими безопасными шагами и откатываться. Логичный следующий вопрос — как понять, что всё это работает, и как измерять улучшение, не скатившись в игру с цифрами.
Читайте дальше: DORA и метрики инженерной эффективности — четыре ключевые метрики (deployment frequency, lead time for changes, change failure rate, time to restore service), их корректный расчёт, ловушки интерпретации и связь с бизнес-результатами.