В преддверии очередного релиза CRIU

от автора

Сегодня я хочу продолжить серию статей о проекте CRIU (Checkpoint/restore mostly in the userspace). Проекту чуть более года, а по возможностям он уже в плотную приблизился к подобной функциональности в OpenVZ.
Первая часть статьи расскажет о новой функциональности, которая появилась в CRIU за последние несколько месяцев. Вторая часть расскажет о нашем опыте внедрения новых технологий для улучшения процесса разработки.

Новая функциональность

Снапшот памяти и итеративная миграция

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

Основная проблема была в реализации механизма, который позволит отследить какие регионы памяти изменились с предыдущего раза. Работает он достаточно просто. Есть возможность пометить все существующие страницы процесса как «чистые». В этом случае ядро запрещает запись в них и если кто-то попытается записать в такую страницу, будет вызвано исключение (pagefault). В результате ядро вернёт странице права на запись и пометит её, как «грязную» (PME_SOFT_DIRTY). Все свойства страницы можно прочитать из файла /proc/PID/pagemap pagemap2. Да, пришлось создать вторую версию файла, так как в формате первого кончились биты, причём часть из них, на самом деле, всегда нули. Именно их и удалось высвободить во второй версии.

Миграция без использования диска

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

Особенность здесь в том, что мы попытались по максимуму избежать лишнего копирования памяти. Ядро представляет ряд системных вызовов для этих целей (vmsplic, splice, etc). К примеру, память процесса можно послать через пайп (pipe), не сделав ни одного копирования.

Сигналы

Казалось бы, сохранить и восстановить сигналы не большая задача, но растянулась она не на один месяц. Если для сохранения или восстановления каких-либо объектов нет соответствующих интерфейсов, то мы стараемся придумать новый интерфейс, который будет полезен не только для CRIU. У этого подхода есть и сторонники, равно как и противники.

Так же было и в этот раз. Можно было добавить ещё одну команду к ptrace (интерфейс, который используют отладчики), но казалось, что его применение слишком узкое, в то время как в Linux уже давно существовал системный вызов signalfd, который делал что-то очень похожее. С началом реализации, начали всплывать проблемы. В обычной жизни, процессы не знают из какой очереди они получают сигнал (очередей две, одна на процесс целиком и по одной на каждый тред), но в случае CRIU — это важно. Мы не можем восстановить сигналы одного треда другому. Так же хотелось подсматривать сигналы, но не забирать их из очереди, потому что процесс дампа не должен быть деструктивным. Обе эти проблемы не выглядели серьёзными, что-то подобное мы уже проделывали над другими объектами. Третья проблема заключалась в том, что ни один из форматов представления siginfo в пользовательское пространство не давал полной информации, достаточной для восстановления. До нас в ядре было два формата. Первый — это то, что мы получаем в обработчике сигналов, и он наиболее приближен к тому, что хранится в ядре, за одним исключением — в нем отсутствует тип объекта. Ядро просто обрезает тип перед тем, как передать его в пространство пользователя. Второй формат — это то что возвращает signalfd. В нем по косвенным признакам можно определить тип сообщения, но в некоторых случаях часть информации просто отсутствует. Решение этой проблемы очевидно — нужен третий формат, который будет возвращать siginfo в том же виде, в котором его хранит ядро.

Четвёртая ключевая проблема заключается в том, что дескриптор signalfd не привязывается к конкретному процессу, в то время как семантика файлового дескриптора предполагает, что после fork() он будет указывать на тот же объект, что и до форка. Это противоречие было добавлено первыми разработчиками signalfd и, для сохранения обратной совместимости, не может быть изменено. Для текущего интерфейса это противоречие ни на что не влияет, но вот когда мы решили расширять возможности signalfd, то начались проблемы. Всем хотелось сделать работу с дескриптор signalfd, максимально похоже на работу с другими дескрипторами, но из-за вышеописанного противоречия найти разумное решение так и не удалось. Я предпринял порядка 5-7 попыток и в результате пришлось сделать первоначальный вариант с ptrace-ом.

Конвертер из OpenVZ имиджей

В предыдущих статьях уже говорилось, что CRIU должен заменить существующий механизм чекпоинтинга в OpenVZ и конечно обратная совместимость должна быть сохранена. Скажу по секрету, что команда OpenVZ в данный момент работает над новой стабильной версией ядра. Как обычно, оно будет базироваться на ядре из следующей версии RHEL (7). Вместе с этим процессом ведётся разработка конвертера из OpenVZ образов в образы CRIU. Эта задача не столько сложная, сколько трудоёмкая.

