Недавно я опубликовал статью OrbStack: Почему я забыл про Docker Desktop, которая вызвала оживленную дискуссию в комментариях. Основные вопросы возникли вокруг производительности различных Docker-подобных решений. Мои аргументы, основанные в первую очередь на личном опыте использования, оказались недостаточно убедительными.

Чтобы получить объективную картину и предоставить сообществу реальные данные, я решил разработать комплексный benchmark для сравнения различных решений. В процессе разработки тестов, комментаторы предложили несколько интересных идей, которые помогли расширить список тестируемых движков. В итоге в тестировании приняли участие:

  • Docker Desktop

  • Podman Desktop

  • Rancher Desktop

  • OrbStack

  • Colima

Конфигурация хоста:

  • OS: MacOS 15.0.1

  • Железо: MacBook Pro 16

  • Процессор: M1 Max (10 ядер (8 производительности и 2 эффективности))

  • RAM: 64 GB

Будем измерять:

  • Время запуска docker (условно нажали на приложение, запускается и становится доступным для запуска контейнеров)

  • Время сборки тяжелых образов (Heavy build) - 2 разных образа

  • CPU и Memory

  • Энергопотребление (чуть ниже описал как считаю, что и для чего это)

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

> Если вы найдете в скриптах/логике ошибку, свяжитесь со мной любым удобным способом. Я доработаю и обновлю статью, если будет возможность, либо только скрипты, если редактирование не будет доступно.

Подготовка

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

Поэтому если хотите изучить скрипты, найти ошибки, предложить улучшения/доработки - заходите на Github. Кроме того, это гарантия того, что скрипты будут актуальными, так как они однозначно будут улучшаться.

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

Во-первых, я постарался написать скрипты таким образом, чтобы можно было как "Запустить все", так и, например, протестировать только runtime для Docker Desktop. У каждого скрипта есть команда --help, благодаря которой можно узнать набор атрибутов, все максимально гибко.

Во-вторых, хотелось бы пояснить расчеты по "Энергопотреблению" и для чего это нужно. Мне хотелось вычислить, сколько энергии тратит приложение, вдохновился бенчмарком OrbStack. Там написано следующее:

> After waiting for CPU usage to settle, we measured the long-term average power usage over 10 minutes by sampling the process group's estimated energy usage (in nJ) from the macOS kernel before and after the test, and converting it to power usage (in mW) using the elapsed time.

Если перевести упрощенно - дождались, пока нагрузка CPU будет стабильной, измерили среднее энергопотребление за период, выбрав расчетное энергопотребление группы процессов и преобразовали его в энергопотребление. Просто мерить, сколько осталось батареи после запуска, я посчитал совсем не точной метрикой.

Самый близкий аналог этого - колонка энерговоздействия в Mac OS

Мониторинг системы
Мониторинг системы

У меня были написаны все скрипты, потому что в целом легко засечь время запуска, посчитать CPU/RAM, замерить время сборки, а вот с энергопотреблением появилось так много сложностей, что вместо расчетных 4-х часов на это занятие, потратил несколько дней. После огромного количества итераций и гугления нашел статью.

И с командой:

top -stats pid,command,power -o power -l 0 | grep 'Docker Desktop'

У меня даже стали появляться результаты:

21930  Docker Desktop   0.4
43853  Docker Desktop H 0.0
21955  Docker Desktop H 0.0
21953  Docker Desktop H 0.0
43853  Docker Desktop H 1.1
21930  Docker Desktop   0.3
21953  Docker Desktop H 0.1

Но эти значения были сильно нереалистичны, потому что просто с запущенными базами, полдня использования компьютера убивают батарею полностью, а тут просто какие-то микродоли. И было непонятно - мне их суммировать или выводить среднее (равное нулю, естественно). Я еще помучился и решил вернуться к парсингу команды:

powermetrics -i 1000 --poweravg 1 | grep 'Average cumulatively decayed power score' -A 20

