Переїзд без зупинки: як 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: старий сервер при цьому лишається недоторканим, тож втрачати нічого.
