lima-docker-lean

Docker-хост на macOS: анализ и замеры

English version

Задача: Docker-хост на Mac с Apple Silicon, который ничего не стоит в простое. Это значит ноль дискового I/O, CPU как можно ближе к нулю и RAM, которая возвращается macOS, когда не нужна. При этом нужен полноценный Docker Engine API. Проект, ради которого всё делалось, завязан на testcontainers, несколько файлов docker compose и werf, поэтому рантайм без docker.sock не подходит.

Стенд

   
Машина Apple M5 (4 производительных + 6 энергоэффективных ядер), 16 ГБ RAM
Хост macOS 26.6.2 (25G83)
VMM Lima 2.2.0, vmType: vz (Apple Virtualization.framework), 4 vCPU, 6 ГиБ, virtiofs, Rosetta включена
Гости Alpine 3.23.4 cloud-образ (ядро 6.18.22 / 6.18.53) и ISO alpine-lima std v0.2.50 (Alpine 3.24.1, ядро 6.18.38)
Docker 29.5.2 из apk (cloud) / 29.8.1 статическая сборка docker.com (ISO); CLI на хосте 29.5.2
Нагрузка postgres:17-alpine + pgbench, foundationdb/foundationdb:7.3.77 (движок ssd) + Python-биндинги 7.3.79
Дата 2026-09-24

Каждая цифра ниже получена за один прогон, без повторов и доверительных интервалов. Разницу в несколько процентов считайте шумом; см. Ограничения.

1. Какие есть варианты

Этот раздел — кабинетное исследование (release notes, документация, issue-трекеры), а не бенчмарк. Руками проверены только Lima и Apple container.

  Lima 2.2 (vz) OrbStack 2.2.3 Colima 0.10.3 Podman 6.1 (libkrun) Docker Desktop 4.92 Apple container 1.4.1