Переписал скрипт с bash на python, чтобы проще было все парсить. Но, честно говоря, до сих пор не уверен, что верно считаю эти данные. Первый блин комом, но хотя бы работает, получил такие цифры:

Начальная мощность: 1035.0mW
Конечная мощность: 1295.0mW
Средняя мощность: 1307.7mW

Потом добавил обработку, чтобы вычислить, сколько именно наш процесс потребляет - общую нагрузку CPU, из нее CPU наших процессов и на основе этого вычисляю долю от общей загрузки. В мониторинге параллельно смотрел, что доля CPU была от 16 до 20%, поэтому после доработки результат вышел вроде реалистичный:

Начальная мощность: 7.2mW
Конечная мощность: 706.5mW
Средняя мощность: 149.1mW

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

Dockerfiles

Сборку хотелось бы провести на действительно тяжелых образах, но без фанатизма - 10-15 минут на сборку нормально, а вот по часу точно не хотелось бы ждать, особенно в рамках отладки скрипта.

Поэтому в первую очередь я сделал простой сервис для отладки bash скрипта.

# test-builds/simple/Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN echo '{"name":"test","version":"1.0.0","dependencies":{"express":"^4.18.2"}}' > package.json

RUN npm install

RUN mkdir src
COPY . .

RUN npm install -g typescript && \
    echo '{"compilerOptions":{"target":"es6","outDir":"dist"}}' > tsconfig.json && \
    mkdir -p src && \
    echo 'const greeting: string = "Hello World"; console.log(greeting);' > src/main.ts && \
    tsc

CMD ["node", "dist/main.js"]

Придумать или скачать тяжелый образ оказалось сложнее. OrbStack в своих benchmarks тестирует на PostHog и Open edX, но у меня нормально их собрать вообще не получилось. Я решил сделать свои. Второй сервис - Java Spring приложение с множеством зависимостей:

# test-builds/java/Dockerfile
# Multi-stage build for Spring Boot application
FROM maven:3.8.4-openjdk-17 AS builder

WORKDIR /app

RUN mkdir -p src/main/java/com/example/demo

# Copy configuration files
COPY pom.xml .
COPY DemoApplication.java src/main/java/com/example/demo/

# Download dependencies and build
RUN mvn dependency:go-offline
RUN mvn package -DskipTests

# Final stage
FROM openjdk:17-slim
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

Нужно еще два файла создать, первый pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.0</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo Spring Boot Application</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

второй файл DemoApplication.java:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Третий сервис - Python ML приложение с TensorFlow:

# test-builds/ml/Dockerfile
FROM python:3.9 as builder

WORKDIR /app

