Эксплуатация и эволюция: жизнь системы после релиза
Двенадцать предыдущих глав вели к моменту, когда код доезжает до пользователя. Кажется, что это финиш. На самом деле это стартовая черта самой длинной фазы: система, которая пережила первый выкат, дальше живёт годами — и почти всё, что с ней произойдёт, произойдёт именно теперь. Эта глава — про понедельник инженера, у которого система в проде уже два года: не про идеальный запуск, а про накопившиеся алерты, про базу, где схема на два поля шире, чем нужно, про модуль, который все обходят стороной, и про эндпоинт, который никто не решается удалить, потому что непонятно, кто им ещё пользуется.
Разграничение сразу, чтобы не тратить ваше время. Трек sre разбирает надёжность как дисциплину — шестнадцать глав про SLO, дежурства, ёмкость и режимы отказа. Глава Наблюдаемость и дежурства — про инструменты и их настройку. Легаси-код — про технику безопасных изменений. Технический долг — про то, как долг выглядит из кресла руководителя. Здесь — взгляд прикладного инженера: какие решения вы принимаете сами, каким языком разговариваете с продуктом и что делаете руками, когда система уже работает.
Стоимость владения: почему сопровождение — большая часть жизни системы
Разработка нового кода — короткий яркий эпизод в биографии системы; всё остальное время её сопровождают. Цифра «60–80 % стоимости жизненного цикла приходится на сопровождение» кочует по презентациям без источника, поэтому назовём источники. Барри Боэм в «Software Engineering Economics» (1981) на данных проектов TRW получил 40–60 %. Беннет Линц и Бартон Суонсон в исследовании 487 организаций («Software Maintenance Management», 1980) показали не только объём, но и структуру: больше половины усилий по «сопровождению» — это вообще не починка багов, а доработка под новые требования. Роберт Гласс в «Facts and Fallacies of Software Engineering» (2002) сводит накопленные оценки в диапазон 40–80 % и добавляет ключевое наблюдение: около 60 % этих расходов — развитие функциональности, а не исправление ошибок.
Отсюда первый честный вывод: «сопровождение» — плохое слово. Оно звучит как уборка за собой, а на деле это основной способ создания ценности. Меир Леман это формализовал в законах эволюции ПО. Три из восьми законов стоит помнить наизусть:
- Непрерывное изменение. Система класса E — та, что решает задачу реального мира, — обязана меняться, иначе становится всё менее полезной. Мир не стоит на месте, и «замороженная» система деградирует относительно него, даже если ни строки в ней не поменялось.
- Растущая сложность. С каждым изменением сложность растёт, если специально не тратить силы на её снижение. Это не лень команды, а свойство процесса.
- Убывающее качество. Воспринимаемое качество падает, если система не адаптируется к изменениям среды.
Второй закон — прямое обоснование того, почему бюджет на рефакторинг не роскошь: без него сложность растёт монотонно. ISO/IEC/IEEE 14764 делит сопровождение на четыре вида, и это деление полезно не для отчётности, а чтобы честно называть, чем вы заняты:
| Вид | Что это | Пример | Кто обычно инициирует |
|---|---|---|---|
| Корректирующее | починка обнаруженных дефектов | падение при пустом списке заказов | поддержка, пользователи |
| Адаптивное | подстройка под изменившееся окружение | база подняла мажорную версию, библиотека выкинула API | внешние обстоятельства |
| Совершенствующее | новые требования и улучшение характеристик | новый фильтр в отчёте, ускорение выгрузки | продукт |
| Превентивное | снижение будущей стоимости изменений | покрытие тестами, разбиение монстра, обновление зависимостей | инженеры |
Диапазон между командами огромный, но пропорция обычно похожая: корректирующее — меньшая часть, совершенствующее — большая, превентивное — то, что урезают первым и о чём потом жалеют. Про место сопровождения в общем жизненном цикле — глава Поддержка и техдолг.
Обратите внимание: вопрос меняется. В первый месяц вы спрашиваете «работает ли», на втором году — «сколько стоит изменить», на пятом — «можно ли это выключить». Инструменты для ответа на эти вопросы разные, и вся глава про них.
Наблюдаемость: как увидеть систему, которую нельзя остановить
Отладчик в проде поставить нельзя. Перезапустить с логами уровня DEBUG — обычно тоже нельзя. Остаётся то, что система рассказала о себе сама, пока работала: это и есть телеметрия, а способность по ней восстановить внутреннее состояние — наблюдаемость. Термин пришёл из теории управления: Рудольф Калман в 1960 году назвал систему наблюдаемой, если её внутреннее состояние восстановимо по внешним измерениям за конечное время. В инженерной практике определение мягче и полезнее: система наблюдаема, если вы можете задать ей новый, заранее не предусмотренный вопрос и получить ответ, не выкатывая новую версию.
Три сигнала не взаимозаменяемы. У каждого своя работа и своя слепая зона:
| Сигнал | Отвечает на вопрос | Чего не может | Стоимость |
|---|---|---|---|
| Метрики | «Плохо ли сейчас и насколько» | сказать, какому пользователю и почему | низкая, растёт по кардинальности |
| Логи | «Что именно случилось в этом конкретном случае» | дать общую картину без агрегации | высокая, линейна по объёму |
| Трассировки | «Где в цепочке сервисов ушло время и что чей вызов» | показать редкий случай, если он не попал в выборку | средняя, зависит от семплирования |
Типичный рабочий цикл: метрика показала, что плохо → трассировка показала, в каком сервисе → лог показал, что именно там произошло. Каждый сигнал сужает поиск, но переход между ними работает, только если они связаны общим идентификатором. Устройство сигналов, кардинальность и семплирование разобраны в главах Мониторинг и Наблюдаемость распределённых систем; дальше — только то, что делает руками прикладной инженер.
Структурированные логи и почему print в проде бесполезен
print(f"user {uid} failed to pay") в проде — это не лог, а мусор. Причины конкретные:
- Нельзя искать по полю. Вы можете грепать подстроку, но не можете спросить «все ошибки оплаты для тарифа business за последний час с длительностью больше секунды».
- Нельзя агрегировать. Посчитать долю неудач по причине — это регулярка поверх свободного текста, которая сломается при первой же правке формулировки.
- Нет контекста. В
printнет версии сборки, инстанса, идентификатора запроса. В момент разбора инцидента вы не поймёте, к какому выкату относится строка. - Нет уровня и семплирования. Один цикл по десяти тысячам позиций заказа кладёт диск и счёт за хранение.
Структурированный лог — это событие с полями, сериализованное в JSON. Пишете вы его так же удобно, а читает его машина.
# app/observability.py — обвязка: структурные логи + контекст запроса + трассировка.
import contextvars, logging, uuid
import structlog
from opentelemetry import trace
from opentelemetry.trace import SpanKind, Status, StatusCode
# Идентификатор запроса живёт в contextvars: виден любому коду ниже по стеку,
# в том числе внутри async-задач, и не требует таскать его параметром через десять слоёв.
request_id_var: contextvars.ContextVar[str] = contextvars.ContextVar("request_id", default="-")
tracer = trace.get_tracer("checkout")
def add_request_context(_logger, _method, event_dict):
"""Процессор structlog: подмешивает в КАЖДОЕ событие корреляционные поля."""
event_dict["request_id"] = request_id_var.get()
ctx = trace.get_current_span().get_span_context()
if ctx.is_valid:
# Те же trace_id/span_id, что и в трассировке, — это и есть шов между сигналами.
event_dict["trace_id"] = format(ctx.trace_id, "032x")
event_dict["span_id"] = format(ctx.span_id, "016x")
return event_dict
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
add_request_context,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.dict_tracebacks, # исключение как поля, а не простыня текста
structlog.processors.JSONRenderer(), # в проде JSON, локально ConsoleRenderer
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
)
log = structlog.get_logger()
Дальше — обработчик запроса. Здесь видно главное: идентификатор запроса приходит снаружи, если клиент или шлюз его прислал, и генерируется, только если не прислал. Иначе сквозная связь рвётся на первом же сервисе.
# app/api.py — один запрос: корреляция, спан, доменные атрибуты, честная ошибка.
async def handle_checkout(request, order_id: str, user_id: str):
# Свой заголовок для человекочитаемой корреляции; W3C traceparent обрабатывает
# инструментация OpenTelemetry сама и связывает спаны между сервисами.
request_id_var.set(request.headers.get("X-Request-Id") or uuid.uuid4().hex)
with tracer.start_as_current_span("checkout", kind=SpanKind.SERVER) as span:
# order_id уникален, но в спане это допустимо: трейсы не индексируются как метрики.
span.set_attribute("order.id", order_id)
span.set_attribute("user.tier", await tier_of(user_id)) # business/free — 2 значения
log.info("checkout.started", order_id=order_id)
try:
with tracer.start_as_current_span("payment.authorize"):
auth = await payments.authorize(order_id) # дочерний спан: видна его доля времени
with tracer.start_as_current_span("inventory.reserve"):
await inventory.reserve(order_id)
except PaymentDeclined as exc:
# Отказ платежа — НЕ ошибка сервиса. Логируем как бизнес-событие, спан оставляем
# успешным, иначе SLO будет считать чужие отказы своими.
log.info("checkout.declined", order_id=order_id, reason=exc.code)
span.set_attribute("checkout.declined_reason", exc.code)
return response(402, {"reason": exc.code})
except Exception as exc:
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR))
log.error("checkout.failed", order_id=order_id, exc_info=True)
raise
log.info("checkout.completed", order_id=order_id, auth_id=auth.id)
return response(200, {"auth_id": auth.id})
Что даёт эта конструкция на практике. В инциденте вы находите в логе строку с request_id, копируете trace_id и одним переходом открываете весь путь запроса через четыре сервиса — с длительностью каждого шага. Обратно тоже работает: увидели медленный трейс — вытащили по trace_id все логи всех сервисов по этому запросу. Без общего идентификатора эти два действия превращаются в сопоставление таймстемпов вручную, а это и есть та работа, из-за которой инциденты длятся часами. Стандарт на формат идентификатора — W3C Trace Context: заголовок traceparent вида 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01. Его понимают все современные библиотеки, и именно поэтому свой самодельный X-Trace — плохая идея: он не проедет через чужой шлюз, брокер или SDK.
Кардинальность: откуда берётся счёт за мониторинг
Метрика с метками — это не одна метрика: каждая уникальная комбинация значений меток создаёт отдельный временной ряд, а система хранения платит за каждый ряд. Считайте так: http_requests_total{route, method, status, instance} при 20 маршрутах, 4 методах, 6 кодах и 30 инстансах даёт до 14 400 рядов — нормально. Добавьте user_id со ста тысячами значений — и вы получите миллиарды потенциальных рядов и упавший Prometheus. Правило прикладного инженера простое: в метки идёт только то, что имеет ограниченный, заранее известный набор значений. Идентификаторы пользователей, заказов, сессий, полные URL с параметрами, тексты ошибок и IP-адреса в метках не живут — им место в логах и трассировках, которые устроены как поиск, а не как индекс по рядам.
OpenTelemetry сегодня — точка консолидации: единый API и SDK для трёх сигналов, единый протокол OTLP и словарь семантических соглашений (http.route, db.system, service.name). Практическая ценность — не в модности, а в развязке: код инструментируется один раз, а бэкенд хранения меняется правкой конфигурации коллектора. Как это разворачивают и сколько стоит — в главе про наблюдаемость трека devops.
Мониторинг против наблюдаемости
Разница не в инструментах, а в типе вопроса. Мониторинг отвечает на те вопросы, которые вы задали заранее. Вы предположили, что важна доля пятисотых ответов, завели график и порог. Это работает для известных режимов отказа — и работает отлично: дёшево, быстро, надёжно.
Наблюдаемость позволяет задать новый вопрос по уже собранным данным. «Почему медленно только у пользователей с корпоративным тарифом, только на мобильном клиенте, только после вчерашнего выката?» Такой вопрос никто не предусматривал. Если в телеметрии есть нужные измерения и они связаны, ответ получается за пять минут. Если нет — нужно дописать код, выкатить и ждать, пока проблема повторится. Практический критерий зрелости: сколько раз за последний квартал вам пришлось выкатывать релиз, чтобы понять причину уже случившейся проблемы. Если часто — вы мониторите, но не наблюдаете. Разворот этой мысли в тезис «известные неизвестные против неизвестных неизвестных» подробно разобран у Чарити Мейджорс в «Observability Engineering» и в главе Мониторинг.
Алерты, которые работают
Определение, из которого следует всё остальное: алерт — это просьба разбудить человека. Каждый раз, отправляя алерт в пейджер, вы тратите чей-то сон и внимание. Отсюда два жёстких правила.
Первое: алерт на симптом у пользователя, а не на технический показатель. CPU 90 % — это не проблема, это, возможно, эффективная утилизация железа. Проблема — когда пользователь не может оформить заказ. Плохие алерты: загрузка процессора, объём свободной памяти, число потоков, длина очереди сама по себе. Хорошие: доля неуспешных запросов, задержка на 99-м перцентиле, падение числа успешных оплат в минуту, растущее отставание обработки очереди относительно допустимого.
| Плохой алерт | Что с ним не так | Чем заменить |
|---|---|---|
| CPU > 90 % пять минут | не факт, что кому-то плохо | доля ответов 5xx > 1 % за 5 минут |
| Свободно меньше 15 % диска | срабатывает ночью, ничего не ломая | прогноз заполнения: диск кончится за 4 часа |
Сервис не отвечает на /health |
в кластере из 30 подов это норма жизни | доля здоровых реплик ниже кворума |
| Ошибка в логе с уровнем ERROR | одна ошибка — не инцидент | скорость сжигания бюджета ошибок |
Исключение из правила — «предсказуемо превратится в симптом»: сертификат истекает через сутки, диск закончится за четыре часа. Такие алерты допустимы, но должны идти в тикет, а не в пейджер. Второе правило: каждый алерт требует действия. Если на алерт нельзя сделать ничего осмысленного прямо сейчас — это не алерт, а дашборд. Практический тест: у алерта есть ранбук с конкретными шагами. Нет ранбука — алерт недоделан. Нарушение этих двух правил приводит к отказу, который не виден ни в одном мониторинге: усталость от оповещений. Механика арифметическая. Если из десяти ночных срабатываний девять — шум, дежурный за месяц обучается открывать телефон с мыслью «наверное, опять ерунда». В этот момент система перестаёт работать, хотя все её компоненты «зелёные»: сигнал есть, а получателя у него больше нет. Именно поэтому число алертов на дежурство — метрика, которую надо мерить и снижать, а «известная проблема, все привыкли» — самый дорогой из технических долгов.
SLI, SLO и бюджет ошибок — коротко и по делу
Это язык, на котором инженер разговаривает с продуктом о надёжности, не срываясь в «надо бы сделать понадёжнее»:
- SLI — измеримый показатель качества с точки зрения пользователя. Например: доля запросов к
/checkout, завершившихся успешно быстрее 300 мс. - SLO — целевое значение SLI на окне. Например: 99,9 % за 30 дней.
- Бюджет ошибок — то, что остаётся: 0,1 % от месячного трафика можно провалить. Это не «допустимый брак», а валюта. Бюджет цел — команда катит рискованные изменения. Бюджет сожжён — приоритет уходит в надёжность, и это решение принято заранее, а не в разгар спора.
Ценность конструкции в том, что она превращает разговор «нам кажется, что стало хуже» в «мы сожгли 78 % бюджета за девять дней, релиз функции переносится». Подробно — SLI и SLO, Бюджет ошибок и Алерты, где разобраны в том числе оповещения по скорости сжигания бюджета.
Инцидент по шагам
Инцидент — это не «что-то сломалось», а «пользователям плохо, и надо действовать сейчас». Порядок действий фиксированный, и его смысл в том, чтобы не думать под стрессом.
Ключевое правило: сначала восстанови сервис, потом ищи причину. Оно противоречит инстинкту инженера — хочется понять, что происходит. Но пока вы понимаете, счётчик потерь тикает. Смягчение и диагностика — разные задачи, и вторая почти всегда может подождать: причину можно искать на копии данных, на стенде, по сохранённой телеметрии. Отсюда прикладное следствие: откат — первое действие, а не последнее. Если за последний час был выкат, а сейчас плохо — откатывайтесь, не разбираясь, виноват ли он. Вероятность, что виноват именно он, высокая, стоимость отката низкая, а время дорогое. Это работает только при условии, что откат отрепетирован и занимает минуты, — а это уже требование к конвейеру из главы CI/CD и к стратегиям выката из Стратегии релиза. Отдельно проверьте, что откат возможен: миграция базы, удалившая колонку, откату не поддаётся — про это ниже, в разделе про expand/contract.
Ошибка, которую делают чаще всего, — считать инцидент закрытым в состоянии «Смягчён»: сервис работает, все расходятся, а через две недели то же самое повторяется, потому что смягчение — это не устранение.
Роли. Даже в маленькой команде разделение обязательно, иначе один человек одновременно чинит, отвечает в чате и объясняет поддержке — и делает всё три плохо:
- Командующий инцидентом — не чинит руками. Он держит картину, принимает решения, назначает задачи, следит за временем. Это самая недооценённая роль.
- Оператор — тот, кто выполняет действия в системе. Он один, чтобы изменения не накладывались друг на друга.
- Коммуникатор — пишет статусы наружу с фиксированной частотой (раз в 15–30 минут даже если новостей нет). Отсутствие новостей воспринимается хуже, чем плохие новости.
- Ведущий журнал — фиксирует «в 14:32 откатили сервис оплат». Через сутки никто не вспомнит порядок событий, а без него постмортем превращается в художественную литературу.
Роли и процедуры разобраны глубоко в Реакция на инцидент и Дежурства; взгляд со стороны руководителя — в Дежурства и инциденты.
Постмортем без поиска виноватых
Постмортем — документ, который превращает потраченные впустую часы в знание. Его единственная цель — чтобы такое не повторилось, и достигается она только при одном условии: никто не наказан за то, что рассказал, как было на самом деле. Безвинностность здесь — не про доброту, а про качество данных. Человек, который знает, что за ответом последует разбор его компетентности, отвечает осторожно. Он не скажет «я запустил скрипт на проде, потому что был уверен, что это стенд» — он скажет «произошла ошибка конфигурации». В первой формулировке есть системный дефект (стенд и прод неразличимы в приглашении терминала — это чинится). Во второй нет ничего. Обвинение обменивает информацию на иллюзию контроля.
Второй аргумент сильнее. Если инженер смог одной командой уронить прод, дефект не в инженере — дефект в системе, которая это позволила. Уволите его — придёт другой, у которого будет ровно та же возможность. Сидни Деккер называет это переходом от «истории первой» (кто-то ошибся) к «истории второй» (почему это действие в тот момент выглядело разумным). Ричард Кук в «How Complex Systems Fail» формулирует ещё жёстче: в сложных системах катастрофа всегда требует нескольких одновременных отказов, поэтому «единственной коренной причины» не существует — её выбирают, а не находят. Практику ввёл в оборот Джон Оллспоу в «Blameless PostMortems and a Just Culture». Рабочая структура документа:
- Кратко — что видел пользователь, сколько это длилось, какой масштаб. Три предложения, понятные не-инженеру.
- Влияние в числах — сколько запросов, сколько пользователей, сколько денег, сколько процентов бюджета ошибок сожжено. Не «было плохо», а «14 % оплат не проходили 47 минут».
- Хронология — по минутам, с двумя отдельными отметками: когда началось и когда мы узнали. Разрыв между ними — это качество ваших алертов, и он важнее всего остального.
- Что произошло технически — цепочка условий, а не одна причина.
- Что сработало и что помешало — сюда попадает и «дашборд не открывался, потому что он в том же кластере».
- Действия — самое главное.
Про действия отдельно, потому что здесь постмортемы чаще всего умирают. Действие бесполезно, если у него нет владельца-человека (не «команда платформы»), срока и тикета в том же трекере, где обычная работа. «Улучшить мониторинг» — не действие. «Добавить алерт на глубину пула соединений, порог 80 %, ответственный Иванов, до 30 июля, тикет OPS-431» — действие. Хороший показатель здоровья процесса: доля действий из постмортемов, выполненных в срок; если она ниже половины, разборы пишутся ради галочки, и лучше честно перестать их писать, чем поддерживать ритуал. Подробно про механику разбора — Постмортемы; про то, как написать этот документ так, чтобы его прочли, — Постмортем в треке технического письма.
Технический долг: честное определение
Половина споров о техдолге идёт из-за того, что под этим словом каждый понимает своё; уберём два неверных понимания. Техдолг — это не плохой код. Небрежный код — просто небрежный код, и оправдывать его словом «долг» нечестно. Уорд Каннингем, который придумал метафору в отчёте на OOPSLA 1992, потом объяснял прямо: он имел в виду ситуацию, когда вы осознанно выпускаете код, отражающий ваше текущее, ещё неполное понимание задачи, чтобы быстрее получить обратную связь. Долг здесь — разрыв между вашей моделью предметной области и реальностью.
Техдолг — это не «код, который надо переписать красивее». Рабочее определение, которым удобно пользоваться: техдолг — это разрыв между текущей формой системы и той формой, которая нужна ей сейчас. Ключевое слово — «сейчас». Модуль, написанный пять лет назад под другие требования, не был плохим — требования изменились. Долг возникает не только когда система портится, но и когда мир вокруг неё уходит вперёд (второй и седьмой законы Лемана).
Из метафоры долга обычно теряют самое важное — проценты. Основная сумма — это стоимость привести систему в нужную форму. Проценты — то, что вы платите каждый месяц, пока не привели: каждая задача в этом модуле занимает не два дня, а пять; каждый новый человек тратит неделю на понимание; каждый релиз требует ручной проверки. Практическое следствие: приоритет пункта долга определяется не размером основной суммы, а размером процентов. Уродливый модуль, который никто не трогает, процентов не платит — его можно не чинить годами. Средненький модуль, который правят каждую неделю, съедает бюджет команды.
Квадрант Фаулера полезен тем, что развешивает разные ярлыки на разные ситуации. Верхний правый — нормальная инженерная сделка, если вы записали её условия. Нижний правый — неизбежность: вы не могли знать домен заранее, это цена обучения. Верхний левый — управленческая проблема. Нижний левый — проблема квалификации, и лечится она обучением и код-ревью, а не спринтом рефакторинга.
Как сделать долг видимым
Долг, которого нет в трекере, не существует для планирования — а значит, никогда не будет починен. Три работающих приёма. Первый — реестр долга рядом с кодом: не презентация и не вики-страница, которую никто не открывает, а файл в репозитории, который меняется через pull request и обсуждается на ревью.
# docs/debt/registry.yaml — реестр, который читают, потому что он в репозитории
- id: DEBT-014
title: Расчёт скидок продублирован в трёх местах
where: [billing/pricing.py, api/cart.py, jobs/report.py]
# Боль — не «некрасиво», а измеримый факт из жизни команды.
pain: >
За квартал 4 бага из-за расхождения формул; любая правка тарифа требует
синхронной правки трёх мест и ручной регрессии на 2 часа.
interest_per_month: "около 3 дней инженера" # проценты
principal: "5 дней на вынос в общий сервис" # основная сумма
trigger: продуктовая задача в области тарифов # чинить не отдельно, а вместе с ней
owner: ivanov
quadrant: reckless-deliberate
Обратите внимание на поля pain и interest_per_month. Пункт долга без привязки к конкретной боли — это вкусовщина, и продукт справедливо не даст на него времени. Пункт, к которому приложены четыре бага и два часа ручной регрессии на каждую правку тарифа, — это счёт, который уже оплачивается, просто из другого кармана.
Бюджет на каждый спринт вместо «недели рефакторинга». Выделенная неделя раз в квартал не работает по трём причинам: её отменяют первой при любом сдвиге сроков; за неделю нельзя закончить крупное изменение, а незаконченное хуже неначатого; и главное — она создаёт иллюзию, что качество можно делать отдельно от работы. Постоянные 15–20 % ёмкости каждого спринта дают тот же объём за квартал, но непрерывно и без разрешения на каждый раз.
Правило бойскаута с ограничением. Трогаете файл — оставьте его чуть чище. С важной оговоркой: улучшение должно быть в границах вашей задачи, иначе pull request на три строки превращается в неревьюабельные восемьсот. Каталог того, что именно улучшать, — Запахи кода и рефакторинг; как выбить на это время и как разговаривать с бизнесом — Технический долг.
Работа с легаси
Определение Майкла Физерса из «Working Effectively with Legacy Code» (2004) звучит вызывающе: легаси — это код без тестов. Не старый код, не чужой код, не код на устаревшем языке. Логика такая: код без тестов нельзя менять уверенно — каждое изменение это ставка, а не операция. Возраст тут ни при чём: код, написанный вами вчера без тестов, уже легаси. Отсюда следует порядок действий, обратный интуитивному: хочется сначала переписать, потом покрыть, а правильно — наоборот.
Характеризующие тесты. Прежде чем менять непонятный код, зафиксируйте его текущее поведение — включая то, которое кажется неправильным. Вы пишете тест не на то, как должно быть, а на то, как есть: подаёте вход, смотрите фактический выход, записываете его как ожидаемый. Такой тест ничего не утверждает о корректности, он утверждает «после моей правки поведение не изменилось». Странности, которые вы обнаружите (округление в меньшую сторону в третьем знаке, пустая строка вместо null), — не баги по умолчанию: возможно, на них уже кто-то опирается. Это закон Хайрама: при достаточном числе пользователей любое наблюдаемое поведение системы становится чьим-то контрактом.
Шов (seam) — место, где можно изменить поведение программы, не редактируя код в этом месте. Параметр конструктора вместо new внутри метода, интерфейс вместо конкретного класса, переопределяемый метод-фабрика, точка внедрения через конфигурацию. Швы — это то, что превращает нетестируемый код в тестируемый минимальными правками. Каталог техник разрыва зависимостей — в Легаси-код; как выделять границы в старой системе по-доменному — DDD в легаси.
Удушающая смоковница
Как заменить систему целиком, если останавливать её нельзя. Strangler Fig — метафора Фаулера: фикус-душитель прорастает вокруг дерева-хозяина, постепенно замещает его, и когда старое дерево отмирает, новое уже стоит само. Механика — пять шагов, каждый из которых обратим.
- Поставить фасад перед старой системой так, чтобы клиенты ходили через него, а не напрямую. Пока за фасадом всё то же самое — это изменение с нулевым риском, но именно оно даёт точку управления.
- Выбрать один кусок функциональности — по возможности такой, у которого понятная граница и мало общих данных. Реализовать его заново.
- Пустить долю трафика на новую реализацию: сначала теневую копию (запрос уходит в обе системы, отвечает старая, ответы сравниваются), затем 1 %, 5 %, 50 %.
- Перенести владение данными. Самый трудный шаг: пока обе системы пишут в одну таблицу, вы не разделили ничего. Стратегии — в Выбор и миграции баз и Слой данных.
- Удалить старый маршрут — и это отдельный шаг, который надо запланировать, иначе вы получите две системы вместо одной навсегда.
Почему не «перепишем с нуля». Джоэл Спольски в «Things You Should Never Do, Part I» назвал это «худшей стратегической ошибкой» на примере переписывания Netscape, стоившего компании три года. Аргумент не про эстетику, а про информацию: уродливый код часто уродлив потому, что содержит знание. Каждая странная ветка — это чей-то отчёт о баге, обходной путь для конкретного браузера, требование регулятора. Переписывая с нуля, вы выбрасываете вместе с кодом весь этот накопленный опыт и обязуетесь пройти те же грабли заново — только теперь под давлением, потому что старая система всё это время требует поддержки и вы платите за две. Добавьте сюда то, что переписывание не даёт ценности до самого конца, и вы поймёте, почему такие проекты закрывают на 70 % готовности.
Переписывание оправдано в узких случаях: система маленькая, требования известны точно, платформа под ней мертва (рантайм не получает обновлений безопасности) или стоимость сопровождения превысила стоимость замены с большим запасом. Во всех остальных случаях — смоковница.
Миграции и удаление: как выключить старое
Удаление кода — самая недооценённая инженерная работа. Оно не даёт демо, его нельзя показать на ревью продукта, но каждая удалённая тысяча строк — это навсегда снятые расходы: на сборку, на тесты, на обновление зависимостей, на объяснение новичкам, на аудит безопасности. Проблема в том, что удалять страшно: неизвестно, кто ещё пользуется. Это лечится измерением, а не смелостью.
# api/deprecation.py — прежде чем удалять, измерь. Счётчик использования,
# предупреждение потребителю и дата отключения в одном месте.
from datetime import datetime, timezone
from prometheus_client import Counter
# Метка client_id — низкой кардинальности: это ваши известные потребители,
# а не произвольные пользователи. Именно она отвечает на вопрос «кому звонить».
DEPRECATED_HITS = Counter("api_deprecated_endpoint_hits_total",
"Обращения к устаревшим эндпоинтам", ["endpoint", "client_id"])
SUNSET = datetime(2026, 10, 1, tzinfo=timezone.utc)
async def deprecated_v1_orders(request):
client = request.headers.get("X-Client-Id", "unknown")
DEPRECATED_HITS.labels(endpoint="/v1/orders", client_id=client).inc()
resp = await handle_orders_v1(request)
resp.headers["Deprecation"] = "true" # RFC 9745: ресурс устарел
# RFC 8594: дата, после которой ресурса не станет. Формат — HTTP-date.
resp.headers["Sunset"] = SUNSET.strftime("%a, %d %b %Y %H:%M:%S GMT")
resp.headers["Link"] = '</v2/orders>; rel="successor-version"'
return resp
Порядок вывода из эксплуатации, который работает:
- Объявить устаревшим и дать замену. Без готовой замены объявление — просто раздражение.
- Измерять использование по потребителям. Метрика с меткой клиента отвечает на вопрос «кому писать», а не «сколько ещё вызовов».
- Написать потребителям адресно — не рассылкой в общий канал, а конкретным командам с цифрами их трафика и датой отключения.
- Провести учения на отказ: выключить эндпоинт на 15 минут в согласованное окно. Это находит потребителей, которых нет ни в одном списке, — гораздо дешевле, чем найти их в момент финального удаления. Механика таких учений — Хаос и учения.
- Отключить, но не удалять — оставить возможность вернуть за минуту (флаг, а не откат релиза).
- Удалить код и данные через оговорённый срок. Именно код: закомментированный блок или ветка
if False— не удаление, а ещё один долг.
Эволюция без остановки
Всё вышеперечисленное упирается в одно требование: менять систему, не выключая её. Три техники, которые прикладной инженер применяет постоянно.
Обратная совместимость как дисциплина. В распределённой системе старая и новая версии кода живут одновременно — во время выката, а иногда неделями, если клиент мобильный. Значит, изменение контракта должно быть таким, чтобы старый код работал с новыми данными, а новый — со старыми. Правила совместимости разобраны в Совместимость и эволюция контрактов, а формы контрактов между сервисами — в Интеграции.
Expand/contract (параллельное изменение) — способ выполнить несовместимое изменение серией совместимых. Три фазы: расширяем (добавляем новое, не трогая старое), мигрируем (переводим читателей и писателей), сжимаем (удаляем старое). Классический пример — переименование колонки, которое «в лоб» ломает прод в момент выката:
-- ФАЗА 1: EXPAND. Только добавление. Откат безопасен, старый код не замечает изменений.
ALTER TABLE orders ADD COLUMN total_amount_cents BIGINT;
-- Заполняем существующие строки пачками, чтобы не держать долгую блокировку.
UPDATE orders SET total_amount_cents = ROUND(total * 100)
WHERE total_amount_cents IS NULL AND id BETWEEN :lo AND :hi;
-- ФАЗА 2: MIGRATE. Код версии N пишет в ОБА поля и читает старое.
-- Код версии N+1 пишет в оба и читает новое. Обе версии живут одновременно.
-- Здесь же — сверка: расхождение полей должно быть нулевым.
SELECT count(*) FROM orders WHERE total_amount_cents <> ROUND(total * 100);
-- ФАЗА 3: CONTRACT. Только после того, как ни один инстанс не читает старое поле.
-- Между фазой 2 и 3 должно пройти БОЛЬШЕ, чем время жизни самого старого клиента.
ALTER TABLE orders DROP COLUMN total;
Ценность в том, что после каждой фазы система работоспособна и откатываема. Цена — временное дублирование и дисциплина довести до третьей фазы: незавершённый expand/contract и есть тот самый долг, который потом никто не может объяснить. Приём описан у Данило Сато как Parallel Change.
Теневой трафик и сравнение ответов. Когда вы заменяете компонент, тесты доказывают, что новый код проходит придуманные вами сценарии. Прод проверяет его настоящими. Схема: запрос обрабатывает старая реализация и отдаёт ответ пользователю, параллельно тот же вход уходит в новую, ответы сравниваются, расхождения пишутся в отчёт. Пользователь ничего не замечает — новый путь не влияет на ответ. За неделю такого прогона вы находите то, что не нашли бы за месяц придумывания тест-кейсов. Инструментальный образец — GitHub Scientist. Две оговорки: теневой путь не должен иметь побочных эффектов (никаких повторных списаний — см. идемпотентность), и он удваивает нагрузку на зависимости, что надо учесть по ёмкости (Масштабирование). Все три техники опираются на управление поведением без выката — флаги, конфигурацию и разделение окружений из главы Окружения, конфигурация и секреты.
Типичные ошибки
Алерт на технический показатель без симптома. Пейджер звонит на CPU, диск и рестарты подов, а падение конверсии оплат замечает продукт через два часа. Лечится переписыванием алертов от пользователя внутрь, а не от инфраструктуры наружу.
«Известная проблема, все привыкли». Алерт, который срабатывает каждую ночь и который все игнорируют, — это не проблема мониторинга, это сломанный процесс. Либо чините причину, либо удаляйте алерт. Третьего состояния быть не должно: молчаливое согласие с шумом обучает команду игнорировать и настоящие сигналы.
Постмортем ради галочки. Документ написан, обсуждён, действия записаны — и никто не проверил их через месяц. Проверяемый признак: посмотрите тикеты из трёх последних постмортемов; если больше половины открыты и просрочены, вы тратите на разборы часы впустую. Сюда же — смягчение, принятое за устранение: перезапустили сервис, полегчало, разошлись, через неделю то же самое ночью и в худший момент.
Рефакторинг без тестов. «Я просто аккуратно перепишу» — самая дорогая фраза в этой главе. Без характеризующих тестов вы не рефакторите, а переписываете вслепую, и обнаружите это в проде.
Долг, который никто не считает. Пока долг живёт в головах, он всегда проигрывает продуктовым задачам, у которых есть оценка и срок. Реестр с болью и процентами превращает «нам бы отрефакторить» в аргумент, который можно положить рядом с продуктовой задачей и сравнить.
Система, которую никто не удаляет. Сервис, у которого два запроса в сутки и оба от мониторинга. Эндпоинт v1, который «вроде кто-то использует». Флаг, включённый на 100 % два года назад. Каждый из них требует обновлений безопасности, попадает в каждый аудит и участвует в каждой миграции. Заведите правило: у каждой временной конструкции с рождения есть дата смерти.
Мини-итог главы
- Большая часть жизни системы и её стоимости — после первого релиза, и основная часть этой работы — развитие функциональности, а не починка багов. Законы Лемана дают инженерное обоснование бюджета на качество: сложность растёт сама, снижается только осознанным усилием.
- Три сигнала телеметрии не заменяют друг друга: метрики говорят, что плохо, трассировки — где, логи — что именно. Их связывает общий идентификатор запроса, и без него разбор инцидента превращается в сопоставление таймстемпов.
- Мониторинг отвечает на заранее заданные вопросы, наблюдаемость позволяет задать новый без релиза. Проверьте себя по числу релизов «чтобы понять причину».
- Алерт — просьба разбудить человека: только на симптом у пользователя, только с ранбуком, только если есть что сделать. Шум в пейджере — отказ системы, невидимый в мониторинге.
- Порядок инцидента: обнаружение → смягчение → диагностика → устранение → разбор. Сначала верните сервис; откат — первое действие, если он отрепетирован. Постмортем без поиска виноватых даёт больше информации, потому что люди говорят правду; действие без владельца, срока и тикета — не действие.
- Техдолг — разрыв между текущей и нужной формой системы. Приоритет задают проценты, а не основная сумма. Делайте долг видимым и платите по нему каждый спринт, а не раз в квартал.
- Легаси — код без тестов. Сначала характеризующие тесты и швы, потом изменения. Замена по частям через удушающую смоковницу почти всегда бьёт переписывание с нуля.
- Удаление — полноценная инженерная работа: объявить, измерить использование, предупредить адресно, провести учения на отказ, отключить, удалить. Менять систему на ходу позволяют совместимость, expand/contract и теневой трафик со сравнением ответов.
Мини-итог всего трека
Тринадцать глав складываются в одну картину — путь от запроса пользователя до системы, которая живёт годами. Всё начинается на клиенте (глава 01) — в недоверенной среде на чужом железе, где вы отвечаете за впечатление, но не за истину. Запрос уходит на сервер (02), где живёт бизнес-логика и принимаются решения, и упирается в слой данных (03) — самую консервативную часть системы, которая переживёт три поколения кода вокруг себя. Части системы разговаривают через интеграцию (04): контракты, синхронные и асинхронные вызовы, границы, за которыми начинается чужая ответственность. Как эти части расставлены в пространстве, определяет форма системы (05) — от модульного монолита до сервисов, и выбор здесь диктуется устройством команды не меньше, чем техникой.
Дальше — то, как это делается руками. Контроль версий (06) хранит не файлы, а историю решений. Рабочее окружение (07) и сборка с зависимостями (08) отвечают на вопрос воспроизводимости: та же команда даёт тот же результат у вас, у коллеги и на сервере сборки. Ворота качества (09) переносят обнаружение проблем влево, к моменту, когда правка стоит минуты, а не сутки. Конвейер (10) превращает изменение в выкатанную версию без ручных шагов и делает откат дешёвым. Конфигурация и окружения (11) отделяют то, что меняется между стендами, от того, что меняется вместе с кодом, — и дают возможность управлять поведением без релиза. Масштабирование (12) отвечает на рост нагрузки — но только после того, как измерено, что именно упирается.
И, наконец, эта глава: эксплуатация и эволюция — фаза, в которой система проводит почти всю жизнь. Сквозная мысль трека простая: работающая система — не набор технологий, а набор согласованных решений о границах и ответственности, каждое из которых имеет цену и срок годности. Инженер отличается от исполнителя тем, что видит эти границы целиком, умеет объяснить, почему выбрал именно так, и знает, где искать глубину, когда её не хватает. Начало трека и карта его глав — в обзоре.
Источники
- Barry Boehm. Software Engineering Economics (1981) — первая систематическая оценка доли сопровождения; Robert Glass. Facts and Fallacies of Software Engineering (2002), факты 41–45 — сводка оценок и структура этих расходов.
- Lehman’s laws of software evolution — почему система обязана меняться и почему сложность растёт сама. ISO/IEC/IEEE 14764 — стандарт сопровождения ПО и четыре вида работ.
- OpenTelemetry и W3C Trace Context — нынешний стандарт телеметрии и формат сквозного идентификатора.
- Charity Majors, Liz Fong-Jones, George Miranda. Observability Engineering (O’Reilly, 2022).
- Google SRE Book — главы про алертинг, дежурства и постмортемы, откуда пошла современная практика.
- John Allspaw. Blameless PostMortems and a Just Culture (2012).
- Richard Cook. How Complex Systems Fail — восемнадцать тезисов, которые стоит перечитывать перед каждым разбором.
- Martin Fowler. Technical Debt Quadrant, Strangler Fig Application, Parallel Change.
- Michael Feathers. Working Effectively with Legacy Code (2004) — швы, характеризующие тесты, техники разрыва зависимостей. Рядом — Hyrum’s Law: почему нельзя безнаказанно менять «недокументированное» поведение.
- Joel Spolsky. Things You Should Never Do, Part I (2000).
- RFC 8594 (Sunset) и RFC 9745 (Deprecation) — как машиночитаемо объявить об отключении ресурса.
- GitHub Scientist — эталонная реализация сравнения старой и новой реализации на живом трафике.
Что дальше
Это последняя глава трека — дальше дороги расходятся; выберите ту, что отвечает на ваш нынешний вопрос.
Глубже в надёжность и эксплуатацию — трек sre. Здесь мы прошли эксплуатацию на уровне «что делает прикладной инженер»; шестнадцать глав трека разбирают то же самое как отдельную дисциплину: SLI и SLO с арифметикой, бюджеты ошибок, режимы отказа, ёмкость, деградация, учения. Идти туда стоит, если вы отвечаете за работающую систему и хотите перестать спорить о надёжности словами.
Глубже в инфраструктуру и поставку — трек devops. Контейнеры, Kubernetes, IaC, GitOps, облака и то, сколько всё это стоит. Отдельно — Наблюдаемость и дежурства, где всё из первой половины этой главы доведено до рабочих конфигураций Prometheus, Alertmanager и коллектора OpenTelemetry.
Глубже в форму системы — трек architecture-patterns. Если вы поняли, что боль вашей системы не в коде и не в эксплуатации, а в том, как она разрезана на части, — вам сюда: монолит и микросервисы, событийные архитектуры, паттерны устойчивости, архитектурные решения и ADR.
Качество кода, принципы и работа с легаси — трек principles: SOLID, связность и зацепление, принципы тестирования, эволюция контрактов — фундамент, который определяет, насколько дорого обходится каждое изменение через два года. Если завтра вам предстоит менять код, которого вы боитесь, начните прицельно с Легаси-кода и Запахов кода — там каталог техник, а не рассуждения.
Платформа и опыт разработчика — трек platform-engineering. Когда команд становится много и каждая решает описанные здесь задачи заново, появляется отдельная работа: сделать «золотую тропу», по которой все эти вещи получаются по умолчанию. Отдельно полезна глава Миграции — о том, как переводить на новое десятки команд, а не один сервис.
Карьера и роли — трек sdlc-and-career. Куда всё это встраивается организационно: этапы жизненного цикла, роли в команде, грейды, рост, собеседования и первые 90 дней на новом месте.
Общая карта портала со всеми треками и рекомендуемыми маршрутами — Роадмап.