Открыть список
Как стать автором
Обновить
31,01
Рейтинг
Virtuozzo
Разработчик ПО для контейнерной виртуализации

Как обновить ядро в системе без перезапуска сервисов (пошаговая инструкция)

Блог компании VirtuozzoOpen sourceСистемное программированиеРазработка под Linux
Как вы думаете насколько реально зайти на машину по ssh, обновить систему, загрузить новое ядро и при этом оставаться в той же ssh сессии. Сейчас есть модное движения по обновлению ядра на лету (ksplice, KernelCare, ReadyKernel, etc), но у этого способа есть много ограничений. Во-первых, он не позволяет применять изменения, которые меняют структуру данных. Во-вторых, объекты в памяти могут уже содержать неверные данные, которые могут вызвать проблемы в дальнейшем. Здесь будет описан более «честный» способ обновить ядро. На самом деле, сам способ уже давно известен [1], а ценность этой статьи в том, что мы разберем все в деталях на реальном примере, поймем, насколько это просто или сложно, и чего стоит ждать от подобных экспериментов.

Travis CI — одна из популярных систем непрерывной интеграции, которая хорошо работает с Github. Сервис быстро развивается и если несколько лет назад он предоставлял только контейнеры с не очень свежими дистрибутивами, то сегодня там есть выбор между контейнерами и вмками, есть поддержка не только Linux систем и многое другое.

Мы начали использовать Travis-CI в нашем проекте CRIU (checkpoint/restrore in userspace) несколько лет назад и всегда брали от сервиса максимум. Начинали с проверки компиляции на x86_64, а сегодня Travis-CI запускает наши тесты, проверяет компиляцию на всех архитектурах, с разными компиляторами и даже тестирует совместимость с новыми ядрами, в том числе и самой нестабильной и передовой веткой Linux-Next.

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

А теперь к делу, господа…


Но сегодня я хочу рассказать совсем не о том, как мы тестируем CRIU, а об одном интересном варианте его использования. Представьте, что на входе у нас есть виртуальная машина, в которой через ssh запущен процесс. Как нам загрузить свое ядро так, чтобы процесс этого не заметил? Это ровно та ситуация, которую мы имеем в Travis-CI.

Внешнего доступа к виртуалке мы не имеем, и если процесс Travis по какой-то причине умирает (завершается), то сервис завершает задачу и удаляет ВМ. Согласитесь, задачка, прямо скажем, непростая. Мы даже сделали внизу голосование – просигнальте, пришло ли вам сразу в голову решение или нет.

Но мы поступили следующим образом: берем CRIU, дампим ssh-сессию Travis, загружаем новое ядро, восстанавливаем процессы и бежим дальше. Примерно так я думал, когда решил немного развлечься после обеда и показать, как все это взлетит.

