Загружаем научный разбор
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Автор: Казачкин Даниил Михайлович · Обновлено
Историческое исследование IBM о Docker и KVM и современная методика собственного сравнения: эквивалентные условия, парные замеры, задержка и пропускная способность.
Команда собирается перенести сервис в контейнеры и находит график, где Docker быстрее виртуальной машины. Возникает соблазн сразу подставить процент в расчёт стоимости серверов. Но график мог измерять последовательную запись, а сервис ждёт сеть; виртуальная машина могла иметь другой путь к диску; данные относятся к версиям десятилетней давности.
Полезный научный разбор должен сначала установить, что именно сравнивали и какие переменные удерживали постоянными. Историческая работа IBM даёт хороший повод научиться читать такие эксперименты. Здесь мы не определяем победителя для современного оборудования. Мы строим проверяемую методику, по которой можно получить ответ для собственной нагрузки.
Felter, Ferreira, Rajamony и Rubio сравнили выполнение на обычном Linux, в Docker и в KVM. Работа рассматривает разные ресурсы: CPU, память, сеть и хранение, а также прикладные нагрузки. Основной вывод опубликованного исследования: в исследованных случаях контейнеры обычно показывали сопоставимую или лучшую производительность по сравнению с VM, при этом интенсивный ввод-вывод требовал настройки обоих вариантов. Конференционная публикация ISPASS 2015.
Доступен и более ранний авторский отчёт IBM RC25482 от 21 июля 2014 года. Его следует отличать от окончательной конференционной версии: совпадение названия не делает две редакции одним документом. При ссылке на конкретный эксперимент нужно указать использованную версию. Мы не переносим таблицы и численные результаты отчёта в оценку сегодняшних систем.
Возраст исследования не обесценивает постановку вопросов. Он ограничивает применимость конкретных измерений: ядро, драйверы, устройства, runtime и условия выполнения меняются. Фраза «Docker быстрее VM на столько-то процентов» без описания нагрузки и стенда не является результатом, который можно проверить.
Пусть наша команда обслуживает HTTP-запросы к небольшому каталогу. Пользовательская цель — поддерживать заданный поток при ограничении хвоста задержки и ошибок. Нужен ответ, сколько ресурсов требует каждый вариант для одинаковой полезной работы. Максимальная скорость одиночного арифметического цикла не заменяет такую проверку, хотя может помочь исследовать один из источников расходов.
Другой сервис может выполнять пакетное сжатие файлов. Для него важнее время завершения партии и стоимость обработки единицы данных. Третьему нужна предсказуемая синхронная запись. У этих сценариев разные критерии. Поэтому не стоит сводить все тесты в один средний «балл производительности», пока не определено, какую долю реальной работы представляет каждый тест.
Для нашего HTTP-сервиса запишем одинаковые входные данные, версии приложения, число клиентских соединений, последовательность запросов и условие корректного ответа. Отдельно закрепим лимиты CPU и памяти. Если один вариант свободно использует весь хост, а другой ограничен двумя CPU, сравнение измерит сразу две причины и не позволит отделить накладные расходы среды от ресурсов.
Во время длительного опыта хост может менять частоту CPU, конкурировать за диск или прогревать кеш. Если сначала измерить один вариант, а затем другой, изменение условий смешается с эффектом среды. Один способ уменьшить эту проблему — проводить короткие блоки с обоими вариантами, варьировать порядок и сохранять парные результаты. Такой дизайн не устраняет все помехи, но делает некоторые из них видимыми.
Следующий Python-пример не запускает Docker или KVM. Он анализирует искусственные числа, в которых среда B на 10% медленнее A в каждом блоке, а базовое время хоста постепенно растёт. Мы также добавляем отдельный выброс. Код показывает арифметику и чувствительность сводных метрик; это не набор измерений авторов статьи.
from statistics import mean, median
from math import isclose
paired_ms = [(10, 11), (20, 22), (30, 33), (40, 44)]
ratios = [b / a for a, b in paired_ms]
assert all(isclose(ratio, 1.1) for ratio in ratios)
assert isclose(median(ratios), 1.1)
unpaired_early_a = [10, 20]
unpaired_late_b = [33, 44]
naive_ratio = mean(unpaired_late_b) / mean(unpaired_early_a)
assert naive_ratio > 2.5
with_outlier = ratios + [3.0]
assert isclose(median(with_outlier), 1.1)
assert mean(with_outlier) > 1.4
duration_ratio = 1.1
throughput_ratio = 1 / duration_ratio
assert isclose(throughput_ratio, 10 / 11)
print('paired median:', round(median(ratios), 3))
print('unpaired ratio:', round(naive_ratio, 3))
print('throughput change, %:', round((throughput_ratio - 1) * 100, 2))Вывод содержит 1.1, 2.567 и −9.09%. Парные отношения восстанавливают заложенные 10% увеличения длительности. Сопоставление раннего A с поздним B ошибочно приписывает среде гораздо больший эффект. Увеличение времени на 10% соответствует уменьшению скорости выполнения последовательной фиксированной работы примерно на 9,09%, а не на те же 10%.
Медиана в примере устойчива к одному большому значению, но это не разрешение удалять выброс. Если выброс отражает реальную задержку пользователя, он может быть самым важным наблюдением. Следует сохранить исходные данные и объяснить, что измеряет выбранная сводка. Для очень малого числа запусков нельзя честно обещать устойчивую оценку редкого хвоста распределения.
CPU-тест отвечает на вопрос о конкретном вычислении при конкретных привязках и ограничениях. Тест пропускной способности памяти отвечает на другой вопрос. Приложение, которое в основном ожидает сеть, может почти не заметить разницы в первом тесте. Поэтому микробенчмарки полезны для объяснения причин, а прикладной опыт — для проверки значимости этих причин в продукте.
Для сети фиксируйте расположение клиента, путь пакета, режим адресации, размер сообщения и число соединений. Нельзя сравнивать локальный клиент в одном варианте с удалённым в другом и приписывать весь результат виртуализации. Отдельно убедитесь, что генератор нагрузки сам не упирается в CPU или сеть: иначе ровное плато скорости принадлежит клиенту, а не серверу.
Для диска укажите, куда именно пишет приложение: в слой образа, отдельный volume, файловую систему хоста или виртуальный диск гостя. Кеширование и требование подтверждённой записи могут изменить смысл измерения. Официальная документация Docker о драйверах хранения объясняет различие образных слоёв и записываемого слоя контейнера; фактический путь данных нужно описать для вашего стенда.
Очень быстрый результат может означать, что тест измерил приём данных в память, а не завершённую устойчивую запись. Аналогично маленький набор входных файлов может целиком оказаться в кеше. Это не обязательно ошибка: кеш может соответствовать реальной нагрузке. Ошибка возникает, когда прогретый сценарий называют холодным или обещают результат для данных, которые в память не помещаются.
Представим, что вариант стал быстрее после отключения необходимого шифрования или синхронного подтверждения записи. Сравнение времени корректно описывает две конфигурации, но конфигурации решают разные задачи. В отчёте нужно сохранить функциональные требования: целостность, устойчивость данных, изоляцию и допустимое поведение при отказе. Производительность имеет смысл только среди вариантов, удовлетворяющих этим требованиям.
С другой стороны, не следует искусственно запрещать полезную настройку только одному варианту. Если цель — сравнить разумно настроенные системы, предоставьте обоим одинаковую возможность настройки и опишите затраты. Если цель — сравнить значения по умолчанию, не смешивайте её с первым вопросом. Оба исследования допустимы, но ведут к разным выводам для читателя.
Для повторяемости сохраните версии программ, образ по digest, конфигурацию гипервизора, модель CPU, ядро, размещение данных, команду запуска теста и начальное состояние. Сырые результаты важнее одного скриншота графика. При повторении на другой машине сначала проверьте корректность полезной работы, затем сравнивайте распределения времени и ресурсов.
Постройте план из трёх конфигураций: нативное выполнение, контейнер и VM. Для каждой опишите одинаковый объём полезной работы, ресурсные ограничения и критерий завершения. Предусмотрите прогрев, несколько блоков с разным порядком и сохранение ошибок. До запуска запишите, какое изменение вы считаете практически значимым: решение о миграции не должно зависеть от случайной сотой доли секунды.
В Python-примере добавьте второй набор пар, в котором преимущество меняет знак между нагрузками. Объясните, почему объединение всех времён в одно среднее может скрыть это различие. Затем посчитайте результат отдельно для каждого типа нагрузки и предложите веса, соответствующие вашему продукту. Откуда получены веса, должно быть так же прозрачно, как сами времена.
Продолжить можно в [треке Docker](/lessons/without-university/docker-containerization) и [уроке Linux о логах и journalctl](/lessons/without-university/linux-developer-foundations/linux-developer-23). После такого опыта исторический график становится отправной точкой для гипотезы, а не готовым прогнозом. Решение опирается на собственную проверку при понятных условиях и сохраняет границы, за которыми измерение ничего не утверждает.
Самостоятельный русскоязычный разбор ЯдроКода по указанным первичным источникам. Учебные модели, примеры и выводы редакции отделены от результатов исследований. Материал не является переводом или перепечаткой.
Текст, учебные данные и Python-примеры созданы для этой публикации. Чужие программные реализации, таблицы, схемы и иллюстрации не воспроизводятся.