Как перенести Docker Compose на новую VPS без даунтайма

от автора

Введение

Совсем недавно я переносил работающий прод с одной впс на другую. Причина простая — новый провайдер давал в несколько раз больше ресурсов за те же деньги и с тем же SLA, но были две проблемы: Первая — сервисом пользовались круглосуточно, и даже несколько минут простоя были нежелательны. Вторая проблема — инфраструктура очень скромная, и подозреваю, знакомая многим: один сервер, docker compose, nginx как реверс прокси. Все легкие способы переезда отпадают сразу.

Подготовка: mise en place

В идеале к переносу у вас останется только переключение днс на новый айпи. Перед днем X должно быть закрыто всё из этого списка:

  • Новая впс поднята, базовая настройка сделана

  • Compose стек развёрнут, все контейнеры в состоянии healthy

  • TLS-сертификаты скопированы на новый сервер, nginx отвечает по HTTPS

  • Конфиг прокси для старого nginx написан и лежит наготове, но не включен

  • Обратный конфиг прокси для нового nginx написан, это страховка для отката

  • Желательно снижен TTL у днс записей, это ускорит распространение изменений при переключении

  • Доступ к панели DNS проверен 🙂

Если сервис не поднимается на чистой машине из compose файла, вы узнаете об этом сейчас и устраните проблему в спокойной обстановке.

Данные: главный вопрос переезда

Если у вас бд находится на отдельном сервере — можете смело пропускать этот раздел, у вас все отлично)

Правило этого переезда звучит так: stateful сервисы не переезжают вместе с приложением. Пока что база данных остаётся на старой впс, и оба приложения ходят в нее, пока идёт переходный период. Старое и новое приложение видят одни и те же данные, одно и то же состояние, одни и те же транзакции.

Поэтому старый сервер мы не выключаем, ни в день X, ни на следующий день. Он продолжает жить в двух ролях: прокси для DNS хвоста и хост базы данных.

До переезда база данных жила в docker сети и наружу не смотрела. Сохраняем это свойство и дальше — поднимаем любой защищённый канал между серверами, например, WireGuard. Так решаются сразу два вопроса: шифрование межсерверного трафика и доступ только с нужного сервера. На старой VPS порт базы публикуем с привязкой к адресу wg интерфейса, и приложение на новой машине ходит по этому адресу как по обычному хосту.

Два приложения теперь ходят в одну базу

Два приложения теперь ходят в одну базу

Второй этап — переезд самой базы. Это отдельный этап, который выполняется позже, когда приложение уже стабильно работает на новом сервере. Для бесшовного(почти) переноса обычно используют один из двух подходов:

  • штатную репликацию БД

  • собственный ETL с последующей синхронизацией

Баз данных бывает огромное количество, детальные кейсы тут, к сожалению, не перечислить.

Переключаемся

Здесь уже самое главное. Мы должны настроить реверс прокси как прокси к новому серверу и переключить днс.

До переезда - всё живёт на старой впс

До переезда — всё живёт на старой впс

Настраиваем реверс прокси, дальше выполняем команду: nginx -t && nginx -s reload. Поднимаются новые воркеры с новым конфигом, а старые дорабатывают уже принятые соединения и умирают. Ни одно активное соединение не рвётся.

С этой секунды все запросы обрабатывает новое приложение. Делаем паузу и смотрим на метрики — логи нового сервера, латенси. Учитываем, что прокси добавил сетевую задержку.

Старый nginx стал прокси

Дальше идём в панель DNS провайдера, надеюсь, вы запомнили пароль. Меняем A запись на новый IP. Смена записи не действует мгновенно, резолверы по всему миру закэшировали старый IP и будут отдавать его клиентам, пока не истечёт TTL. Всё это время часть трафика будет продолжать приходить на старый сервер.

Снова повторю главное правило этой статьи: старый сервер ни в коем случае не выключаем. Он продолжает работать как прокси, пока не обновятся DNS кэши клиентов. И это не единственная причина, ведь stateful сервисы к этому моменту ещё тоже не перенесены.

Наблюдать за хвостом удобно по access логам старого nginx: в первый час трафик на нём заметно проседает, к концу первых суток остаются единицы запросов — особо упрямые резолверы. Но пустые логи это ещё не всё, долгоживущие соединения, установленные до переключения, в access логах не видны, nginx пишет строку только при закрытии. Их проверяем отдельно, например, через ss на старом сервере. Логи пусты, живых клиентских соединений нет — можно переходить к остановке сервисов.

Сменили DNS - хвост кэшей дожимается через прокси

Сменили DNS — хвост кэшей дожимается через прокси

Убиваем старое

К этому моменту всё обрабатывает новое приложение. На старом сервере остались работающие, но уже ненужные сервисы: веб приложение, воркеры, и nginx в роли прокси.

Старый веб контейнер не получает новых запросов с момента, когда мы настроили проксирование запросов на новый сервер. Когда метрики нового сервера за какое-то время не показали ничего подозрительного, старый веб можно останавливать обычным docker compose stop

С воркерами интереснее. Они, в отличие от веба, всё ещё работают: старые и новые воркеры вместе разбирают одну шину событий, которая осталась на нашей впс или работает где-то отдельно. Нельзя просто так взять и убить старых воркеров — для неидемпотентных задач это прямой путь к неконсистентному состоянию. Поэтому старые воркеры нужно завершать корректно, дав им дообработать текущие задачи:

  1. Воркер получает SIGTERM, его посылает docker compose stop

  2. Внутри приложения переключается распределенный флаг is_accepting=False — специальный guard класс, обёртка над консьюмерами.

  3. С этого момента новые сообщения воркер не берёт: всё, что приходит, сразу возвращается в очередь через nack — и его подхватят воркеры на новом сервере.

  4. Задачи, которые уже были в обработке, спокойно дорабатываются до конца.

  5. Выждав время максимальной задачи (с запасом), процесс корректно завершается.

Механизм остановки старых воркеров

Механизм остановки старых воркеров

Подробный разбор этого механизма с кодом: guard-класс, декоратор над консьюмерами, тонкости с graceful timeout контейнера есть в моей статье про blue-green деплой на Docker Compose. Вся прелесть этого механизма еще в том, что мы не зависим от брокера.

Nginx прокси выключаем тогда, когда в логах пусто. Ну а базы данных, брокеры и прочие stateful сервисы на этом этапе не трогаем. Новое приложение продолжает ходить к ним по сети. Их перенос выполняется позже, отдельно от переезда приложения.

Получается, что старый сервер не умирает, а меняет роль. Был сервером приложения — стал сервером данных. А может, ему в этой роли и остаться? 🙂 Приложение теперь можно перевозить, пересоздавать и масштабировать хоть каждую неделю, не думая о данных вообще.

Старый сервер меняет свою роль

Старый сервер меняет свою роль

Заключение

Пользователи переезда не заметили: ни одного оборванного запроса, ни одной потерянной задачи. Для них сервис просто продолжил работать, хотя под ним поменяли сервер.

Главная идея этого подхода: не пытаться переносить приложение и данные одновременно. Stateless сервсисы можно спокойно перевезти, проверить под реальной нагрузкой и, при необходимости, быстро откатить изменения. Stateful сервисы остаются на старом сервере до тех пор, пока новое приложение не подтвердит свою стабильность.

ссылка на оригинал статьи https://habr.com/ru/articles/1055858/