Symfony na dockerze dla Maca – optymalizacja

S

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.

📂 symfony-docker-test
    📂 app
    📂 docker
        📂 php
            📄 Dockerfile
            📄 php.ini
        📂 nginx
            📄 app.conf
    📄 docker-compose.yml

Symfony project

W katalogu symfony-docker-test uruchom:

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:

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:

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:

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:

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.

docker-compose up

Twoja aplikacja powinna być widoczna pod adresem: http://localhost:8080/app_dev.php, powinieneś zobaczyć coś podobnego

Słaba wydajność symfony na dockerze mac

Wszystko fajnie… poza jednym: aplikacja jest straszne wolna 😩

Uruchom podstawowy benchmark w środowisku produkcyjnym i developerskim (częściej będziesz korzystał z tego drugiego):

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:

{
    ...
    "config": {
        ...
        "vendor-dir": "/app-vendor"
    }
}

Edytuj app/autoload.php w katalogu app i zmień linijkę:

$loader = require __DIR__.'/../vendor/autoload.php';

na:

$loader = require '/app-vendor/autoload.php';

Dodaj folder /app-vendor do wolumenów kontenera php w docker-compose.yml:

services:  
  php:
    # ...
    volumes:
      # ...
      - /app-vendor

Upewnij się, że cache Symfony został wyczyszczony i zainstaluj zależności composera raz jeszcze:

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

class AppKernel  
{
    // ...
    public function getCacheDir()
    {
        return '/dev/shm/symfony_docker_test/cache/'.$this->environment;
    }
}

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:

sudo gem install docker-sync

Dodaj docker-sync.yml do głównego katalogu projektu

version: '2'  
syncs:  
  app-sync:
    src: './app'

Skopiuj docker-compose.yml do docker-compose-dev.yml i dodaj poniższe linijki na sam koniec:

volumes:  
  app-sync:
    external: true

Dodaj nazwę do wolumenu katalogu app, zmieniając:

services:  
  #...

  php:
    #...
    volumes:
      # ...
      - ./app:/var/www/app

na:

services:  
  #...

  php:
    #...
    volumes:
      # ...
      - app-sync:/var/www/app

Dodaj Makefile do głównego katalogu projektu, pozwoli Ci to na łatwe uruchamianie i zatrzymywanie środowiska:

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:

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

 

services:  
  php:
    # ...
    volumes:
      - # ...
      - ./app:/var/www/app: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 benchmarkProd benchmark
Domyślnie129 sekund17 sekund
rozwiązanie #1:5 sekund1.2 sekundy
rozwiązanie #2:1.2 sekundy0.6 sekundy
rozwiązanie #3:15.7 sekund5.1 sekund

 

O autorze

Filip Nowacki

Swoją przygodę z IT rozpoczął mając 13 lat, kiedy to stworzył swoją pierwszą stronę internetową. Obecnie pracuje jako backend & mobile developer, rozwijając kilka ogólnopolskich produktów o zasięgu blisko 15 milionów użytkowników. Prywatnie maniak optymalizacji, testów A/B, motoryzacji i architektury aplikacji.

Dodaj komentarz