Переезд без остановки: как reloca.to копирует работающий сервер

Почему ночного окна хватает не всем
Классический переезд требует, чтобы старый сервер замолчал. Его грузят в Live OS, снимают диски по сети, собирают систему на новом железе, проверяют и только потом включают. Для сервера с терабайтом данных это от получаса до нескольких часов, и всё это время сайт лежит, база не отвечает, а письма копятся у отправителей.
Часть клиентов такое окно согласует на ночь и спокойно переезжает. Остальные откладывают переезд годами. У кого-то интернет-магазин, у кого-то биллинг, у кого-то просто нет человека, готового сидеть до утра и смотреть в консоль. Мы услышали это достаточно раз, чтобы попробовать убрать само окно.
Что здесь можно взять у виртуализации
В виртуализации задача решена давно. vMotion у VMware и live migration у KVM переносят работающую машину на другой хост: память копируется в несколько проходов, пока гостевая система продолжает работать, а в конце она замирает на доли секунды, догоняет последнюю порцию и просыпается уже на новом хосте.
С выделенными серверами так не выйдет. Процессоры, чипсеты и сетевые карты разные, перенести оперативную память вместе с состоянием процессов невозможно, перезагрузка останется в любом случае. Но главную часть идеи взять можно: копировать диски, пока система работает, несколько раз досылать только то, что успело измениться, и останавливать сервер в самом конце и ненадолго. Этот режим мы назвали on-line relocation, а классический переезд через Live OS на обеих машинах теперь называется off-line relocation.
Как копировать диск, в который пишут
В on-line режиме старый сервер остаётся в своей системе. Утилита приносит на него один статический бинарник reloca-agent, запускает его от root и дальше разговаривает с ним по ssh. На доноре не появляется ни пакетов, ни демонов, ни записей на диск.
Агент делает две вещи одновременно. Он читает блочные устройства напрямую, мимо кеша, и отдаёт их кадрами по сети. И он следит за каждой записью на эти устройства, чтобы знать, какие участки копии уже устарели.
Следит он двумя способами, по возможностям ядра. Где есть eBPF, агент собирает крошечную программу прямо в памяти, цепляет её на точку block_bio_queue и отмечает в битовой карте каждый кусок по 64 КиБ, в который кто-то написал. Где eBPF нет, а среди переезжающих систем CentOS 7 с ядром 3.10 попадается постоянно, он включает ftrace и читает тот же поток событий текстом из trace_pipe. Второй способ обходится дороже, но работает на всём, что мы пробовали, вплоть до 2013 года.
Первый проход читает тома целиком. Пустые места при этом не едут: агент узнаёт блок из нулей и вместо данных отправляет короткую запись «здесь столько-то нулей», а получатель обнуляет весь диск одним ioctl в самом начале, поэтому ему и писать ничего не приходится. На томе, где занято 2.7 ГиБ из 12, это экономит и трафик, и минуты.
Проходы и сходимость
После первого прохода агент закрывает эпоху трекера и смотрит, что накопилось. Дальше по сети идут только изменившиеся куски. Каждый следующий проход короче предыдущего, потому что за меньшее время успевают испортить меньше блоков, и дельта сжимается от прохода к проходу.
Остановиться нужно в правильный момент. Агент считает его так: оставшаяся дельта на уже измеренной скорости канала должна передаваться за то время, которое вы готовы признать простоем. По умолчанию это десять секунд, -o converge=10. Как только очередная дельта в это укладывается, начинается финал.
Несколько секунд заморозки
Копия живой файловой системы всегда немного рассогласована: пока читали начало, кто-то переписал середину. Поэтому последнюю дельту снимают с остановленной записи.
Останавливает её fsfreeze, то есть ioctl FIFREEZE. Ядро сбрасывает грязные страницы, замораживает журнал и задерживает всех, кто пытается писать. Процессы не падают и не получают ошибок, они просто ждут в своём write несколько секунд. Чтение продолжает работать, поэтому nginx в это время отдаёт статику, а ssh-сессия не отваливается.
Агент морозит все смонтированные файловые системы донора разом, начиная с самых глубоких точек монтирования. То, что заморозить нельзя, он пропускает и пишет об этом в лог: ESP на vfat такой ioctl не поддерживает, но на загруженной системе в раздел с загрузчиком никто и не пишет. Затем проходит последняя дельта, и донор остаётся замороженным до подтверждения. Если на новом сервере что-то не так, откат это одна команда fsfreeze -u с консоли, после которой старый сервер продолжает работать как ни в чём не бывало.
На стенде с Debian 12 и Rocky 9 окно заморозки укладывается в 1.7 до 2.4 секунды при канале 10 GbE. Это и есть весь простой до перезагрузки нового сервера.
Когда дельты перестают уменьшаться
Сходится эта схема не всегда. Если сервер пишет на диск быстрее, чем канал уносит данные, очередная дельта окажется не меньше предыдущей, и ждать сходимости можно бесконечно.
Поэтому агент меряет обе скорости: сколько мегабайт в секунду уходит на провод и сколько мегабайт в секунду пачкается на доноре. Из их отношения он прямо считает, сколько проходов осталось до заказанного окна и сколько это займёт минут, и говорит об этом вслух:
the donor writes 20.4 MiB/s, the link carries 14.9 MiB/s (1.37x): the deltas
cannot shrink, this will not converge
Если же предсказание выглядит оптимистично, агент не упирается в лимит проходов. Когда расчёт говорит, что ещё три прохода дадут нужное окно и займут двенадцать минут, он сам поднимает лимит и копирует дальше, пока укладывается в бюджет терпения (-o patience=1800, полчаса по умолчанию). Если не укладывается, он останавливается и предлагает то окно, которое есть.
А если и это окно слишком длинное, запуск отказывается морозить донора вообще и объясняет, что именно изменит решение:
the shortest freeze on offer is 180 s, above the 100 s this run was allowed
(--max-est): nothing is frozen, the donor is untouched
what changes it: a faster link, less writing on the donor while it copies,
or -o max-est=180
Диск нового сервера к этому моменту уже записан, а старый сервер не остановлен ни на секунду, так что переезд можно повторить ночью, когда записи меньше.
Сжатие, которое выбирает себя само
Сжимать кадры на доноре выгодно ровно до тех пор, пока его процессор успевает за каналом. На 10 GbE одно ядро с zstd быстро становится узким местом. На канале между дата-центрами в 100 Мбит оно, наоборот, простаивает.
Раньше это была опция, которую приходилось угадывать. Теперь агент меряет и её. Он знает, сколько секунд потратил на сжатие и сколько байт из этого вышло, то есть знает производительность своего компрессора в мегабайтах в секунду, и сравнивает её со скоростью провода. Пока процессор вдвое быстрее канала и данные вообще сжимаются, уровень растёт (1, 3, 5, 7). Как только перестаёт успевать, уровень опускается, и этот потолок больше не пробуется.
compression: donor 621 MiB/s at zstd:1, link 11 MiB/s, ratio 1.56x -> zstd:3
На CentOS 7 через 100 Мбит сквозной переезд с compress=auto занял 823 секунды против 953 с жёстко заданным первым уровнем и 932 с третьим.
Предпроверка: репетиция на снимке
В off-line режиме порядок простой: скопировали, доработали копию в chroot, проверили, перезагрузились. В on-line режиме доработка и проверка приходятся на копию, которая ещё догоняет оригинал. Узнать, что система не загрузится, хочется до того, как старый сервер ушёл из сети.
Поэтому по умолчанию донор морозится дважды. После первой заморозки утилита снимает dm-снимок копии и размораживает донора. Пока он снова обслуживает пользователей, на снимке целиком прогоняются postfix и verify: собирается initramfs с драйверами нового контроллера, ставится grub, проверяется загрузочная запись, fstab и сеть. Снимок выбрасывается, дельты догоняются, донор морозится второй раз, и только теперь происходит настоящее переключение.
Стоит это одного лишнего окна заморозки. На быстром канале обе заморозки около секунды. На 100 Мбит это 100 секунд перед предпроверкой и ещё 87 перед cutover, и там -o precheck=0 экономит заметно, ценой того, что postfix и verify впервые выполнятся уже после последней заморозки.
Что показал стенд
Все цифры ниже с одного и того же переезда: CentOS 7.9, корень на XFS, 2.7 ГиБ данных, генератор нагрузки пишет на диск около 20 МБ/с. Канал между машинами задаётся netem, то есть задержка и потери настоящие.
| Канал | Первый проход | Сходимость | Окно заморозки | Весь переезд |
|---|---|---|---|---|
| 10 GbE | 15 с, 147 МиБ/с | на третьем проходе | 1.7 с | 223 с |
| 400 Мбит, 20 мс | 41 с, 66 МиБ/с | на четвёртом проходе | 8.7 с | 341 с |
| 100 Мбит, 30 мс, 0.05% потерь | 185 с, 14.9 МиБ/с | не сходится | 100 с | 953 с |
Третья строка это тот самый случай, когда сервер пишет быстрее канала. Агент предсказал окно в 98.8 секунды, фактическое вышло 100.1, то есть оценка, по которой принимается решение, не врёт. На таком канале стоит либо переезжать в тихие часы, либо выбрать off-line режим и честное окно обслуживания.
Чего on-line relocation не делает
- Не переносит оперативную память. Процессы стартуют заново при загрузке нового сервера, как после обычной перезагрузки.
- Не сохраняет IP-адрес: новый сервер стоит в нашей сети, со своим адресом, и DNS придётся переключить. Это общая беда любого переезда между провайдерами.
- Не обходится без root на старом сервере и без ядра, умеющего отслеживать записи.
- Не работает с btrfs, ZFS, LUKS и NTFS. Поддерживаются ext2/3/4 и XFS, в том числе на md RAID и LVM.
- Не отменяет перезагрузку нового сервера: она и есть то самое переключение.
Какой режим выбрать
| off-line relocation | on-line relocation | |
|---|---|---|
| Старый сервер | загружен в Live OS | работает в своей системе |
| Простой | весь переезд, от получаса до нескольких часов | секунды заморозки плюс одна перезагрузка |
| Требования к старому серверу | kexec или ISO через консоль провайдера | root и ядро с eBPF или ftrace |
| Нагрузка во время переезда | никакой | чтение дисков и один поток zstd |
| Когда брать | окно обслуживания согласовано, сервер можно остановить | останавливать нельзя, а канал быстрее, чем сервер пишет |
Утилита, фазы и план в обоих режимах одни и те же. Режим она определяет сама по тому, откуда ответил старый сервер: из Live OS или из своей системы.
С чего начать
- Закажите сервер у XServer и загрузите его в Live OS: панель клиента, сервер, меню загрузки, Live OS.
- На старом сервере ничего делать не нужно, он продолжает работать. Понадобится только доступ root по ssh.
- Зайдите на новый сервер, поставьте утилиту и посмотрите план в сухом прогоне. Ничего не записывается, пока вы не прочитаете, что на диске, и не введёте его имя и размер с экрана.
ssh -A root@НОВЫЙ-СЕРВЕР curl -fsSL reloca.to | bash reloca.to -n root@СТАРЫЙ-IP # сухой прогон: план и каждая команда reloca.to root@СТАРЫЙ-IP # переезд целиком - Держите открытой консоль нового сервера на время переключения: первую загрузку видно только там.
Все опции с их значениями по умолчанию собраны в справочнике на reloca.to, фазы и коды выхода описаны там же. Если разметка нестандартная, ОС старая или проверка не прошла, напишите в поддержку XServer: старый сервер при этом остаётся нетронутым, так что терять нечего.
