
Если проблему на проде видят пользователи — значит, вы уже теряете деньги или репутацию. Поэтому откатываться нужно быстро, а на наших масштабах — очень быстро: в RuntimeCloud, внутреннем облаке Яндекса, работает миллион контейнеров на ста с лишним тысячах серверов. Во времена tar-архивов откатывались со скоростью переключения symlink. В современном мире деплой умеет то, что раньше и не снилось, — он научился изоляции, декларативности и воспроизводимости. Только про быстрый откат многие забыли.
Привет, Хабр! Меня зовут Андрей Мичурин, я руковожу разработкой систем деплоя Yandex Infrastructure. RuntimeCloud (RTC) отвечает за выкладки, управление трафиком, изоляцию, платформизацию. Под нашим управлением находится 1 млн контейнеров, более 100 тысяч серверов и 8 млн ядер. В этой статье расскажу, почему для современного деплоя очень важно уметь быстро делать откат, какие решения мы рассматривали и как сделали своё.
Эта статья написана по мотивам моего доклада на infra.conf'26.
Если вам больше нравится слушать, а не читать — вот видео доклада.
Эволюция деплоя
Итак, откатываться нужно очень быстро. Чтобы объяснить, почему скорость отката стала критичной, посмотрим на историю развития деплоя.

На заре деплоя не было никаких чётких правил, автоматизации и контроля: каждый делал, как хотел и когда хотел. Программа жила на физическом носителе. Собрал бинарь на одной машине — запустил на другой, в соседний город носитель отправил обычной почтой. Единого репозитория и стандартной сборки не существовало: что инженер принёс, то и попало на сервер.
Если инженер был хорошим, он запускал файл каким-то watchdog — программой, которая перезапускает процесс, если он упал. Если не очень хорошим, он его запускал в домашке (home directory) — личной папке пользователя на сервере. Там процесс мог случайно упасть, после чего его никто не найдёт, и он не будет автоматически перезапущен.
Позднее стали выкатывать бинари по сети. Например, в случаях, когда бинарь уже не запускается на том компьютере, на котором он собирался. Либо, например, этот компьютер не справляется с нагрузкой и нужен кластер. Если очень сильно повезло — компьютер большой, памяти много и предыдущий бинарь сохранился, — то на него можно откатиться. А если не повезло, нужно заново собирать бинарь, приносить файл на носителе или отправлять по сети.
Дальше появились бандлы: бинарь вместе с его конфигом и другими данными, нужными для запуска. Всё это было удобно упаковать в архив, например, в tar.gz. Такой архив приносили на сервер, распаковывали в какую-то директорию, для которой делался symlink. А из неё уже запускался бинарь.
Так возникла первая атомарная система деплоя. Откат выглядел так: бинарь останавливался, symlink переключался на другую директорию, бинарь снова запускался.
Позже начали управлять зависимостями. Статически собранный бинарь просто работает, а вот разделяемыми библиотеками нужно как-то управлять. Эту проблему пытались решить при помощи пакетов. Пакет — это тоже архив, бандл, но в нём описано, какие библиотеки нужны для запуска, как запустить бинарь и что произойдёт после установки.
Однако здесь есть нюанс. Если вы хотите откатить пакет, вы его откатите. Если же он содержит ещё 100 пакетов зависимостей, вам придётся откатить ещё и их (dependency hell). Если вам повезло, то у вас всё это закешировано на хосте. А если нет — нужно будет все 100 пакетов перекачать снова, и время отката будет чуть-чуть больше.
Когда появился зоопарк серверов и операционных систем, этим нужно было как-то управлять. Тогда возникли системы управления конфигурацией.
Автоматизация управления