Сразу скажу, что задача – отнюдь не абстрактная. У нее есть несколько реальных применений. Одно из них — это желание некоторых пользователей загрузить Ubuntu 16.04 (https://github.com/travis-ci/travis-ci/issues/5821). Разработчики Travis решать эту задачу пока не собираются, а мы можем попробовать сделать это без их помощи. Идея тут та же, берем начальную систему 14.04, обновляем ее и перезагружаемся в новое окружение.

Решение


Обновление системы — меньшая из бед, решается парой команд:

sed -i -e "s/trusty/xenial/g" /etc/apt/sources.list
apt-get update && apt-get dist-upgrade -y

Но дальше становится намного веселее. Во-первых, возникает опрос: откуда начинать дампить? Во-вторых, как будем восстанавливать? Если что-то пойдет не так, как мы узнаем, что именно? От замороженного Travis помощи ждать не приходится.

Так что начнем разбираться своими силами. Смотрим на дерево процессов и понимаем, что дампить надо начинать с процесса SSHD, который обрабатывает нашу SSH-сессию.

Дерево процессов:

12253 ?        Ss     0:03 /usr/sbin/sshd -D
32443 ?        Ss     0:00  \_ sshd: root@pts/0
32539 pts/0    Ss     0:00  |   \_ -bash

Идем по всем родителям, начиная с себя, и берем второй процесс sshd от init-а:

ppid=""
pid=$$
while :; do
   p=$(awk '/^PPid:/ { print $2 }' /proc/$pid/status)
   test “$p” -eq 1 && break
   ppid=$pid
   pid=$p
done
echo $pid

Теперь мы знаем кого дампить и надо решить кто будет этим заниматься. Стоит учесть, что CRIU не позволяет «пилить сук, на котором сидит», так что придется создать сторонний процесс:

setsid bash -c "setsid ./scripts/travis/kexec-dump.sh $ppid < /dev/null &> /travis.log &"

Пришло время сочинить команду для дампа. Если вы думаете, что это не сложно, то сильно ошибаетесь. В CRIU уже наросло такое количество опций, что не все разработчики могут сразу в них разобраться. Но на самом деле, все не так плохо, если разобраться. Строчка кода получилась достаточно короткая.

./criu/criu dump -D /imgs -o dump.log -t $pid --tcp-established --ext-unix-sk -v4 --file-locks —link-remap

Если перевести ее на русский язык, это команда звучит примерно так: “CRIU, сделай нам дамп поддерева начиная с процесса $pid, все данные сложи в директорию /imgs, логи сохрани в файле dump.log, рассказывай подробно обо всем что делаешь, а также разрешаем тебе сохранить tcp-сокеты, unix-сокеты, связанные с внешним миром, файловые локи и дескрипторы на удаленные файлы”.

Кажется, тут все понятно, кроме удаленных файлов — откуда они возьмутся? Но достаточно вспомнить, что мы установили мажорный update на систему, а это значит, что обновилось почти все, в том числе библиотеки и запускаемые файлы. При этом наш процесс не перезапускался и по прежнему использует старые версии этих файлов. Именно для них мы и указываем опцию --link-remap.

Тут же возникает и еще одна проблема. Между сохранением и восстановлением процессов сетевой трафик должен быть заблокирован, иначе нет никакой гарантии, что TCP соединения переживут эту операцию. CRIU добавляет для этого пару правил iptables, и наша задача — эти правила восстановить после загрузки нового ядра, но до того как произойдет настройка сети. Здесь мне пришлось немного погуглить, но в целом также задача решилась не слишком сложно.

cat > /etc/network/if-pre-up.d/iptablesload << EOF
#!/bin/sh
iptables-restore < /etc/iptables.rules
unlink /etc/network/if-pre-up.d/iptablesload
unlink /etc/iptables.rules
exit 0
EOF

chmod +x /etc/network/if-pre-up.d/iptablesload
iptables-save -c > /etc/iptables.rules

Восстановление


Итак, процессы сохранены, и пришло время время подготовить того, кто будет их восстанавливать. Тут нам придется написать свой небольшой сервис.

cat > /lib/systemd/system/crtr.service << EOF
[Unit]
Description=Restore a Travis process

[Service]
Type=idle
ExecStart=/root/criu/scripts/travis/kexec-restore.sh $d $f

[Install]
WantedBy=multi-user.target
EOF

Кажется все готово и можно взлетать. Ключ на старт.

kernel=$(ls /boot/vmlinuz* | tail -n 1 | sed 's/.*vmlinuz-\(.*\)/\1/')
echo $kernel
kexec -l /boot/vmlinuz-$kernel --initrd=/boot/initrd.img-$kernel --reuse-cmdline

Полетели!


kexec -e

Так мы взлетели, но, как и SpaceX, с первого раза сесть не смогли. А не смогли мы, потому что посадочная платформа была кем-то уже занята. А если серьезно, то проблема в том, что CRIU позволяет восстанавливать процессы только с теми же идентификаторами, что у них были на момент дампа. Мы же перезагрузились в новую систему, где systemd (!!!) и процессов стало немного больше. Эта проблема уже давно изучена наукой, и тут нам помогут контейнеры, точнее говоря, только их маленькая часть, называемая пространством имен процессов (pid namespace).

unshare -pfm --mount-proc --propagation=private ./criu/criu restore \
-D /imgs -o restore.log -j --tcp-established --ext-unix-sk \
-v4 -l --link-remap &

Попробуем взлететь, и снова наш корабль не выходит на связь. На этот раз идей о неполадках никаких нет, и надо как-то добывать логи. Тут было решено не думать долго, а взять да и залить их на одно из популярных хранилищ разных отходов.

#!/usr/bin/env python2
import dropbox, sys, os
access_token = os.getenv("DROPBOX_TOKEN")
client = dropbox.client.DropboxClient(access_token)
f = open(sys.argv[1])
fname = os.path.basename(sys.argv[1])
response = client.put_file(fname, f)
print 'uploaded: ', response
print "====================="
print client.share(fname)['url']
print "====================="

Под прицелом камер мы теряем очередной корабль и понимаем, что шутки кончились. На этот раз нам жалуются на DBus сокет, т е это уже такая связь, состояние которой нам недоступно, ведь ей владеет только DBus-демон. С другой стороны, зачем sshd нужен этот сокет? Наверняка он хочет отслеживать состояние сети и прочую ерунду. Мы ничего такого делать не собираемся (точнее мы уже все сделали), так что давайте просто восстановим этот сокет как-нибудь и поедем дальше.

diff --git a/criu/sk-unix.c b/criu/sk-unix.c
index 5cbe07a..f856552 100644
--- a/criu/sk-unix.c
+++ b/criu/sk-unix.c
@@ -708,5 +708,4 @@ static int dump_external_sockets(struct unix_sk_desc *peer)
                               if (peer->type != SOCK_DGRAM) {
                                       show_one_unix("Ext stream not supported", peer);
                                       pr_err("Can't dump half of stream unix connection.\n");
-                                       return -1;
                               }

Фактически мы сделали свой собственный патч для CRIU. Это можно было решить более элегантно с помощью плагинов, но так было быстрее. Снова заливаем наши изменения и ждем очередного падения. На этот раз возникает проблема с псевдотерминалами: нужные нам номера уже кем-то используются. Мы могли бы монтировать devpts с newinstance, но эта опция с недавнего времени не работает.

   - The newinstance mount option continues to be accepted but is now
      Ignored. // Eric W. Biederman

Похоже пришло время залезть в образы процессов и немного подправить их напильником. Давайте поменяем в них номера псевдотерминалов и добавим префикс 1. Был терминал с номером 1, станет с номером 11. Для этого в CRIU есть возможность переформатировать образа в Json формат и обратно. Выглядит это примерно так:

./crit/crit show /imgs/tty-info.img  | \
   sed 's/"index": \([0-9]*\)/"index": 1\1/' | \
   ./crit/crit encode > /imgs/tty-info.img.new
./crit/crit show /imgs/reg-files.img  | \
   sed 's|/dev/pts/\([0-9]*\)|/dev/pts/1\1|' | \
   ./crit/crit encode > /imgs/reg-files.img.new

Опять запускаем и ждем. Время уже давно послеобеденное, и вся эта затея явно сильно затянулась. Привычно получаем ошибку — на этот раз о том, что какие-то fifo файлы из /run/systemd/sessions не могут быть восстановлены. Разбираться, что это за файлы, нет никакого желания, поэтому перед восстановлением просто создадим их и побежим дальше.

f=$(lsof -p $1 | grep /run/systemd/sessions | awk '{ print $9 }')
...
criu dump
kexec
mkfifo $f
criu restore

Опять падаем, и на этот раз похоже налетаем на баг в CRIU. Видим, что sys_prctl(PR_SET_MM, PR_SET_MM_MAP, …) возвращает EACCES, лезем в ядро и находим, что виной тому восстановление ссылки на запускаемый файл. Ядро видит, что мы передаем ссылку на файл, у которого нет соответствующего бита. Вы же помните, что мы обновили систему целиком, и теперь эта ссылка из процесса указывает на удаленный файл. Оказывается, что перед тем как удалить файл, dpkg снял с него права на запуск.

# strace -e chmod,link,unlink -f apt-get install --reinstall sudo
...
3331  link("/usr/bin/sudo", "/usr/bin/sudo.dpkg-tmp") = 0
3331  chmod("/usr/bin/sudo.dpkg-tmp", 0600) = 0
3331  unlink("/usr/bin/sudo.dpkg-tmp")  = 0
...

Кажется, достаточно сделать еще один патч к CRIU, и золотой ключик будет у нас в кармане.

diff --git a/criu/cr-restore.c b/criu/cr-restore.c
index 12f13ae..39277cf 100644
--- a/criu/cr-restore.c
+++ b/criu/cr-restore.c
@@ -2278,6 +2278,23 @@ static int prepare_mm(pid_t pid, struct task_restore_args *args)
       if (exe_fd < 0)
               goto out;

+       {
+               struct stat st;
+
+               if (fstat(exe_fd, &st)) {
+                       pr_perror("Unable to stat a file");
+                       return -1;
+               }
+
+               if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
+                       pr_debug("Add the execution bit for %d (st_mode %o)\n", exe_fd, st.st_mode);
+                       if (fchmod(exe_fd, st.st_mode | S_IXUSR)) {
+                               pr_perror("Unable to add the execution bit");
+                               return -1;
+                       }
+               }
+       }
+
       args->fd_exe_link = exe_fd;
       ret = 0;