Netlink сокеты, миграция TCP соединений, конвертация vdso

Так же в данный момент ведётся работа над восстановление vDSO библиотеки. Основная сложность в том, что библиотека предоставляется ядром, и механизм ее взаимодействия с ядром не фиксирован. Эта библиотека загружается динамически, так что и адреса функций могут изменяться от ядра к ядру. Мы решили, что проще всего будет создать прокси, которая будет выглядеть как старая библиотека, но вызывать функции из новой. Даже на этом пути не все гладко. К примеру, процесс мог зайти в код библиотеки и быть прерван обработкой сигнала. В обработчике сигнала процесс может делать что угодно и понять как он туда попал в общем случае невозможно. В LKML даже мелькнула мысль сделать “стабильную” библиотеку vdso, код которой не будет меняться. Пока мы не придумаем решение лучше, будем жить с прокси и надеяться что в коде vdso процесс не прервут.

До недавнего времени все TCP соединения использовали глобальный счетчик для выставления временных меток на пакеты (TCP timestamp). Счетчик этот сбрасывается при каждой перезагрузке. Эта схема мешала миграции TCP соединений с одной машины на другую. В следующем ядре Linux появится возможность выставлять смещение для каждого сокета в отдельности, что позволит CRIU мигрировать соединения.
Еще одной не большой фичей стала поддержка Netlink сокетов. Текущий интерфейс представляемый файлом /proc/net/netlink не дает достаточной информации, поэтому пришлось расширить socket diag для netlink сокетов.

Технологии

Непрерывная интеграция (англ. Continuous Integration)

Процесс разработки CRIU строится по образу и подобию разработки ядра Linux. Основные принципы, которым мы следуем:

  • Один коммит — одно логическое изменение. Каждый коммит — это одна законченная мысль. Такой принцип существенно облегчает процесс проверки (review) и повышает его качество.
  • Любой коммит не должен ломать билд, т е в любой точке проект должен компилироваться и все тесты должны проходить. Этот принцип помогает находить привнесённые проблемы (бисектить).

Первый принцип контролируется человеком, который просматривает изменения и заносит их в основной репозиторий. Второй принцип можно проверить только экспериментально. Обычно этот процесс называют «continuous integration», для автоматизации которого существует несколько решений. Мы выбрали популярный на сегодняшний день проект Jenkins. Установка и настройка не отнимает много времени. По результатам прошедших двух месяцев, мы можем смело сказать, что усилия были потрачены не зря. Не, билд мы ломаем крайне редко, но из-за большого количества прогонов юнит тестов, он поймал несколько багов, связанных с гонками ресурсов или стечением каких-то обстоятельств.

Технология будет работать по полной, когда мы начнем запускать все тесты (не только юнит), во всех вариациях (проверка обратной совместимости, проверка на не деструктивность дампа), на всех конфигурациях (две архитектуры, две версии ядра). Эта та работа, которую разработчику проделывать самому накладно.

Статические анализаторы кода

К статическим анализаторам кода я всегда относился скептически, по какой-то причине было мнение, что пользы они принесут не много, а вот время утилизировать помогут. Иногда в процессе работы случаются такие моменты, когда делать ничего не хочется и надо отвлечься. Я в такие моменты люблю попробовать что-то новое. Именно так мы начали использовать Jenkins и именно так, наш код был прогнан через clang-analyzer. Нельзя сказать, что мы получили какие-то сверхъестественные результаты, но пару багов на путях обработки ошибок он указал.

Вдохновленный полученными результатами, другой разработчик зарегистрировал наш проект на scan.coverity.com/. Здесь движок несколько мощнее, чем у clang-analyzer-а, но и количество «ложных срабатываний» выше. Моё мнение о статических анализаторах такое: пользу они приносят, но приоритет их не сильно высокий. Если ваш проект хорошо покрыт тестами и багов они не находят, то можно и на статические анализаторы время потратить.

Ссылки

lwn.net/Articles/546966/
lwn.net/Articles/531939/
habrahabr.ru/post/152903/
habrahabr.ru/post/148413/
jenkins-ci.org/
ru.wikipedia.org/wiki/CRIU
criu.org

ссылка на оригинал статьи http://habrahabr.ru/post/177499/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *