Вывод из эксплуатации и миграция
Обзор
Код — это обуза, а не актив. У каждой строки кода есть постоянная стоимость сопровождения: баги, которые надо чинить, зависимости, которые надо обновлять, патчи безопасности, которые надо накатывать, и новые инженеры, которых надо вводить в курс дела. Вывод из эксплуатации — это дисциплина удаления кода, который больше не оправдывает своего содержания, а миграция — процесс безопасного перевода пользователей со старого на новое.
Большинство инженерных организаций хорошо умеют строить. Мало кто хорошо умеет удалять. Этот скилл закрывает этот пробел.
Когда применять
- Заменяешь старую систему, API или библиотеку новой
- Выводишь из эксплуатации функциональность, которая больше не нужна
- Объединяешь дублирующиеся реализации
- Удаляешь мёртвый код, которым никто не владеет, но от которого все зависят
- Планируешь жизненный цикл новой системы (планирование вывода из эксплуатации начинается на этапе проектирования)
- Решаешь, поддерживать ли легаси-систему или вложиться в миграцию
Базовые принципы
Код — это обуза
У каждой строки кода есть постоянная стоимость: ей нужны тесты, документация, патчи безопасности, обновления зависимостей и умственные затраты каждого, кто работает рядом. Ценность кода — это функциональность, которую он даёт, а не сам код. Когда ту же функциональность можно обеспечить меньшим количеством кода, меньшей сложностью или лучшими абстракциями, старый код должен уйти.
Закон Хайрама делает удаление трудным
При достаточном числе пользователей от любого наблюдаемого поведения кто-то начинает зависеть — включая баги, причуды таймингов и незадокументированные побочные эффекты. Именно поэтому вывод из эксплуатации требует активной миграции, а не только объявления. Пользователи не могут «просто переключиться», когда они зависят от поведения, которое замена не воспроизводит.
Планирование вывода из эксплуатации начинается при проектировании
Строя что-то новое, спроси: «Как мы будем это удалять через 3 года?» Системы, спроектированные с чистыми интерфейсами, фича-флагами и минимальной внешней поверхностью, выводить из эксплуатации проще, чем системы, у которых детали реализации протекают повсюду.
Решение о выводе из эксплуатации
Прежде чем что-либо выводить, ответь на эти вопросы:
1. Даёт ли эта система до сих пор уникальную ценность?
→ Если да, поддерживай. Если нет, продолжай.
2. Сколько пользователей/потребителей от неё зависят?
→ Оцени объём миграции в числах.
3. Существует ли замена?
→ Если нет, сначала построй замену. Не выводи из эксплуатации без альтернативы.
4. Какова стоимость миграции для каждого потребителя?
→ Если тривиально автоматизируется — делай. Если вручную и трудоёмко — взвесь против стоимости сопровождения.
5. Какова постоянная стоимость сопровождения, если НЕ выводить?
→ Риск безопасности, время инженеров, упущенные возможности из-за сложности.
Рекомендательный и принудительный вывод из эксплуатации
| Тип | Когда применять | Механизм |
|---|---|---|
| Рекомендательный | Миграция необязательна, старая система стабильна | Предупреждения, документация, подталкивания. Пользователи мигрируют в своём темпе. |
| Принудительный | У старой системы есть проблемы с безопасностью, она блокирует развитие или стоимость её сопровождения непосильна | Жёсткий срок. Старая система будет удалена к дате X. Предоставь инструменты миграции. |
По умолчанию — рекомендательный. Принудительный используй, только когда стоимость сопровождения или риск оправдывают принуждение к миграции. Принудительный вывод требует предоставить инструменты миграции, документацию и поддержку — нельзя просто объявить срок.
Процесс миграции
Шаг 1: построй замену
Не выводи из эксплуатации без работающей альтернативы. Замена должна:
- Покрывать все критичные сценарии старой системы
- Иметь документацию и руководства по миграции
- Быть проверенной в продакшне (а не «теоретически лучшей»)
Шаг 2: объяви и задокументируй
## Уведомление о выводе из эксплуатации: OldService
**Статус:** выведен из эксплуатации с 2025-03-01
**Замена:** NewService (см. руководство по миграции ниже)
**Дата удаления:** рекомендательно — жёсткого срока пока нет
**Причина:** OldService требует ручного масштабирования и лишён наблюдаемости.
NewService решает и то, и другое автоматически.
### Руководство по миграции
1. Замените `import { client } from 'old-service'` на `import { client } from 'new-service'`
2. Обновите конфигурацию (примеры ниже)
3. Запустите скрипт проверки миграции: `npx migrate-check`
Шаг 3: мигрируй инкрементально
Переводи потребителей по одному, а не всех сразу. Для каждого потребителя:
1. Найди все точки соприкосновения с выводимой системой
2. Переведи на замену
3. Убедись, что поведение совпадает (тесты, интеграционные проверки)
4. Убери ссылки на старую систему
5. Подтверди отсутствие регрессий
Правило оттока: если ты владеешь выводимой из эксплуатации инфраструктурой, ты отвечаешь за миграцию своих пользователей — либо за обратно совместимые обновления, которые вообще не требуют миграции. Не объявляй вывод из эксплуатации, бросив пользователей разбираться самим.
Шаг 4: удали старую систему
Только после того, как мигрировали все потребители:
1. Убедись, что использования нет (метрики, логи, анализ зависимостей)
2. Удали код
3. Удали связанные тесты, документацию и конфигурацию
4. Удали уведомления о выводе из эксплуатации
5. Порадуйся — удаление кода это достижение
Паттерны миграции
Паттерн «душитель»
Держи старую и новую системы параллельно. Постепенно переводи трафик со старой на новую. Когда старая система обслуживает 0% трафика, удали её.
Фаза 1: новая система обслуживает 0%, старая — 100%
Фаза 2: новая система обслуживает 10% (канареечный запуск)
Фаза 3: новая система обслуживает 50%
Фаза 4: новая система обслуживает 100%, старая простаивает
Фаза 5: удалить старую систему
Паттерн «адаптер»
Создай адаптер, который переводит вызовы со старого интерфейса на новую реализацию. Потребители продолжают использовать старый интерфейс, пока ты мигрируешь бэкенд.
// Адаптер: старый интерфейс, новая реализация
class LegacyTaskService implements OldTaskAPI {
constructor(private newService: NewTaskService) {}
// Старая сигнатура метода, делегирует новой реализации
getTask(id: number): OldTask {
const task = this.newService.findById(String(id));
return this.toOldFormat(task);
}
}
Миграция через фича-флаг
Используй фича-флаги, чтобы переводить потребителей со старой системы на новую по одному:
function getTaskService(userId: string): TaskService {
if (featureFlags.isEnabled('new-task-service', { userId })) {
return new NewTaskService();
}
return new LegacyTaskService();
}
Миграции схемы БД (расширение/сжатие)
Изменение схемы — самая рискованная миграция, потому что данные это единственное, что нельзя откатить откатом деплоя. Ломается всё так: изменение схемы связывают с изменением кода — переименовывают колонку в том же релизе, где начинают использовать новое имя, и в окне выкатки, когда старый и новый код работают одновременно, один из них обращается к несуществующей колонке. Лекарство — никогда не менять колонку на месте. Мигрируй аддитивными фазами, чтобы и старый, и новый код были корректны на каждом шаге.
РАСШИРЕНИЕ ──────────→ МИГРАЦИЯ ──────────────→ СЖАТИЕ
добавить новую колонку, заполнить существующие когда старую колонку
nullable, рядом со строки, писать в обе никто не читает, удалить
старой из приложения её отдельным деплоем позже
Разобранный пример — переименование name в full_name:
- Расширение. Добавь
full_nameкак nullable. Выкати. (Старый код её игнорирует, ничего не ломается.) - Двойная запись. Приложение пишет и
name, иfull_nameпри каждой вставке/обновлении. Выкати. - Заполнение. Скопируй
name → full_nameдля существующих строк порциями, чтобы не блокировать таблицу. - Переключение чтения. Направь приложение на
full_name, продолжая писать в обе. Выкати и дай отстояться. - Сжатие. Прекрати писать в
name, а затем — отдельным, более поздним деплоем — удали колонку.
Каждый шаг выкатывается и откатывается независимо: если шаг 4 повёл себя плохо, откати код, и full_name продолжит заполняться. Относись к каждой фазе как к тонкому вертикальному срезу — см. скилл incremental-implementation.
Правила:
- Сначала аддитивное, разрушительное — последним и в одиночку. Добавления (новая nullable-колонка, новая таблица, новый индекс) безопасны в любом деплое; удаления и переименования получают свой деплой после того, как код перестал ссылаться на старую форму.
- У каждой миграции есть проверенный путь отката. Миграция, которую нельзя обратить, — это деплой, который нельзя откатить. Напиши и прогони
downдо вливания. - Заполняй порциями, вне горячего пути. Один
UPDATEпо миллионам строк блокирует таблицу; дроби и притормаживай. - Строй большие индексы без блокировки записи (например,
CREATE INDEX CONCURRENTLYв Postgres). - Разделяй с кодом через фича-флаг, когда переключение рискованно, ровно как в паттерне миграции через фича-флаг выше.
Зомби-код
Зомби-код — это код, которым никто не владеет, но от которого все зависят. Он не сопровождается активно, у него нет ясного владельца, и он накапливает уязвимости и проблемы совместимости. Признаки:
- Нет коммитов 6+ месяцев, но есть активные потребители
- Нет назначенного сопровождающего или команды
- Падающие тесты, которые никто не чинит
- Зависимости с известными уязвимостями, которые никто не обновляет
- Документация, ссылающаяся на системы, которых больше нет
Реакция: либо назначить владельца и нормально сопровождать, либо вывести из эксплуатации с конкретным планом миграции. Зомби-код не может оставаться в подвешенном состоянии: он получает либо вложения, либо удаление.
Типовые самооправдания
| Самооправдание | Как на самом деле |
|---|---|
| «Оно ещё работает, зачем удалять?» | Работающий код, который никто не сопровождает, накапливает долг по безопасности и сложность. Стоимость сопровождения растёт незаметно. |
| «Может, кому-то ещё понадобится» | Если понадобится позже — можно построить заново. Держать неиспользуемый код «на всякий случай» дороже, чем построить его снова. |
| «Миграция слишком дорогая» | Сравни стоимость миграции со стоимостью сопровождения за 2–3 года. В долгую миграция обычно дешевле. |
| «Выведем из эксплуатации, когда доделаем новую систему» | Планирование вывода начинается при проектировании. К моменту готовности новой системы у вас будут новые приоритеты. Планируй сейчас. |
| «Пользователи мигрируют сами» | Не мигрируют. Дай инструменты, документацию и стимулы — или сделай миграцию сам (правило оттока). |
| «Мы можем поддерживать обе системы сколько угодно» | Две системы, делающие одно и то же, — это двойные затраты на сопровождение, тестирование, документацию и ввод новых людей. |
| «Просто переименуй колонку, это одна строка» | Во время выкатки старый и новый код работают вместе — один из них обратится к несуществующей колонке. Расширение/сжатие, никогда не переименование на месте. |
| «Добавлю колонку и удалю старую в той же миграции» | Это связывает безопасное добавление с разрушительным удалением. Удаления получают свой деплой, после того как код перестал ссылаться на старую форму. |
| «Напишем откат, если понадобится» | Миграция без пути отката — это деплой, который нельзя обратить. Напиши и прогони down до вливания. |
Тревожные признаки
- Выводимые из эксплуатации системы, для которых нет замены
- Объявления о выводе из эксплуатации без инструментов миграции и документации
- «Мягкий» вывод, который годами остаётся рекомендательным без всякого прогресса
- Зомби-код без владельца, но с активными потребителями
- Новая функциональность, добавляемая в выводимую систему (вкладывайся в замену)
- Вывод из эксплуатации без измерения текущего использования
- Удаление кода без проверки, что активных потребителей нет
- Изменение схемы и зависящий от него код, выкаченные одним деплоем
- Колонка, переименованная или удалённая на месте, а не через расширение/сжатие
- Миграция, влитая без проверенного пути отката, или заполнение, блокирующее таблицу
Проверка
После завершения вывода из эксплуатации:
- Замена проверена в продакшне и покрывает все критичные сценарии
- Есть руководство по миграции с конкретными шагами и примерами
- Все активные потребители мигрированы (подтверждено метриками/логами)
- Старый код, тесты, документация и конфигурация полностью удалены
- В кодовой базе не осталось ссылок на выведенную систему
- Уведомления о выводе из эксплуатации удалены (они выполнили своё назначение)
После миграции схемы БД:
- Изменение выкатывается аддитивными фазами (расширение → заполнение → сжатие), а не одной правкой на месте
- И старый, и новый код корректны относительно схемы на каждом шаге выкатки
- У каждой миграции есть проверенный путь отката; заполнение идёт притормаживаемыми порциями
- Разрушительные шаги (удаление/переименование) выкатываются отдельным деплоем после того, как код перестал ссылаться на старую форму