В нашем грустном и грустном коронавирусном мире очень хочется смотреть на все таким же грустным взглядом.
Так вот, так случилось, что к нам приходит с жалобой очень важный клиент. А он говорит: «У тебя молоко кончилось! Ваши снимки виртуальных машин работают очень медленно и печально».
В этом посте мы рассказываем, как наша команда справилась с этой проблемой.
На первый взгляд примерно так и должно быть.
Технологии Open Source — это, конечно, хорошо, но есть и печальные моменты.
Снимки виртуальных машин в QEMU на данный момент полностью синхронны.
Это означает, что все время сохранения состояния и памяти машины гость остается полностью остановленным.
Это (почти) правда.
Есть специальный вариант, когда сохранение снапшота работает как итеративная миграция, то есть мы начинаем экономить память на работающей виртуальной машине: проходим все страницы и сохраняем их на диск.
После этого проходим его второй раз и сохраняем только измененные страницы и т. д. Делаем это до тех пор, пока измененных страниц не останется относительно мало.
Дальше идет точно такой же синхронный процесс.
Но этот вариант нас не устроил, так как состояние записывается не в момент отправки команды на создание снапшота, а в произвольный (абсолютно непредсказуемый) момент времени.
Разговоры о нормальной реализации процесса миграции, когда мы записываем состояние виртуальной машины на момент вызова команды и ставим защиту от записи на всю память виртуальной машины, по состоянию на весну-лето этого года в основной поток разработки QEMU зависал из-за установки защиты от записи с уведомлением.
Это было невозможно из-за ошибки пользователя fd. Эта функциональность еще не вошла в ядро Linux. Наши попытки реализовать эту функцию через специальный выход из пользовательского пространства KVM были отвергнуты сообществом 2 года назад. Как уже говорилось выше, с OpenSource разработкой не все так хорошо.
Сейчас этот функционал появился в ядре и процесс разработки возобновился, но для работы в боевом режиме пока ничего не готово.
ХОРОШО.
Примерно это мы и планировали написать клиенту.
Но не тут-то было.
Наши инженеры поддержки поймали нас за руку с этим письмом.
Наше распределенное хранилище работало на машине клиента и в тестах выдавало производительность в районе 120-200 Мб/сек при записи с одного хоста.
А вот время сохранения снапшота для ВМ объемом 8 ГБ составило около 300 секунд, что давало всего 27 Мбит/сек на запись.
Оказалось, что здесь что-то не так, и с этой проблемой пришлось бороться даже с учетом кривой архитектуры.
Нам нужно это выяснить, нам нужно это выяснить.
QEMU хорошо оснащен.
В коде достаточно много разного рода точек трассировки, каждую из которых можно включить независимо от других.
Трассировка записывается в журнал виртуальной машины, который обычно находится в /var/log/libvirt/qemu/vm-name.log. В целом, если говорить об отладке QEMU, то стоит вспомнить идеологию связки QEMU/libvirtd. Это действительно важно.
Сам QEMU не содержит никакой логики, связанной с организацией той или иной операции.
Все управляется извне через стандартизированный интерфейс.
На самом деле таких интерфейсов два — HMP (протокол человеческого монитора) и QMP (машинный протокол QEMU).
QMP является более полным.
Все команды HMP присутствуют в QMP. Обратное уже неверно.
Более того, есть стандартная возможность отправить на этот интерфейс произвольную команду через «вирш».
Стандартное описание протокола обычно поставляется вместе с самим QEMU и любой желающий может его прочитать по адресу https://github.com/qemu/qemu/blob/master/docs/interop/qmp-spec.txt .virsh qemu-monitor-command VM-name [--hmp] <command>
Запросы отправляются в формате JSON и полностью указаны в https://github.com/qemu/qemu/blob/master/qapi/ , Например https://github.com/qemu/qemu/blob/master/qapi/block.json .
На основе этих описаний при сборке проекта автоматически генерируется код парсера и проектная документация.
В этом случае нам не нужны сложные команды; просто используйте следующую простую команду: # virsh qemu-monitor-command VM --hmp trace-event qcow2\* on
Начать создание снимка # virsh snapshot-create VM
И начинаем смотреть лог qcow2_snapshot_create_finish bs 0x55c1e47ca000 id 1
qcow2_writev_start_req co 0x55c1e47adb80 offset 0x788346000 bytes 20480
qcow2_writev_start_part co 0x55c1e47adb80
qcow2_alloc_clusters_offset co 0x55c1e47adb80 offset 0x788346000 bytes 20480
qcow2_handle_copied co 0x55c1e47adb80 guest_offset 0x788346000 host_offset 0x0 bytes 0x5000
qcow2_l2_allocate bs 0x55c1e47ca000 l1_index 0
qcow2_cache_get co 0x55c1e47adb80 is_l2_cache 0 offset 0x200000 read_from_disk 1
qcow2_cache_get_done co 0x55c1e47adb80 is_l2_cache 0 index 0
qcow2_cache_get co 0x55c1e47adb80 is_l2_cache 0 offset 0x200000 read_from_disk 1
qcow2_cache_get_done co 0x55c1e47adb80 is_l2_cache 0 index 0
qcow2_cache_flush co 0x55c1e47adb80 is_l2_cache 0
qcow2_cache_entry_flush co 0x55c1e47adb80 is_l2_cache 0 index 0
qcow2_cache_flush co 0x55c1e47adb80 is_l2_cache 1
qcow2_cache_entry_flush co 0x55c1e47adb80 is_l2_cache 1 index 0
qcow2_l2_allocate_get_empty bs 0x55c1e47ca000 l1_index 0
Что нам интересно в этом логе (после отсеивания неважного):
На первый взгляд все выглядит хорошо.
Запись последовательная, блоки сравнительно большие.
Но все равно почему-то медленно работает. Нам нужно копать глубже.
Давайте возьмем в руки blktrace и посмотрим, как он выглядит с точки зрения ядра.
# blktrace -d /dev/sda -o - | blkparse -i -
Сейчас в системе ничего не работает, поэтому особо усердствовать с фильтрацией результата не нужно.
Результат выглядит примерно так
и эта картина уже выглядит очень грустно и всё объясняет.
Итак, что мы видим? Мы видим, что каждому запросу на запись предшествуют два запроса на чтение размером в один сектор.
При этом существует строгий порядок — 2 запроса на чтение, 1 запрос на запись.
Никакого параллелизма нет. Пока чтение не завершено, ничего не пишется.
Пока запись не закончится, нового прочтения тоже нет. Для дисковых операций такая нагрузка в принципе не может приблизиться к пределу пропускной способности накопителя ни при каких разумных условиях.
Почему это случилось? Вообще говоря, у этой структуры операций даже есть специальное название: «чтение-изменение-запись».
Как правило, это наблюдается при выполнении операций, не выровненных по странице.
Более того, в нашем случае файловый дескриптор, использовавший QEMU для операций записи, был открыт в режиме O_DIRECT. Это означает, что невыровненные операции запрещены, и в коде программы мы должны прочитать те самые маленькие кусочки перед основной операцией записи.
Итак, режим O_DIRECT — это зло, и стоит ли от него избавляться? Вопрос на самом деле не так прост. Как выглядит структура любой операции ввода-вывода с точки зрения QEMU? Гость отправляет запрос контроллеру диска.
Этот запрос должен быть преобразован в операцию чтения/записи/удаления файла, расположенного где-то в файловой системе хоста.
И пока этот запрос выполняется, было бы неплохо обработать еще какие-то запросы к диску, находящиеся в очереди контроллера.
Таким образом, вам необходимо иметь возможность реализовывать асинхронные операции ввода-вывода.
Существует всего два основных варианта реализации асинхронных операций ввода-вывода в Linux:
- libaio (через io_submit с уведомлением о завершении через eventfd)
- preadv/pwritev работает в каком-то пуле потоков
- глубина очереди запросов ввода-вывода контроллера равна количеству потоков в пуле
- каждый запрос требует переключения контекста на контекст потока, что приводит к дополнительным задержкам
Итак, что лучше использовать? Очевидный ответ: «Все, что работает быстрее».
Запустив FIO в госте (размер запроса 4к, глубина очереди 128) получаем простой ответ. C O_DIRECT быстрее.
Ну и некоторый бонус в том, что нам не нужна память на уровне ядра хоста для эффективной записи или чтения.
В случае перераспределения памяти это, очевидно, будет работать лучше.
Дополнительно стоит отметить еще одну особенность, которая хорошо видна из исходной трассировки уровня QEMU. Все записи данных происходят в QCOW2, и эти записи переходят в новые блоки.
У таких операций есть приятная особенность.
Новый блок изображения всегда выделяется целиком.
Частичный выбор невозможен.
Это значит, что если данных для всего блока недостаточно, для оставшегося куска вызывается Fallocate(FALLOC_FL_ZERO_RANGE), и при выполнении следующей операции мы получаем дополнительное обновление метаданных в файловой системе хоста.
Операции выполняются быстрее всего с данными, которые соответствуют размеру кластера.
Почему все работает так неэффективно? Ответ на самом деле довольно прост. Весь код создания моментальных снимков вырос из кода миграции.
А миграция — это сокет для отправки данных на другой компьютер или в другой процесс.
Оказывается, никто даже не думал об оптимизации записи на диск.
Пришло время наконец заняться проблемой.
Для этого вам не нужно ничего делать
- согласовать записываемые данные с размером кластера
- организовать очередь записанных кластеров некоторой разумной глубины
https://lists.gnu.org/archive/html/qemu-devel/2019-08/msg03152.html .
aio_task_pool, чтобы помочь нам.
Мы взяли размер очереди в 8 запросов по 1 Мб и сразу получили отличный результат (время создания снимка, в секундах).
Операция выполняется быстрее во всех режимах.
В том числе и в стандартном режиме кэширования из-за того, что теперь все операции привязаны к блокам изображений.
Теперь пришло время обсудить бонус.
Совершенно очевидно, что переход на снапшот устроен абсолютно таким же неэффективным образом.
Нам нужно сделать примерно то же самое.
Но это также не работает. На SSD/NVME с очередью все работает нормально, а вот на HDD это вызывает падение производительности примерно на 30%.
И тут мы были поражены.
Операция перехода на снапшот на NVME происходит медленнее, чем на HDD. Объяснить этот факт мы могли только наличием очереди в стандартном вращающемся диске и наличием самого диска упреждающего чтения, который при последовательном чтении хорошо угадывает, какие данные и когда нужно прочитать.
Это означает, что нам нужно изменить наш подход. Прочитаем данные последовательно: один запрос в очереди с кэшем тех же 10 МБ и новый запрос, начинающийся с конца предыдущего.
Это уже начинает работать правильно и дает отличные результаты.
Такие результаты были представлены на KVM Forum 2020. Патчи были отправлены в мейнстрим в июне и до сих пор не приняты.
Но мы не теряем надежды, что Кевин Вольф и Макс Рейтц все же вдохновятся необходимостью взять этот код в основную ветку.
Теги: #разработка Linux #с открытым исходным кодом #QEMU #снимок
-
Номер Службы Поддержки Клиентов Windows 10
19 Oct, 24 -
Прототипирование Asic На Fpga
19 Oct, 24 -
Робот-Канатоходец
19 Oct, 24 -
Выполнение Воли Сатоши Накамото
19 Oct, 24 -
Занимательная Физика
19 Oct, 24 -
Зам-С - Выпуск №51
19 Oct, 24 -
Пятьдесят Оттенков Инфракрасного
19 Oct, 24