Автоматизация породила подход, который мы используем по сей день, — инфраструктуру как код. Позже он эволюционировал в GitOps: выкатка и откат стали коммитом в репозиторий.
Рецепты и манифесты в каждой системе устроены по-своему, но обычно это файл со списком серверов: какие компоненты на них поставить и в каком состоянии они должны оказаться.
Например, чтобы запустить прокси-сервер Nginx, его нужно настроить. Допустим, в группе 4 сервера, и вы хотите связать их с бэкендом. Для этого вы создаёте новый файл конфигурации, описываете в нём все серверы бэкенда и то, что нужно подключить. Разумеется, вам может потребоваться и база данных, которую вы также можете настроить и интегрировать. После этого всё будет работать.
Ключевые преимущества этого подхода — декларативность и повторяемость. Дальше можно тестировать. Например, взять файл, запустить в виртуалке и проверить, что все пакеты и файлы приедут, а архивы распакуются. Есть и ещё одно преимущество — быстрое масштабирование: достаточно расширить группу серверов.
Тем временем серверы стали мощнее, и понадобилось запускать на одном физическом сервере, например, десять Nginx. Для этого инженеры стали использовать виртуализацию.
Linux Containers
LXC-контейнеры — это первый успешный встроенный в mainstream ядро Linux легковесный подход. Это ОС-контейнеры: внутри есть свой супервизор, например systemd. Администрировать его нужно точно так же, как физический хост: приносить пакеты, конфиги, секреты. Для этого подойдёт любая система управления конфигурациями, например CFEngine.
Любой контейнер, в том числе LXC, — это не что иное, как cgroups и namespaces.

Это два механизма ядра Linux, которые позволяют делать контейнеры. Вместе они задают видимость и геометрию контейнера. Через namespaces мы говорим процессу, что его PID — 1 и других процессов в системе нет. Можем сделать изоляцию по диску (mnt), тогда процесс видит только свою файловую систему. По сути мы говорим процессу: «Ты совсем один в системе».
Cgroups позволяет задавать геометрию контейнера, например, ограничения. У контейнера может быть 2 ядра и 2 ГБ памяти. Таким образом, контейнер — это набор ограничений, которые работают за счёт механизмов ядра Linux. При помощи cgroups и namespaces мы можем делать любые абстракции, вложенность и дальше с этим работать.
ОС-контейнеры со своим супервизором нужно настраивать и наполнять пакетами, а это сложно, поэтому инженеры начали упрощать. В итоге стали использовать по дефолту application-контейнеры. Самый известный пример — знакомый всем Docker: достаточно указать базовый образ и команду, которую надо запустить. В отличие от ОС-контейнеров, в Docker-контейнере по дефолту запускается один процесс, что упрощает управление и приближает нас к модели «приложение как контейнер».
Однако всё это по-прежнему нужно оркестрировать. Тут возникает Kubernetes — система оркестрации контейнеров, которая позволяет их управляемо выкладывать на кластеры. Там можно делать собственные контроллеры и писать любую логику.
В Kubernetes есть киллер-фича, которая мне очень нравится, — readiness probe. Это механизм, с помощью которого Kubernetes проверяет, готов ли под принимать трафик. Если проба не проходит, Kubernetes убирает под из списка эндпоинтов сервиса: балансировщик нагрузки перестаёт слать туда запросы. Если проходит — под снова начинает получать трафик. Правда, readiness probes добавляет задержку в выкладку и съедает часть скорости.
В итоге системы деплоя усложнились, а мы разучились откатываться со скоростью symlink: изоляция, воспроизводимость, декларативность есть, а вот скорости отката нет. И мы поставили себе цель вернуть скорость symlink в мир контейнеров.
Для ясности давайте разберём принцип работы нашей системы деплоя. Это собственная разработка: она похожа на Kubernetes, но не копирует его. В этом объяснении я буду использовать терминологию, характерную для Kubernetes, чтобы упростить восприятие.
Схема системы деплоя

Самая важная часть в системе деплоя — конечно, человек: именно он пишет спецификацию. Спека отвечает на вопросы: что катить, как катить и куда катить. Ну, например, в спеке мы можем написать: «Я хочу выкатить Nginx, 10 подов в 3 дата-центра и проверить доступность 80-го порта». Эта спека попадает в базу данных Yandex Planner поверх YTsaurus (получается аналог etcd).
Дальше спеку читают и обогащают контроллеры: включают логирование из коробки, чтобы все Nginx показывали логи в интерфейсе. Добавляют мониторинг с графиками потребления CPU и памяти, чтобы пользователь видел, какая у него геометрия контейнеров и сколько ресурсов они съедают. Настраивают алерты, чтобы пользователь узнал, когда заканчивается место на диске или память.
Обогащённая спека снова попадает в базу данных, а дальше её подхватывает хостовый агент, который написан на основе поведенческих деревьев. Он читает спеку и собирает под прямо на хосте. Агент идёт в container runtime и сообщает: «У меня есть геометрия пода — нужны контейнеры с такими-то ресурсами, ограничениями и видимостью». Контейнерный runtime позволяет не ходить с системными вызовами в ядро Linux, а собирать контейнеры при помощи API контейнерного runtime.
При помощи cgroups и namespaces мы можем создавать любые контейнеры любой формы и вложенности. Именно этим мы и воспользовались. Мы поняли, что можно ускорить выкладку, если разбить её на два этапа — prepare и switch.

Первый этап, подготовка, — самый длительный. Здесь мы скачиваем ресурсы, готовим сеть, запускаем хуки, приносим секреты, проверяем целостность файлов. Всё сложное и дорогое делаем в фазе подготовки. На этапе активации стартует workload — при необходимости проверяем, что он работает.
Эти фазы независимы, поэтому мы разнесли их и сделали систему подконтейнеров.

Внутри подконтейнеров живут Docker-контейнеры. На картинке видно, что один активный, а два скачались, разархивировались, подкачали слои, разложили секреты, проверили файлики, но команду запуска мы не выполняли.
Работает это так. Пользователь говорит: «Хочу хранить 10 релизов». Допустим, их Docker-образ весит 1 ГБ. Тогда резервируем место под 10 релизов — 10 ГБ на диске. При этом не просим дополнительно ни памяти, ни CPU, потому что эти контейнеры ничего не делают: они только лежат на диске и ждут, когда мы их запустим.
Подготовка не привязана к активации: её можно сделать хоть за сутки, хоть за месяц до релиза, а затем активировать в нужный момент. Мы буквально гасим один контейнер и поднимаем другой. Это и есть symlink в мире контейнеров.

Сравниваем системы откатов
Дальше расскажу, как мы пришли к этому решению, какие были альтернативы и почему они нам не подошли.
Blue-Green: нужно вдвое больше ресурсов
Возьмём, например, Blue-Green. Он работает так: есть два кластера — кластер A и кластер B. Вы выкатываете релиз на кластер B и, если там всё хорошо, переводите туда трафик. Нюанс в том, что Blue-Green требует дополнительных ресурсов: памяти и процессорного времени. А у нас есть, например, Яндекс Поиск. Blue-Green требует ×2 ресурсов, а Поиск не влезает уже в ×1.
Получается, что Blue-Green — классная штука, если у вас мало подов и есть запасные ресурсы. К сожалению, в масштабах Яндекса это не работает.
Canary: ловит не те проблемы
Канарейка — это «мини-кластер», куда идёт часть нагрузки. Если что-то пошло не так, вы снимаете нагрузку, откатываете канарейку и возвращаете нагрузку. Канарейка тоже требует дополнительных ресурсов. И тут есть один подвох. Дело в том, что у нас есть тестовые кластеры и LLM, которая пишет тесты и сама их поднимает, так что к моменту выкатки всё уже протестировано — канарейке нечего ловить.
В таком случае мы выкатимся на весь кластер и только тогда поймём, что есть какая-то проблема. Откуда она берётся, если всё протестировано? Например, вот так: мы полностью протестировали свой релиз, смежная команда — свой. Но совместимость двух релизов не тестировал никто. Мы входим в клинч, и кто-то из нас должен откатиться. Такой откат будет долгим, потому что мы уже выкатились на весь кластер. Поэтому канарейка нам тоже не подходит.
Подконтейнеры: платим только диском
Ещё способ накатывать и откатывать. Допустим, у нас кластер из четырёх нод с подконтейнерами. Мы говорим системе: «Хотим Docker-образ с новым релизом. Подготовь». И это может происходить с бюджетом 100%. Это неинвазивная и безопасная операция — мы просто перетаскиваем файлы. Когда они скачались, мы готовы с ними работать.
Дальше второй этап — активация, сам релиз. Он двигается окном: последовательно гасим и запускаем подконтейнеры. Получается, буквально перещёлкиваем symlink. Откат происходит в обратном порядке. Это гораздо быстрее, чем полностью пересобрать контейнер и заново качать файлы.
И главное — мы не платим за это дополнительные CPU/RAM, используется только дополнительное место на диске.
Откат накатом: 10 минут на откат
Секретный способ, который все используют, — откат накатом. Представим, у вас есть кластер и там произошла какая-то ошибка. Вы просто собираете новый релиз и заново катите его целиком. Это долго, дорого и, к сожалению, тоже нам не подходит.
На примере 1000 подов можно увидеть, как это работает. Эмулируем выкладку и говорим, что у нас есть оценка: в среднем одна минута на один под. Катим бюджетом по 10%, то есть активируем батчами по 100 подов, и так десять раз. В таком случае 100 подов параллельно поднимаются примерно 1 минуту, а 1000 подов — примерно 10 минут.

С Blue-Green мы очень быстро можем выкатиться, и скорость переключения зависит только от скорости переключения трафика. Тут откатиться получится быстро.
С канарейкой как повезёт. Если мы поймали проблему на канарейке, то можем откатиться за одну минуту. Ну, потому что там 100 подов — это условно кластер. Если не повезло, то мы будем откатываться целых 10 минут.
С нашими подконтейнерами весь кластер мы можем откатить меньше чем за 2 минуты, потому что активация — это 10% от всего пайплайна. То есть примерно 10 секунд мы будем активировать workload и переключать symlink. Соответственно, откат накатом — это 10 минут на откат.
Классная система, но она не решает всех проблем, а закрывает одну конкретную задачу — быстрый откат. Она не поможет, если у вас, например, база данных и есть какая-то миграция. Вы быстро откатитесь, но ничего не будет работать, так как откатится только код.
Например, если у вас долго поднимаются бинари и долго проходят readiness probe, быстро откатиться тоже не выйдет: время съедает подъём процесса, а не переключение. Ключевой момент: решение дорогое. Чтобы его воплотить, нужна собственная инфраструктура или свой способ работать с cgroups и namespaces.
Мы в Яндексе всегда стремились быстро откатывать, и для нас это был де-факто стандарт. Поэтому интеграция была дешёвой, а разработка — достаточно дорогой.
Что с этим всем делать
Повторить подконтейнеры без своей инфраструктуры сложно. А вот саму идею можно использовать как угодно: скачивание, распаковку, прогрев, секреты, проверки достаточно сделать заранее и держать на диске рядом с работающим релизом. Тогда и на релизе, и на откате остаётся только переключение.
Отсюда проверка для любой системы деплоя: сколько реального времени уходит на возвращение старого кода? Если ответ измеряется десятками минут, посмотрите, сколько из них уходит на подготовку, а сколько — на сам старт. Подготовку почти всегда можно вынести вперёд, а вот старт не всегда.
Вся эта работа понадобилась ради умения, которое было у нас ещё во времена tar-архивов. Зато теперь у нас есть изоляция, воспроизводимость, декларативность — и скорость отката, близкая к symlink.
Кстати, насчёт работы. Узнать, как мы делаем внутреннюю инфраструктуру Яндекса, можно в нашем канале. А вопросы про деплой давайте обсудим в комментариях.