# Install system dependencies
RUN apt-get update &amp;&amp; apt-get install -y \
    build-essential \
    curl \
    software-properties-common \
    git \
    &amp;&amp; rm -rf /var/lib/apt/lists/*

# Create requirements.txt
RUN echo 'tensorflow==2.13.0' &gt; requirements.txt &amp;&amp; \
    echo 'torch==2.0.1' &gt;&gt; requirements.txt &amp;&amp; \
    echo 'transformers==4.31.0' &gt;&gt; requirements.txt &amp;&amp; \
    echo 'scipy==1.11.2' &gt;&gt; requirements.txt &amp;&amp; \
    echo 'scikit-learn==1.3.0' &gt;&gt; requirements.txt &amp;&amp; \
    echo 'pandas==2.0.3' &gt;&gt; requirements.txt &amp;&amp; \
    echo 'numpy==1.24.3' &gt;&gt; requirements.txt &amp;&amp; \
    echo 'matplotlib==3.7.2' &gt;&gt; requirements.txt &amp;&amp; \
    echo 'seaborn==0.12.2' &gt;&gt; requirements.txt

# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Create sample ML application
COPY . .
RUN echo 'import tensorflow as tf' &gt; app.py &amp;&amp; \
    echo 'import torch' &gt;&gt; app.py &amp;&amp; \
    echo 'import transformers' &gt;&gt; app.py &amp;&amp; \
    echo 'from sklearn.ensemble import RandomForestClassifier' &gt;&gt; app.py &amp;&amp; \
    echo 'import numpy as np' &gt;&gt; app.py &amp;&amp; \
    echo 'import pandas as pd' &gt;&gt; app.py &amp;&amp; \
    echo 'print("TensorFlow version:", tf.__version__)' &gt;&gt; app.py &amp;&amp; \
    echo 'print("PyTorch version:", torch.__version__)' &gt;&gt; app.py &amp;&amp; \
    echo 'print("Transformers version:", transformers.__version__)' &gt;&gt; app.py

FROM python:3.9-slim

WORKDIR /app

# Copy only necessary files from builder
COPY --from=builder /app/app.py .
COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages

CMD ["python", "app.py"]

Теперь ничего не мешает запустить тестирование.

Тестирование

Для запуска тестирования достаточно запустить скрипт:

./engine-benchmark.sh -v all #Запуск тестирования всех движков

# Или запустить только нужный
./engine-benchmark.sh -v podman-desktop

Результаты складываются по каждому тесту, и общий результирующий сохраняется в result. Например docker-desktop_idle_resources.json:

{
    "engine": "docker-desktop",
    "timestamp": "2024-10-28T21:49:35.419319Z",
    "repeat_count": 3,
    "results": {
        "average": 10.817972977956137,
        "min": 3.1761507987976074,
        "max": 25.995931148529053,
        "all_times": [
            25.995931148529053,
            3.281836986541748,
            3.1761507987976074
        ]
    }
}

Во время работы приложения требуется периодически вводить пароль - для установки и удаления, а также для запуска последнего теста, так как powermetrics может работать только с правами sudo. Также нужно периодически реагировать на окна, так как время от времени требуется разрешение на запуск, например, для Podman Desktop. Полностью автоматизировать процесс, чтобы запустить на два часа без вмешательства, не удалось, но и так в целом неплохо, улучшить можно позже.

Результаты

Чтобы удобно работать с результатами, написал простой скрипт. Что бы посмотреть - достаточно зайти в ./graphics и открыть index.html

Для того чтобы быть уверенным, что один движок не повлияет на другой - сначала устанавливается первый движок, прогоняется весь набор тестов, удаляется и только потом происходит переход к следующему.

Все результаты тестов и логи загружены на github, вы можете зайти в папку graphics и запустить index.html.

Время запуска

Проверяется 4 раза. При этом на первом запуске везде есть нюансы. Например, у Docker после проверки надо запустить вручную первый раз, непонятно почему - он запускает его, но приложение не стартует. Остальные приложения тоже в первый раз открываются долго. Поэтому решил считать среднее между тремя последними запусками.

Также при запуске я выполняю три команды, чтобы проверить, что все работает корректно:

if docker info &gt;/dev/null 2&gt;&amp;1 &amp;&amp; \  
   docker ps &gt;/dev/null 2&gt;&amp;1 &amp;&amp; \  
   docker run --rm hello-world &gt;/dev/null 2&gt;&amp;1; then  
    return 0  
fi

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

Еще отдельная история с Rancher - пофиксить так и не смог. Когда случается сбой, первый запуск надо делать вручную, не знаю почему, все последующие скрипт корректно запускает и останавливает.

[2024-10-29 22:40:45] Тестирование времени запуска rancher-desktop...
Тестирование rancher-desktop...
Попытка 1 из 4
Запуск rancher-desktop...

При этом он позволяет подряд запустить только два раза, на третий каждый раз выдает 0 (перезапускал несколько раз, пофиксить так и не смог). Но, к счастью, у него в интерфейсе есть информация о том, сколько стартует виртуальная машина - это, как правило, 30-40 секунд. Поэтому тут решил не добавлять, в любом случае цифры реалистичные.

Тестирование запуска. Комбинировано
Тестирование запуска. Комбинировано

То же самое, но в другом отображении:

Тестирование запуска. Разделено
Тестирование запуска. Разделено

Тут из интересного - при тестировании запуск Podman занял 0 секунд. После первого запуска действительно столько. Первый запуск был 5 секунд (тоже самый низкий из всех), 3 последующих - за 0. Я отдельно попробовал, запустил тест для него, получил в целом аналогичные результаты:

Тестирование podman-desktop...
Попытка 1 из 4
Запуск podman-desktop...
Первый запуск пропущен: 5.0 секунд
Останавливаем podman-desktop...
Попытка 2 из 4
Запуск podman-desktop...
Время запуска: 0 секунд
Останавливаем podman-desktop...
Попытка 3 из 4
Запуск podman-desktop...
Время запуска: 1.0 секунд
Останавливаем podman-desktop...
Попытка 4 из 4
Запуск podman-desktop...
Время запуска: 1.0 секунд
Останавливаем podman-desktop...
Результаты сохранены в results/startup/podman-desktop_startup.json
Тестирование завершено. Результаты в директории results/startup

Сборка

Тут из проблемного - это Colima. Она ругалась на docker-compose, отсутствие buildx (без него может работать, но ругается, что deprecated, поэтому добавил сборку через него) и docker. То есть помимо Colima нужно устанавливать и эти инструменты. Buildx решил не ставить, потому что потребовалось бы много дополнительной логики для тестирования, да и понятно, что через него будет быстрее. После всех доработок она начала просто отказываться работать, непонятно почему, но проснулся утром и все заработало. В общем, очень странная история, но потом все стало нормально функционировать. А затем опять начала ругаться на docker compose, пришлось добавить условия для Colima - запуск через docker-compose, а не docker compose. Это помогло.

Результат:

Результаты сборки
Результаты сборки

Производительность

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

СPU
СPU
RAM
RAM
Энергопотребление
Энергопотребление

Приятно удивил Docker Desktop по энергопотреблению. Либо я тест неправильно сделал, либо они сильно продвинулись с прошлого года, когда был сделан тест от OrbStack.

Итоги

Техническая сторона доставила мне много проблем, почти все очень деликатное, постоянно какие-то нюансы, хотя казалось бы, везде, кроме Podman, обратная совместимость по командам полная. Но я однозначно получил удовольствие от процесса еще и параллельно познакомился со всеми продуктами очень плотно.

В итоге реализация есть, она работает, но иногда надо помогать - вводить пароль, открывать приложение при первом запуске. Глобально можно позже допилить и сделать более удобным и стабильным (надеюсь, что сообщество поможет). Кроме того, можно расширить набор тестов, например, добавить I/O.

Самое важное, что инструмент дает данные, и на мой взгляд, достаточно реалистичные, и данный Benchmark позволит сделать выводы для себя. Я для себя решил остаться на OrbStack, но параллельно буду присматриваться к Podman Desktop. Что выберете вы - решать исключительно вам.

P.S. Когда вы будете воспроизводить тесты, учитывайте разность окружений. Я ожидаю, что цифры будут другими, но порядок останется одинаковым.

P.P.S. Для тех, кто пролистал, ссылка на Github

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


  1. somech
    30.10.2024 21:42

    Так или иначе, все равно решает удобство: кому-то подавай красивый гуй, кому-то просто автозапуск движка. MacOS не является средой для массового запуска «кровавого ентерпрайза». Особенно в широтах восточной Европы. Тут скорее вопрос DX

    Замер скорости запуска софта на маке вообще вызывает улыбку.


    1. Apokalepsis Автор
      30.10.2024 21:42

      Про удобство - полностью согласен. Часть параметром, которые тестировал - это в том числе про удобство. Если например одно приложение, при прочих равных позволит дольше работать без зарядки, это же классно?

      Скорость запуска в данном случае играет. После запуска Rancher например, мне не особо хочется им пользоваться, настолько долго открывать - это жесть. А я например постоянным не держу открытым Docker, только в момент разработки и мне очень удобно, когда это происходит почти мгновенно.

      Но в любом случае это синтетика и каждый действительно выберет для себя то что будет удобно именно ему.


  1. Tony-Sol
    30.10.2024 21:42

    В приведенном тесте colima не использует vz и rosetta, кажется с ними разрыв будет не столь драматичным.

    На самом деле в целом огромную роль играет удобство софта, ведь так или иначе это все сводится к тому "как запущенная виртуальная машина под капотом". Например я остановился на связке podman + podman desktop в качестве GUI (и podman-tui в качестве TUI) и docker поверх lima с podman desktop в качестве GUI (и lazydocker в качестве TUI), потому что для меня критична возможность конфигурации размещения всех файлов приложения.

    Так, например, docker desktop игнорирует переменную окружения DOCKER_CONFIG, а orbstack вообще не позволяет изменить пути для ~/.orbstack и ~/OrbStack , но с podman desktop это не проблема.

    Плюс podman desktop может слушать одновременно 2 сокета - как podman, так и docker, таким образом можно следить за 2 средами одновременно:

    При этом контексты docker'а могут быть любыми, главное чтобы был симлинк на /var/run/docker.sock (Надеюсь и это скоро перестанет быть обязательным благодаря развитию этой идеи в этот эпик)

    Пока что, для меня единственный минус podman desktop это отсутствие возможности управлять (или хотя бы просматривать) виртуальными машинами, созданными через lima, но это тоже можно будет реализовать.


    1. Apokalepsis Автор
      30.10.2024 21:42

      Под vs и Rosetta нужно как-то отдельно настраивать на сколько я понимаю?

      Если не секрет, с какой целью вам требуется конфигурирование путей?


      1. Tony-Sol
        30.10.2024 21:42

        Под vs и Rosetta нужно как-то отдельно настраивать на сколько я понимаю?

        https://github.com/abiosoft/colima?tab=readme-ov-file#customization-examples

        Так же к слову и конфигурации VM, чтобы все тестируемые имели одинаковое количество выделенных ядер и памяти

        Если не секрет, с какой целью вам требуется конфигурирование путей?

        Не секрет: помимо какого-то навязчивого желания все контролировать на грани ОКР и желания держать $HOME максимально чистым больше целей и нет:)


  1. Gummilion
    30.10.2024 21:42

    Может я чего-то не понимаю, но графики производительности по docker и podman нереалистичные, ну не может быть околонулевой нагрузки (или тогда бы и время сборки было во много раз больше). Может, у них вся работа перекладывается на какой-то отдельный процесс, который не отследили?


    1. Apokalepsis Автор
      30.10.2024 21:42

      Много раз перетестировал, потому-что тоже смущало, там не около нуля. По памяти - Docker - 560 MB, Podman - 688 в idle и 850 под нагрузкой. По энергии Docker - 41, Podman idle - 910, Load 239.

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

      Время сборки у них одно из самых высоких. Возможно не понял этот пункт


  1. mystdeim
    30.10.2024 21:42

    Спасибо за репу со скриптом, классно всё сделано, играюсь локально. Правильно я понимаю что Docker Desktop оказался самым ресурсоэффективным?

    Единственное что не хватает: тестирования контейнеров под нагрузкой. Мой рабочий вокрфлоу как разработчика включает в себя постоянные прогоны тестов, некоторые могут дольвольно тяжелые интеграционные с контейнерами. Docker UI я почти никогда не открываю. Эксперименты на жирных рабочих проектах (прогон тестов с контейнерами) показали, что orbstack на 1 процент быстрее всего лишь, но CPU есть больше ~10%


  1. homm
    30.10.2024 21:42

    Так вы включали Virtualization framework в Docker Desktop?