Prawdopodobnie zauważyłeś także, że korzystanie z Symfony opartego o Dockera na Macu jest cholernie wolne. Rzekłbym nawet, że niemożliwe. Niezależnie od tego, jak szybki jest Twój Mac. Ten artykuł pokaże Ci kilka sposobów na przyspieszenie środowiska.
Wstęp
Docker zyskuje co raz większe grono poparcia jeśli chodzi o stawianie środowiska developerskiego. Jak zapewnie wiesz, docker daje nam wiele korzyści, które ułatwiają pracę programistom – między innymi:
- Identycznie środowisko developerskie i produkcyjne u każdego programisty
- Łatwość w użyciu (dostawienie jakiejkolwiek usługi zajmuje dosłownie kilka linijek konfiguracji)
- Zdecydowanie większa wydajność w porównaniu do VM, przynajmniej na Linuksie – tak właśnie jest w tym artykule
Na początek utwórzmy sobie prosty projekt, który pokaże problem. Jeśli już takowy posiadasz, pomiń ten krok.
Demo
Ten przykładowy projekt zawierać będzie wygenerowany projekt Symfony oraz podstawową konfigurację dockera.
1 2 3 4 5 6 7 8 9 10 |
📂 symfony-docker-test 📂 app 📂 docker 📂 php 📄 Dockerfile 📄 php.ini 📂 nginx 📄 app.conf 📄 docker-compose.yml |
Symfony project
W katalogu symfony-docker-test uruchom:
1 |
symfony new app |
Do tego potrzebujesz polecenia symfony
w swoim lokalnym środowisku, opisane tutaj.
Konfiguracja kontenerów
Edytuj docker-compose.yml zgodnie z poniższym:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
version: '2' services: php: build: ./docker/php/ environment: TIMEZONE: Europe/Paris volumes: - ./docker/php/php.ini:/usr/local/etc/php/php.ini:ro - ./app:/var/www/app working_dir: /var/www/app webserver: image: nginx:1.11 depends_on: - php volumes_from: - php volumes: - ./docker/nginx/app.conf:/etc/nginx/conf.d/default.conf:ro ports: - 8080:80 composer: image: composer:1.4 volumes_from: - php working_dir: /var/www/app |
Budowanie kontenera PHP
Dodaj plik Dockerfile do katalogu docker/php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
FROM php:7.1-fpm # Install recommended extensions for Symfony RUN apt-get update && apt-get install -y \ libicu-dev \ && docker-php-ext-install \ intl \ opcache \ && docker-php-ext-enable \ intl \ opcache # Permission fix RUN usermod -u 1000 www-data |
Oraz konfigurację PHP do pliku docker/php/php.ini:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
date.timezone = ${TIMEZONE} short_open_tag = Off log_errors = On error_reporting = E_ALL display_errors = Off error_log = /proc/self/fd/2 memory_limit = 256M ; Optimizations for Symfony, as documented on http://symfony.com/doc/current/performance.html opcache.max_accelerated_files = 20000 realpath_cache_size = 4096K realpath_cache_ttl = 600 |
Utworzenie wirtualnego hosta dla nginx
Edytuj plik docker/nginx/app.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
upstream php-upstream { server php:9000; } server { root /var/www/app/web; listen 80; server_tokens off; location / { try_files $uri @rewriteapp; } location @rewriteapp { rewrite ^(.*)$ /app.php/$1 last; } location ~ ^/(app|app_dev|app_test|config)\.php(/|$) { fastcgi_pass php-upstream; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTPS off; } } |
Uruchom projekt
Wszystko gotowe, czas uruchomić środowisko.
1 |
docker-compose up |
Twoja aplikacja powinna być widoczna pod adresem: http://localhost:8080/app_dev.php, powinieneś zobaczyć coś podobnego
Wszystko fajnie… poza jednym: aplikacja jest straszne wolna 😩
Uruchom podstawowy benchmark w środowisku produkcyjnym i developerskim (częściej będziesz korzystał z tego drugiego):
1 2 3 |
ab -n 100 -r http://127.0.0.1:8080/ ab -n 100 -r http://127.0.0.1:8080/app_dev.php |
Wyniki?
- prod: 17 sekund
- dev: 129 sekund!!!
Rozwiązanie #1: Wyniesienie vendorów i cache
Po kilku testach (brane stąd: https://github.com/michaelperrin/docker-symfony-test), okazało się, że wąskim gardłem jest współdzielony katalog vendor, który zawiera bardzo dużo plików.
Jedynym skutecznym rozwiązaniem jest wyniesienie vendorów w miejsce, które widzi tylko kontener, a nie host.
By to zrobić, edytuj plik composer.json w katalogu app i dodaj parametr config-dir w sekcji config:
1 2 3 4 5 6 7 8 |
<span class="token punctuation">{</span> ... <span class="token property">"config"</span><span class="token operator">:</span> <span class="token punctuation">{</span> ... <span class="token property">"vendor-dir"</span><span class="token operator">:</span> <span class="token string">"/app-vendor"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Edytuj app/autoload.php w katalogu app i zmień linijkę:
1 2 |
<span class="token variable">$loader</span> <span class="token operator">=</span> <span class="token keyword">require</span> <span class="token constant">__DIR__</span><span class="token punctuation">.</span><span class="token string">'/../vendor/autoload.php'</span><span class="token punctuation">;</span> |
na:
1 2 |
<span class="token variable">$loader</span> <span class="token operator">=</span> <span class="token keyword">require</span> <span class="token string">'/app-vendor/autoload.php'</span><span class="token punctuation">;</span> |
Dodaj folder /app-vendor
do wolumenów kontenera php w docker-compose.yml
:
1 2 3 4 5 6 7 |
<span class="token key atrule">services</span><span class="token punctuation">:</span> <span class="token key atrule">php</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true"># ...</span> <span class="token key atrule">volumes</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true"># ...</span> <span class="token punctuation">-</span> /app<span class="token punctuation">-</span>vendor |
Upewnij się, że cache Symfony został wyczyszczony i zainstaluj zależności composera raz jeszcze:
1 2 |
docker-compose run --rm composer install |
Wyniki po tym zabiegu:
- Prod: 2.8 sekundy
- Dev: 16 sekund
Zobaczmy czy osiągniemy lepsze wyniki bez udostępniania katalogu cache. W pliku AppKernel.php edytuj metodę getCacheDir
1 2 3 4 5 6 7 8 9 |
<span class="token keyword">class</span> <span class="token class-name">AppKernel</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// ...</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">getCacheDir</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token string">'/dev/shm/symfony_docker_test/cache/'</span><span class="token punctuation">.</span><span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">environment</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Wyniki:
- Prod: 1.2 sekundy
- Dev: 5 sekund
Nie jest źle ale uważaj! Vendory są teraz w kontenerze, nie są widocznie na hoście. Nie będziesz w stanie debugować, podpowiedzi w IDE nie będą dostępne. Moja rada? Pierw zainstaluj zależności w standardowy sposób, następnie zmień composer.json by zainstalować je do kontenera. To rozwiązanie nie jest złe, jeśli Twoje zależności nie zmieniają się zbyt często.
Rozwiązanie #2: Docker sync
Docker sync (http://docker-sync.io/) to narzędzie korzystające z rsynca do synchronizacji volumenów między hostem a kontenerem.
Zainstaluj docker-sync:
1 2 |
sudo gem install docker-sync |
Dodaj docker-sync.yml do głównego katalogu projektu
1 2 3 4 5 |
<span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">'2'</span> <span class="token key atrule">syncs</span><span class="token punctuation">:</span> <span class="token key atrule">app-sync</span><span class="token punctuation">:</span> <span class="token key atrule">src</span><span class="token punctuation">:</span> <span class="token string">'./app'</span> |
Skopiuj docker-compose.yml do docker-compose-dev.yml i dodaj poniższe linijki na sam koniec:
1 2 3 4 |
<span class="token key atrule">volumes</span><span class="token punctuation">:</span> <span class="token key atrule">app-sync</span><span class="token punctuation">:</span> <span class="token key atrule">external</span><span class="token punctuation">:</span> <span class="token boolean important">true</span> |
Dodaj nazwę do wolumenu katalogu app, zmieniając:
1 2 3 4 5 6 7 8 9 |
<span class="token key atrule">services</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true">#...</span> <span class="token key atrule">php</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true">#...</span> <span class="token key atrule">volumes</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true"># ...</span> <span class="token punctuation">-</span> ./app<span class="token punctuation">:</span>/var/www/app |
na:
1 2 3 4 5 6 7 8 9 |
<span class="token key atrule">services</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true">#...</span> <span class="token key atrule">php</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true">#...</span> <span class="token key atrule">volumes</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true"># ...</span> <span class="token punctuation">-</span> app<span class="token punctuation">-</span>sync<span class="token punctuation">:</span>/var/www/app |
Dodaj Makefile do głównego katalogu projektu, pozwoli Ci to na łatwe uruchamianie i zatrzymywanie środowiska:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
OS := $(shell uname) start_dev: ifeq ($(OS),Darwin) docker volume create --name=app-sync docker-compose -f docker-compose-dev.yml up -d docker-sync start else docker-compose up -d endif stop_dev: ifeq ($(OS),Darwin) docker-compose stop docker-sync stop else docker-compose stop endif |
Od teraz możesz startować środowisko poprzez:
1 2 |
make start_dev |
To polecenie uruchomi kontenery oraz docker-synca (za pierwszym razem, wszędzie odpowiadaj na tak)
Wyniki są imponujące, ze 129 sekund zeszliśmy do 1!
- Prod: 0.6 sekundy
- Dev: 1.2 sekundy
Udało się! Niestety, czasem występują problemy z synchronizacją, a pliki nie są synchronizowane z hostem do kontenera, a także z niektórymi prawami użytkownika w niektórych plikach (chmod 777 do ręcznej naprawy).
Rozwiązanie #3: Cache Dockera
Zespół Dockera jest świadom słabej wydajności Dockera dla Maców. Ostatnia beta Dockera pozwala montować wolumeny w nowy sposób. Jeśli masz pobraną wersję beta, po prostu edytuj docker-compose.yml dodając opcję :cached
1 2 3 4 5 6 7 |
<span class="token key atrule">services</span><span class="token punctuation">:</span> <span class="token key atrule">php</span><span class="token punctuation">:</span> <span class="token comment" spellcheck="true"># ...</span> <span class="token key atrule">volumes</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token comment" spellcheck="true"># ...</span> <span class="token punctuation">-</span> ./app<span class="token punctuation">:</span>/var/www/app<span class="token punctuation">:</span>cached |
Wyniki?
- Prod: 5.1 sekund
- Dev: 15.7 sekund
Nie jest źle, aczkolwiek nie tak skutecznie jak pozostałe rozwiązania. Nie jestem w chwili obecnej w stanie powiedzieć jakie są wady tego rozwiązania – ciągle z nim eksperymentuję.
Podsumowanie
Dobra wiadomość! Docker na Macu może pociągnąć projekt Symfony. Rozwiązanie nie jest idealne ale dobre chociaż tyle. W poniższej tabelce zebrałem wszystkie wyniki rozwiązań. Wybierz te, które najbardziej Ci odpowiada.
Dev benchmark | Prod benchmark | |
---|---|---|
Domyślnie | 129 sekund | 17 sekund |
rozwiązanie #1: | 5 sekund | 1.2 sekundy |
rozwiązanie #2: | 1.2 sekundy | 0.6 sekundy |
rozwiązanie #3: | 15.7 sekund | 5.1 sekund |