А зачем?

Если вы работали на macos в docker окружении, то наверняка сталкивались с проблемой в производительности при volume mount, например, при работе над php проектом, операции с файловой системой хоста (обновление пакетов composer, ребилд контейнеров symfony, etc.) занимают просто неадекватное количество времени. Об особенностях работы docker'а на macos написано уже множество статей, а также workaround'ов как заставить его работать быстрее. В этой небольшой статье покажу как в решении этой проблемы Mutagen помог мне с php проектом и быть может поможет вам.

Что такое Mutagen

Mutagen - мощный инструмент для файловой синхронизации и сетевой переадресации, он является быстрой альтернативой стандартного volume mount средствами docker и при этом субъективно более удобной в сравнении с Docker Sync или NFS Mount и может быть легко добавлен в конечном проекте.

Описание гласит:

Mutagen’s file synchronization uses a novel algorithm that combines the performance of the rsync algorithm with bidirectionality and low-latency filesystem watching.

Хорошо, low-latency filesystem watching это как раз то, что нам нужно.

Установка Mutagen

В первую очередь необходимо установить mutagen (логично). В примере покажу установку с помощью brew, она тривиальна за исключением необходимости tap'нуть нужное хранилище:

$ brew tap mutagen-io/mutagen
$ brew update
$ brew install mutagen

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

Делается это единожды, с помощью встроенного генератора нужного shell скрипта:

С недавних пор, при установке через brew, вместе с mutagen'ом создается файл автокомплита в /opt/homebrew/share/zsh/site-functions, что удобно и не вынуждает дописывать что-то в .zshrc

# ZDOTDIR="${HOME}/.zsh"
$ mutagen completion zsh > "${ZDOTDIR}/compoteion/mutagen.sh"
echo 'source "${ZDOTDIR}/completion/mutagen.sh"' >> .zshrc

В качестве оболочки для которой генерируется скрипт, помимо zsh возможны bash, fish и даже powershell - удобненько).

На хостовой машине mutagen работает как демон, запускаемый командой:

$ mutagen daemon start

а чтобы не делать этого постоянно, он умеет добавлять (и удалять) себя в автозапуск через

$ mutage daemon register # unregister

После всех этих манипуляций можно получить список sync'ов, чтобы проверить что все установилось без проблем:

$ mutagen sync list
--------------------------------------------------------------------------------
No synchronization sessions found
--------------------------------------------------------------------------------

Как настроить работу с Mutagen'ом

Часто для создания рабочего окружения мы пользуемся docker-compose - им удобно собирать всю инфраструктуру, которая может пригодиться. И для того чтобы воспользоваться преимуществами синхронизации файлов через mutagen, нам необходимо внести изменения в docker-compose.yml, в которых отключим volume mount для директории с проектом и добавляем volume'ы в которые будет идти синхронизация:

$ diff --git a/docker-compose.yml b/docker-compose.yml
index 124dfb9..d94338e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,3 +2,15 @@ version: "3.8"
 # Add new services, volumes, networks or
 # override declared in .environment/docker-compose.yml services
 # if it's necessary
version: "3.8"
+
+volumes:
+    storage-php:
+    storage-nginx:
+
services:
    nginx:
        depends_on:
            - php
        image: nginx:1.21.1
        env_file: .env
        ports:
            - ${PUBLISHED_NGINX_PORT}:8080
        volumes:
-            - ../public:/var/www/app/public
+            - storage-nginx:/var/www/app/public
    php:
        image: php:8.1
        working_dir: /var/www/app
        env_file: .env
        volumes:
-            - ../:/var/www/app
+            - storage-php:/var/www/app
+            - storage-nginx:/var/www/app/public
        ports:
            - ${PUBLISHED_FPM_PORT}:8080
        user: root
        extra_hosts:
            - "host.docker.internal:host-gateway"

и перезапустим окружение. В моем случае поднятие проекта сделано через команду в makefile'е и включает в себя composer install как последний этап, из-за чего при первом запуске увидел такое сообщение:

Composer could not find a composer.json file in /var/www/app
To initialize a project, please create a composer.json file. See https://getcomposer.org/basic-usage
make: *** [docker-start] Error 1

Что логично - выполняемый composer install не может найти файл composer.json в созданных volume'ах т.к. файлы еще не перенесены в контейнер, поэтому добавим такое действие в makefile:

DOCKER=docker-compose --env-file=./.env --file=./docker-compose.yml