Настоящий Docker Engine + docker.sock ✅ ✅ ✅ (это и есть Lima) ⚠️ эмуляция API ✅ ❌
Опубликованные порты на localhost ✅ guestagent ✅ ✅ ✅ ✅ ❌ у machine
Возвращает RAM в macOS ❌ (#4220, #2789) ✅ «динамическая память» (со слов производителя) ❌ — частично —
CPU в простое ~1% ядра, замеры ниже ~0,1% (со слов производителя) как у Lima — выше —
Лицензия Apache-2.0 закрытый код, платно для коммерческой работы MIT Apache-2.0 платно для крупных компаний Apache-2.0

По Lima: в 2.2.0 исправлена утечка сокета на каждое проброшенное соединение (#5210, фикс #5216). На долгоживущем инстансе она в итоге упиралась в RLIMIT_NOFILE, и проброс портов молча умирал. Для testcontainers это важно: он открывает много соединений. Ещё в Lima 2.2 убрали template:_images/alpine, теперь шаблоны должны называть версию (_images/alpine-3.23).

2. Apple container machine как Docker-хост

Проверено на container 1.3.1, потом сверено с release notes 1.4.1. Конфигурация: свой Alpine-образ, в котором dockerd слушает tcp://0.0.0.0:2375, запущенный через container machine.

Что не работает, причём так задумано, а не из-за ошибок настройки:

1.3.1 → 1.4.1. Версию 1.4.0 отозвали. В 1.4.1 два исправления безопасности в Containerization (обход каталога через симлинки при загрузке OCI-образа; переполнение длины sun_path), команда container clean, расширенный container system status, слэши в JSON больше не экранируются, Containerization обновлён до 0.45.0. Для сети machine ничего не изменилось. Среди открытых запросов — пользовательские маунты (apple/container#1805, #2278) и machine stats (#1919).

3. LinuxKit и самодельная microVM

Идея: собрать гостя по образцу vminit от Apple или VM Docker Desktop, где нет ничего, кроме ядра, init и dockerd.

Ближе всего к этому из того, что Lima поддерживает штатно, — ISO alpine-lima. Сравнение ниже.

4. Гостевая ОС: немутабельный ISO или cloud-образ

  ISO Cloud, как есть Cloud с правками (итоговый шаблон)
Пустой простой, CPU VZ-процесса на хосте 0,96% ядра 1,31% 1,00–1,12%
Запись на диск гостя в простое, 180 с (одна сессия) 0 Б 86 КБ за окно 120 с¹ 0 Б
RAM гостя после загрузки (used) 133 МБ + 76 МБ корня в tmpfs 148 МБ —
RSS dockerd / containerd 79 / 41 МБ 85 / 47 МБ —
limactl restart → READY ~12 с ~26 с ~14 с
Можно менять командную строку ядра ❌ зашита в ISO ✅ grub ✅ grub
Простой с запущенными FDB и Postgres 3,88% 4,13% 3,77%
pgbench, 8 клиентов, tps 15 495 15 084 14 497
Запись в FDB, ключей/с 245 тыс. 246 тыс. 253 тыс.

¹ Замер через две отдельные сессии limactl shell, то есть вместе со входами; с цифрами по одной сессии не сравнивается.

Вывод. После правок варианты одинаково тихие и одинаково быстрые. Решает командная строка ядра: только в cloud-образе можно задать init_on_free=1, а это примерно вдвое уменьшает RAM, которую VM реально удерживает на хосте после нагрузки (§6). ISO-вариант оставлен в репозитории для тех, кому немутабельность важнее.

5. Простой: что стоит ресурсов, а что нет

Диск: с ~25 КБ/мин до нуля

Единственный, кто пишет на диск в простаивающем госте, — lima-guestagent. Каждые 10 с он пишет в лог SyncTime: system time synchronized with host (drift was 377.9ms). Дрейф никогда не сходится, и цикл нельзя отключить конфигом (в Lima 2.2 это по-прежнему так, #5365 только взял строку в кавычки). Около 600 Б/мин лога превращаются в ~25 КБ/мин блочной записи через журнал ext4. Та же строка копится и на хосте в ~/.lima/<name>/ha.stderr.log (~900 КБ/сутки, без ротации, обнуляется рестартом).

Решение — /var/log в tmpfs плюс commit=60, причём в provision: mode: boot, чтобы это произошло до того, как guest agent откроет свой лог. mode: system уже поздно: демон продолжит писать в старый inode. Результат — 0 байт за 180–240 с простоя. В ISO корень и так в tmpfs, там это не нужно.

CPU: мелкие утечки в cloud-образе

Профиль пробуждений гостя за 30 с в пустом простое, снят bench/wakeups.sh:

  ISO Cloud, как есть
Прерывания arch_timer ~102/с ~157/с
Переключения контекста lima-guestagent ~22/с ~24/с
containerd ~17/с ~23/с
rcu_preempt ~1,3/с ~9,5/с
прочее — log_proxy ~4/с, syslogd ~2/с, init ~2/с

Правки в cloud-образе применялись по одной; после каждой — CPU VZ-процесса на хосте:

Шаг CPU на хосте
как есть 1,31%
ядро 6.18.22 → 6.18.53 1,28%, то есть дело не в ядре
cloud-init-hotplugd выключен, syslogd в кольцевой буфер 1,27%
getty на ttyAMA0 выключен 1,00%

inittab образа запускает getty на ttyAMA0, а такого устройства в VZ нет, поэтому busybox init перезапускает его бесконечно. Это самое большое падение из замеренных.

Ещё:

Остаток, около 1% одного ядра, — это в основном таймеры Go-рантайма в lima-guestagent и containerd. Настройками Lima его не убрать. --tick (по умолчанию 3 с) — это только интервал опроса портов.

Колонка IDLEW у top накопительная с момента старта процесса, сравнивать её между прогонами нельзя. Смотрите на прирост CPU-времени (ps -o time).

6. Память: VZ её не отдаёт, и что с этим делать

Что видно. После одного прогона pgbench phys_footprint VZ-процесса стоит на полных 6 ГиБ. echo 3 > /proc/sys/vm/drop_caches в госте освобождает 5,6 ГБ внутри гостя, но footprint на хосте остаётся 6158 МБ. У VZ нет free page reporting, а Lima не управляет balloon.

Как мерить правильно. phys_footprint (и колонка Memory в Activity Monitor) считает сжатые страницы в несжатом размере, поэтому возврат памяти по ней не увидеть. Например, vmmap показывал 4,9 ГБ из 6 ГБ VM в swapped_out (swap здесь нет, это значит «сжато»), а footprint по-прежнему был 6 ГБ. Честная цифра — сколько памяти хост получает обратно, когда VM останавливается: прирост free по vm_stat.

Эксперимент (bench/memtest.sh): нагрузка pgbench + FDB → drop_caches в госте → 40 с давления на память хоста (memory_pressure -l warn) → limactl stop.

  Без init_on_free С init_on_free=1
RAM, освобождённая на хосте остановкой VM (реальная цена VM) 4193 МБ 2054 МБ
Страницы VM в компрессоре → сколько места занимали 5043 МБ → 3025 МБ (1,7×) 4327 МБ → 238 МБ (18×)
pgbench tps / FDB ключей/с в том же прогоне 15 084 / 246 тыс. 15 713 / 242 тыс.

Почему это работает: с init_on_free=1 ядро гостя обнуляет каждую освобождаемую страницу. Компрессор XNU хранит страницы из одного повторяющегося значения почти бесплатно, поэтому, когда памяти начинает не хватать, свободные страницы VM почти ничего не стоят. Но page cache — это занятая память с настоящими данными. Поэтому итоговый шаблон запускает маленький сервис cache-trim: он сбрасывает чистый page cache раз в 10 минут и только когда гость простаивает (load1 < 0,2).

Побочные эффекты:

Без давления у macOS нет причин забирать память, и от этого ничего не теряется.

7. Контейнеры под нагрузкой: Postgres и FoundationDB

Итоговый шаблон, по results/05-final-template-idle-and-load.txt:

   
pgbench -s 50, 8 клиентов × 60 с 14 497 tps, средняя задержка 0,55 мс
pgbench, 1 клиент × 20 с 3331 tps, средняя задержка 0,30 мс
FDB, 400 тыс. ключей × 100 Б, 8 потоков, 500 ключей на транзакцию 253 тыс. ключей/с
FDB, задержка коммита одного ключа p50 1,07 мс, p99 1,61 мс
FDB, чтение всего диапазона 535 тыс. ключей/с
Простой с обоими контейнерами 3,8% ядра хоста; ~33 КБ/с записи на диск гостя

8. Мелкие находки

Ограничения

Как воспроизвести

limactl start --name=dev ./templates/minimal-docker.yaml && limactl restart dev
docker context create lima-dev --docker "host=unix://$HOME/.lima/dev/sock/docker.sock"
./bench/idle.sh dev 120 60        # пустой простой
./bench/disk-writes.sh dev 180    # запись на диск в простое, одна сессия
./bench/wakeups.sh dev 30         # кто будит гостя
./bench/load.sh dev               # FDB + Postgres: простой с контейнерами, нагрузка, простой после
./bench/memtest.sh dev            # реальная цена RAM после нагрузки (останавливает VM, давит на память хоста)

Сырые выводы всех упомянутых прогонов лежат в results/.