out:

Заключение


Ура! Все работает https://travis-ci.org/avagin/criu/builds/181822758. На самом деле, это очень краткий пересказ всей истории. Мне пришлось запускать эту задачу в Travis 33 раза, прежде чем она впервые прошла успешно.

Что мы этим доказали? Во-первых, решили пару прикладных задач, а во-вторых показали, что CRIU — это очень низкоуровневый инструмент и даже простая задача может потребовать глубоких знаний системы. Зато старания компенсируются мощностью, гибкостью и широкими возможностями. Хотя никто не гарантирует, что вам не придётся повоевать с багами.

Удачи на космических просторах!
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как быстро Вы догадались, что мы будем сохранять состояние процессов?
15.79% Понял еще по заглавию 15
10.53% Дочитал до ката 10
26.32% Сразу, как увидел CRIU 25
27.37% Зачем так извращаться? 26
2.11% У меня все локально, могу делать все, что захочу 2
7.37% У меня все в Docker контейнерах, мне без разницы какая там система 7
3.16% У нас в Windows все намного проще 3
1.05% У нас на Java своя виртуальная машина 1
6.32% Ничего нового, еще на мейнфремйах процессы сохранял 6
Проголосовали 95 пользователей. Воздержались 45 пользователей.
Теги:linuxopen sourcetravis-citraviscriuopenvzvirtuozzovzkernelsystemdubuntucontainerskexeccheckpointdocker
Хабы: Блог компании Virtuozzo Open source Системное программирование Разработка под Linux
Всего голосов 28: ↑27 и ↓1 +26
Просмотры10.7K

Комментарии 11

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
www.virtuozzo.com
Численность
101–200 человек
Дата регистрации

Блог на Хабре