.PHONY: mutagen-sync # Sync app volume with mutagen
mutagen-sync:
	${DOCKER} images php | awk '{ if (NR!=1) { print $$1 } }' | ( read container; \
mutagen sync create \
--name=${COMPOSE_PROJECT_NAME} \
--default-file-mode-beta=0644 \
--default-directory-mode-beta=0755 \
--sync-mode=two-way-resolved \
--ignore=.git/,.idea/,.DS_Store \
. docker://root@$$container/var/www/app )

Что действие делает: оно создает новый sync в mutagen между volume'мом с именем из переменной окружения COMPOSE_PROJECT_NAME, задаст права для файлов и директорий которые будут синхронизированы в volume, установит режим синхронизации и добавит в игнор то, что нет смысла синхронизировать.

Важно то, что mutagen подключается прямо к контейнеру по его имени, но при запуске docker-compose сам создает контейнерам имена по собственному шаблону, поэтому чтобы точно сказать mutagen'у куда подключаться, мы просто awk'аем список контейнеров по имени сервиса из docker-compose.yml.

Как пользоваться

Максимально просто - после всех манипуляций описанных выше, достаточно после поднятия docker-compose выполнить make mutagen-sync, и увидеть в консоли

$ make mutagen-sync
Created session sync_ka1nSJCfLPlOnLxiXyPe4ScFQ0WtyOmikqlAPaGnxos

Это значит что сессия синхронизации успешно создана. Для дальнейшего мониторинга статуса синхронизации и ее остановки добавим в makefile пару действий:

.PHONY: mutagen-terminate # Terminate mutagen app sync
mutagen-terminate:
	mutagen sync terminate ${COMPOSE_PROJECT_NAME}

.PHONY: mutagen-monitor # Stats of mutagen syncing
mutagen-monitor:
	mutagen sync monitor ${COMPOSE_PROJECT_NAME} --long

Теперь make mutagen-monitor покажет подробное real-time состояние sync'а проекта:

$ make mutagen-monitor
Name: php-project
Identifier: sync_ka1nSJCfLPlOnLxiXyPe4ScFQ0WtyOmikqlAPaGnxos
Labels: None
Configuration:
        Synchronization mode: Default (Two Way resolved)
        Maximum allowed entry count: Default (2⁶⁴−1)
        Maximum staging file size: Default (18 EB)
        Symbolic link mode: Default (Portable)
        Ignore VCS mode: Default (Propagate)
        Ignores: None
Alpha configuration:
        URL: /Users/tonysol/Projects/php-project
        Watch mode: Default (Portable)
        Watch polling interval: Default (10 seconds)
        Probe mode: Default (Probe)
        Scan mode: Default (Accelerated)
        Stage mode: Default (Mutagen Data Directory)
        File mode: Default (0600)
        Directory mode: Default (0700)
        Default file/directory owner: Default
        Default file/directory group: Default
Beta configuration:
        URL: docker://root@php-project_php_1/var/www/app
        Watch mode: Default (Portable)
        Watch polling interval: Default (10 seconds)
        Probe mode: Default (Probe)
        Scan mode: Default (Accelerated)
        Stage mode: Default (Mutagen Data Directory)
        File mode: Default (0644)
        Directory mode: Default (0755)
        Default file/directory owner: Default
        Default file/directory group: Default
Status: Watching for changes

а make mutagen-terminate остановит созданный sync по его имени - безопасно если есть несколько параллельно живущих проектов docker-compose.

И вот пример списка sync'ов после запуска:

$ mutagen sync list
--------------------------------------------------------------------------------
Name: php-project
Identifier: sync_ka1nSJCfLPlOnLxiXyPe4ScFQ0WtyOmikqlAPaGnxos
Labels:
        None
Alpha:
        URL: /Users/tonysol/Projects/php-project
        Connection state: Connected
Beta:
        URL: docker://root@php-project_php_1/var/www/app
        Connection state: Connected
Status: Watching for changes

Статус Watching for changes говорит о том что файлы успешно синхронизированы. Убедиться в этом можно, перейдя в консоль контейнера и запустив composer install.

Результат

Самое интересное - а насколько быстрее стала работа.

Использование mutagen'а обкатывалось на тестовом проекте основанном на Symfony. При этом для docker включены опции:

  • ✔ Use gRPC FUSE for file sharing (используется macFUSE 4.2.4)

  • ✔ Use Docker Compose V2

  • ✔ Use the new Virtualization framework

Содержание ~/.docker/daemon.json:

{
        "builder": { "gc": { "defaultKeepStorage": "20GB", "enabled": true } },
        "experimental": false,
        "features": { "buildkit": true }
}

Выделенные docker'у ресурсы:

CPUs: 4 | Memory: 6.00 GB | Swap: 2 GB | Disk image size: 59.6 GB

Перед каждым замером времени в консоли, полностью удалялись директории vendor/ и var/cache/dev/

Docker volume mount

root@22d281c05041:/var/www/app# time composer install --quiet

real    6m50.509s
user    2m26.838s
sys     1m27.755s
root@22d281c05041:/var/www/app# time bin/console cache:clear

 // Celearing the chae for the dev environment with debug true


 [OK] Cache for the "dev" environment (debug=true) was successfully created.



real    0m35.079s
user    0m6.226s
sys     0m3.308s
root@22d281c05041:/var/www/app#

Mutagen

root@9a6d7a272f38:/var/www/app# time composer install --quiet

real    0m25.678s
user    0m18.139s
sys     0m10.999s
root@9a6d7a272f38:/var/www/app# time bin/console cache:clear

 // Celearing the chae for the dev environment with debug true


 [OK] Cache for the "dev" environment (debug=true) was successfully created.



real    0m9.664s
user    0m7.445s
sys     0m2.211s
root@9a6d7a272f38:/var/www/app#

Выводы, субъективные впечатления и etc.

  • Несмотря на то что все описание было в контексте php, все эти манипуляции применимы не только для обхода проблемы с docker volume в целом, но и, например, для real-time файловой синхронизации с выделенным сервером по ssh+scp.

  • Файловые операции внутри контейнера выполняются со скоростью нативного окружения, например rm -Rf vendor/ в случае mutagen выполнялся практически мгновенно, в то время как при volume mount это занимало ощутимое время.

  • Несмотря на то что замеры проводились по 4 раза, такой разбег при выполнении composer install сохраняется (разумеется с определенной долей погрешности), но учитывая что для cache:clear разница не настолько значительная, тяжело предположить причины такого провала скорости.

  • Задержки между синхронизацией, даже если они случаются, не ощущаются:

    • 1) Задержка при volume mount так же присутствует (связана с синхронизацией между VM и host).

    • 2) IDEA сама по себе не настолько быстро обновляет структуру проекта.

  • Судя по системному монитору ресурсов (и htop'у), увеличение нагрузки при работе с mutagen если и есть, то не заметное.

  • У mutagen есть свои инструменты орекстрации - Compose (заменяющий docker-compose, конфигурируемый прямо в docker-compose.yml) и Projects, но это для меня заклинания следующего уровня, поэтому здесь я их не рассматривал.

  • Реализация Compose основана на Docker Compose V2, поэтому рекомендуется включить использование V2 в настройках Docker Desktop, и имеет определенные ограничения.

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


  1. BullDER
    13.05.2022 13:29
    +1

    И насколько это релевантно после релиза virtiofs?


    1. TonyKentnarEarth Автор
      13.05.2022 14:21
      -1

      На момент теста VirtioFS еще не был доступен, надо будет сравнить насколько он будет быстрее/медленнее, чем предложенный вариант


      1. Vladivo
        14.05.2022 02:24
        +1

        VirtioFS, по ощущениям, несколько медленнее, но в целом оказался достаточно хорош, чтобы мы сразу отказались от мутагена. Маунтим около двух гигабайт с node_modules, yarn cache и Cypress. Билдим и гоняем e2e тесты в докере.


    1. glebovgin
      13.05.2022 20:21
      +1

      Мы на рабочих проектах в итоге отказались от mutagen в пользу virtiofs. Медленнее не стало уж точно.


  1. AlexGluck
    13.05.2022 13:48

    Чем бы дитя не тешилось, лишь бы линукс не ставить)


    1. ALexhha
      14.05.2022 08:41
      +1

      Чем бы дитя не тешилось, лишь бы линукс не ставить)

      В больших компаниях есть понятие обязательного корпоративного стандарта. И часто в их списки входят windows и/или macos. Линукс не встречал ни разу. Так что и сам "мучаюсь" последние 3 года на macos


  1. zvermafia
    13.05.2022 16:38

    Работал на iMac проц. (2 ядер, 4 потока), скорость записи на диск ~500MB/s, использовал docker desktop.
    Перешел на ПК Windows (12 ядер, 24 потока), скорость записи на диск ~5300MB/s, использовал docker desktop с опцией WSL2 engine.

    Не заметил разницу что билд образа, что установка пакетов composer внутри контейнера...

    ...занимают просто неадекватное количество времени...
    Вообще такого не было. О чем вы вообще?!


    1. TonyKentnarEarth Автор
      13.05.2022 18:36

      А на mac'е это был выделенный volume или mount на хост?


      1. zvermafia
        14.05.2022 19:11

        использовал опцию -v в CLI, и директиву volumes в конфиг файле для docker compose


  1. jgudmund
    14.05.2022 23:00

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