Содержание статьи
Любому из нас приходится время от времени запускать тяжеловесные процессы, которые могут выполняться по несколько часов кряду. Это может быть компилирование больших проектов, обработка видео или фотографий. К сожалению, не всегда получается возобновить процесс, однажды прервав его, и бывает, что все приходится начинать сначала. Но что, если можно было бы заморозить приложение в любой точке, сохранить состояние на диск и возобновить исполнение в любой момент (возможно, даже на другом компе)?
Введение
Возможность заморозки и возобновления работы процессов — весьма заманчивая идея. На десктопе с ее помощью можно реализовать быструю загрузку операционной системы, когда вместо полноценного запуска все необходимые демоны и сервисы просто будут восстановлены с диска. Или, например, сохранить процесс на флешку, а затем восстановить его на другой машине и продолжать пользоваться приложением как ни в чем не бывало. Ну или, скажем, приостановить процесс обработки видео на время срочных работ с компом. На серверах функция заморозки может быть еще полезней. С ее помощью можно организовать процесс прозрачной миграции виртуальных машин, процесс балансировки нагрузки, или быстрого обновления, или ремонтных работ, при котором простои будут минимальны.
В разное время такую функциональность пытались реализовать многие программисты. Наиболее заметным стал проект CryoPID с одноименной утилитой, работающей в пространстве пользователя и позволяющей быстро сохранить образ процесса на диск, а затем восстановить его. К сожалению, CryoPID была эффективна только для простых консольных программ и часто давала сбои при попытке заморозить более комплексное приложение, работавшее с сетью или обладающее графическим интерфейсом. Также у нее было много других ограничений из-за того, что утилита не могла получить от ядра более детальные сведения о процессе, а затем правильно восстановить нужные структуры данных в нем же.
Многие другие схожие проекты также потерпели фиаско по тем же причинам, поэтому на долгое время об идее вроде бы забыли. Нужный функционал требовал содействия ядра, а соответствующие патчи никто не принимал, как ломающие внутренние структуры. Были и попытки создать реализацию чекпоинтинга с помощью сторонних библиотек, подменяющих системные вызовы и позволяющих более точно отслеживать состояние процесса (проект DMTCP, например), но, как и предшественники, они тоже страдали от проблемы недостатка содействия ядра в этом процессе и не могли реализовать идею заморозки полностью.
Тем не менее разработчикам из небезызвестной компании Parallels не так давно удалось добавить соответствующие функции в официальное ядро, и теперь мы имеем почти полноценный инструмент для заморозки процессов. Называется он CRIU и разрабатывается в составе системы виртуализации уровня ОС OpenVZ.
CRIU
История CRIU (Checkpoint/Restore In Userspace) началась в далеком 2005 году, когда компания Parallels взялась реализовать функцию заморозки/разморозки процессов на уровне ядра для проекта OpenVZ. Через несколько лет к работе подключились и другие компании, в результате чего было представлено более ста патчей для ядра, полностью реализующих нужную функциональность. Однако, чего и следовало ожидать, мантейнеры ядра отвергли патчи, как слишком комплексные, и Parallels не осталось другого выбора, как начать реализацию системы на уровне пользователя с небольшими изменениями в ядре в качестве вспомогательных функций.
В результате в 2011 году главный разработчик OpenVZ Павел Емельянов представил первую реализацию CRIU и соответствующий патчсет. На этот раз функциональность была почти полностью реализована на уровне пользователя, а в ядре содержался лишь минимальный набор функций, необходимых для получения более детальной информации о процессах, никак не ломающих внутреннюю согласованность структур ядра. Сообщество благосклонно приняло эту идею, и в январе 2012 года Линус Торвальдс интегрировал их в официальную ветку ядра, не забыв, правда, добавить ехидный комментарий Эндрю Мортона о сумасшедших русских и их сумасшедших идеях
Комментарий Эндрю Мортона
Это проект, разрабатываемый разными сумасшедшими русскими, по созданию/восстановлению контрольных точек в основном из пользовательского приложения, с различным странным вспомогательным кодом, добавленным в ядро там, где это необходимо. ... Однако я не так, как разработчики, уверен в том, что все это когда-нибудь заработает! Поэтому я прошу их «обернуть» макросом CONFIG_CHECKPOINT_RESTORE каждый кусок нового кода в ядре. Так что если со временем все это закончится слезами и проект в целом развалится, мы сможем пройтись по коду и выкинуть все без следа.
Далее последовали и другие патчи, и к данному моменту в ядре уже появилось девять новых функций, так или иначе относящихся к заморозке процессов. Почти все из них реализованы с помощью экспорта необходимой информации через новые файлы каталога /proc/PID/, которые при запуске читает утилита crtools. Среди добавленной функциональности можно отметить следующее:
- Каталог /proc/PID/map_files/. Содержит ссылки на все файлы, отображаемые приложением в виртуальную память, имя которых представляет собой адрес расположения файла в памяти. Для получения такой информации уже существовал файл /proc/PID/maps, однако новый каталог открывает большую гибкость, а также гарантирует, что замороженный процесс при восстановлении отобразит в память те же файлы, что и до заморозки.
- Файл /proc/PID/task/TID/children, который содержит список всех потомков процесса. Эта информация необходима для заморозки дерева процесса либо всех потоков одного приложения.
- Файл /proc/PID/stat теперь включает информацию об аргументах приложения, список переменных окружения и код возврата.
- Системный вызов prctl() был расширен и теперь может быть использован для восстановления аргументов приложения и переменных окружения после разморозки. Это и предыдущее новшество позволяют полностью восстановить информацию о процессе так, что утилиты типа ps не покажут разницы.
- Новый системный вызов kcmp(), который позволяет сравнить два процесса/потока и выявить разделяемые ими ресурсы.
- Для получения информации, доступной только самому процессу посредством системных вызовов, таких как getitimer() и sigaction(), задействована функция внедрения так называемого паразитного кода, разработанная Tejun Heo. Эта функция позволяет поместить в адресное пространство процесса участок кода, что используется CRIU для сбора недостающей информации с помощью системных вызовов.
- Добавлена новая sysctl-переменная kernel.nslastpid, которая позволяет явно указать, какой PID получит потомок процесса после следующего вызова clone(). Она нужна для того, чтобы после разморозки процесс получил свой прежний PID (изменившийся PID может вызвать очевидные проблемы, вроде различия PID, прописанного в файле /var/run/httpd.pid и реального).
Это только часть механизмов, добавленных в ядро для поддержки CRIU, но уже только они одни позволяют производить заморозку и восстановление многих приложений. Происходит этот процесс примерно так. При запуске процедуры заморозки crtools останавливает процесс с помощью системного вызова ptrace() (PTRACE_SEIZE), затем собирает информацию обо всех открытых им файлах, сокетах, потомках и прочем с помощью чтения файлов каталога /proc/PID/, системного вызова prctl() и вставки паразитного кода. Затем происходит сохранение образа памяти процесса, а также значений всех его регистров (опять же с помощью ptrace) и, наконец, остановка процесса.
После того как будет инициирован процесс разморозки, утилита crtools восстанавливает память процесса, устанавливает нужное значение PID в kernel.nslastpid, форкается, открывает все необходимые ресурсы и запускает исполнение с помощью системного вызова sigreturn(), возвращающего управление процессу именно в ту точку, в которой он был до заморозки. Таким образом удается заморозить и восстановить не только один процесс/поток, но и всех его потомков.
Более того, CRIU, как система, разрабатываемая в расчете на применение в системах виртуализации, позволяет не только полностью восстановить процесс, но и даже не потерять при этом сетевые соединения. Для этого в ядре была реализована система TCP repair mode, которая позволяет в прямом смысле разобрать и заново собрать сокет, никак при этом не взаимодействуя с другой стороной соединения. Эта функциональность, в частности, позволяет заморозить, например, Apache, затем перенести его образ на другую машину, разморозить, и он продолжит работать как ни в чем не бывало, не потеряв соединение даже с уже подключенными клиентами (если, конечно, клиент не успеет сам разорвать соединение из-за истечения времени ожидания).
Кроме этого, CRIU также поддерживает приложения, использующие такие функции, как inotify и epoll, а также другую функциональность ядра, однако эти пачти еще не включены в официальную ветку.
Ядро и утилита
Как я сказал выше, все нужное для работы CRIU уже есть в ванильном ядре версии 3.7. Однако многие дистрибутивостроители используют ядра с выключенной функцией чекпоинтинга, а это значит, что ядро придется пересобирать самостоятельно с нужными опциями. Как это делать, было описано уже миллиарды раз, поэтому ограничусь лишь списком опций, которые должны быть включены при сборке:
Опции ядра, необходимые CRIU
General setup -> Checkpoint/restore support (CONFIG_CHECKPOINT_RESTORE)
General setup -> open by fhandle syscalls (CONFIG_FHANDLE)
General setup -> Enable eventfd() system call (CONFIG_EVENTFD)
General setup -> Enable eventpoll support (CONFIG_EPOLL)
File systems -> Inotify support for userspace (CONFIG_INOTIFY_USER)
Executable file formats -> Emulations -> IA32 Emulation (CONFIG_IA32_EMULATION)
Networking support -> Networking options -> Unix domain sockets -> UNIX: socket monitoring interface (CONFIG_UNIX_DIAG)
Networking support -> Networking options -> TCP/IP networking -> INET: socket monitoring interface (CONFIG_INET_DIAG)
Networking support -> Networking options -> Packet socket -> Packet: sockets monitoring interface (CONFIG_PACKET_DIAG)
Собственно, почти все эти опции являются стандартными, и обычно выключенной оказывается только первая из них (становится доступна после включения опции Configure standard kernel features (expert users)). Чтобы проверить, так ли это, можно воспользоваться следующей командой:
$ zcat /proc/config.gz | grep CONFIG_CHECKPOINT_RESTORE
CONFIG_CHECKPOINT_RESTORE is not set
В данном случае по выводу команды можно заметить, что опция не включена, поэтому ядро придется пересобирать. Сразу скажу, что в данный момент CRIU работает только на системах x86_64, поэтому если ты юзаешь i386-сборку дистрибутива, то ничего не получится.
Теперь, когда есть ядро с поддержкой CRIU, нам понадобится утилита crtools, которая как раз и занимается заморозкой/разморозкой процессов. В дистрибутивах ее тоже нет, поэтому придется опять же собрать из исходников (вместе с пакетом protobuf-c, позволяющим работать с форматом Google’s Protocol Buffers, в котором crtools сохраняет состояние сетевых соединений):
$ cd /tmp
$ wget http://bit.ly/KGCeEq
$ tar -xzf protobuf-c-0.15.tar.gz
$ cd protobuf-c-0.15
$ ./configure --prefix=/usr && make
$ sudo make install
$ cd /tmp
$ wget http://bit.ly/WjDLlc
$ tar -xjf crtools-0.3.tar.bz2
$ cd crtools-0.3
$ make
$ cp crtools ~/bin
$ export PATH=~/bin:$PATH
Для корректной работы crtools также необходим пакет iproute2 версии не ниже 2.6.0. Его можно установить стандартными средствами дистрибутива. После установки можно проверить работоспособность crtools, запустив следующую команду:
$ sudo crtools check
Если сообщений об ошибке на экран выведено не будет, значит, все ОK.
Криокамера
Как же теперь заморозить процесс? Очень просто, будет достаточно следующей команды:
$ sudo crtools -D каталог dump -t PID-процесса
Каталог следует создать заранее, после выполнения команды он заполнится файлами с расширением img, в которых будет сохранена вся информация о процессе. Для восстановления достаточно выполнить обратную команду:
$ sudo crtools -D каталог restore -t PID-процесса
И процесс вновь появится в системе, причем, если это консольное приложение, оно будет запущено в том же окне терминала. Разработчики говорят, что таким образом можно замораживать make и GCC, tar, bz2, sendmail, Apache, MySQL, SSH, crond, VNC-сервер, nginx и многие другие приложения. Лично мне удалось без всяких проблем выполнить заморозку mc, mocp, Vim, однако в случае с графическими приложениями положиться на софт получится не всегда, например, Google Chrome мне восстановить так и не удалось.
Стоит заметить, что crtools сохраняет все дерево процессов, воспринимая переданный ему PID как родительский. Эту особенность можно использовать не только для многонитевых приложений, но и, например, для сохранения контейнеров LXC и OpenVZ. Разработчики рекомендуют использовать для этого следующую команду:
$ sudo crtools dump --tcp-established -n net -n mnt -n ipc -n pid --action-script "net-script.sh" -D dump/ -o dump.log -t init-PID
В данном случае аргумент --tcp-established заставляет crtools сохранить также и состояния сетевых соединений, чтобы их можно было восстановить после разморозки. Опции -n net -n mnt -n ipc -n pid позволяют корректно сохранить информацию о пространствах имен сети, точках монтирования, IPC и процессов. Здесь они необходимы, так как контейнеры выполняются в изолированных пространствах имен, и без них процессы удастся восстановить только в корневую систему. С помощью опции --action-script "net-script.sh" мы указываем команде исполнить скрипт net-script.sh перед заморозкой. Он заблокирует любые сетевые коммуникации на время заморозки. Этот скрипт ты найдешь на прилагаемом к журналу диске. Опция -o dump.log сохраняет лог заморозки в файл dump.log. В конце мы указываем PID процесса init внутри виртуального окружения.
Для разморозки контейнера используется схожая команда:
$ sudo crtools restore --tcp-established -n net -n mnt -n ipc -n pid --action-script "net-script.sh" --veth-pair eth0=интерфейс --root каталог-контейнера -D data/ -o restore.log -t init-PID
В этот раз задействованы две дополнительные опции. Это «--veth-pair eth0=интерфейс», в которой следует указать имя виртуального veth-интерфейса на стороне хост-системы, а также «--root каталог-контейнера» для указания корневого каталога контейнера. Таким образом можно без каких-либо проблем сохранить контейнер на одной машине, затем перекинуть его дамп на другую и восстановить его на ней.
Еще одно неожиданное применение crtools — это возможность внедрять паразитный код в работающие приложения, заставляя их исполнять указанные нами системные вызовы. Среди применений этой технологии разработчики приводят в пример возможность переопределить стандартный поток ввода-вывода:
$ sudo crtools exec -t PID close 1
$ sudo crtools exec -t PID open '&путь-до-файла' 2
Впрочем, ту же операцию можно проделать и с помощью отладчика GDB.
Продакшн?
Несмотря на молодость и недоработанность проекта, CRIU уже используется в последней версии облачной платформы Parallels Cloud Server. Кроме самого CRIU, в ней также задействованы такие технологии, как kexec и pramfs. Используемые совместно, они способны на порядок сократить время простоя сервера при обновлении в сравнении с классической холодной перезагрузкой.
Работает это примерно так. Сначала состояние виртуальных серверов сохраняется с помощью CRIU в виртуальную ФС pramfs (представляет собой аналог tmpfs, главная особенность — ее состояние сохраняется между перезагрузкой ядра через kexec), далее новое ядро загружается с помощью kexec, и состояние контейнеров восстанавливается из pramfs.
Ключевое значение здесь имеют CRIU и pramfs, потому что, со слов разработчиков, сам дамп состояния происходит очень быстро и обычно все упирается в производительность диска, тогда как в pramfs сохранение происходит со скоростью работы оперативной памяти, что дает заметный выигрыш в скорости дампа и восстановления.
WWW
- Детальный обзор механизма TCP repair mode: lwn.net/Articles/495304;
- список ядерных коммитов CRIU, включенных и еще не включенных в ядро.
WARNING
В данный момент CRIU работает только на системах x86_64.
Выводы
CRIU до сих пор находится в активной стадии разработки, но уже сейчас он позволяет замораживать многие приложения, в том числе целые контейнеры. Пока есть некоторые проблемы с графическими приложениями, однако, надо полагать, их вскоре исправят, учитывая опыт и способности программистов из Parallels.