Today I was working on a php development environment using docker on MacOSX (you might run into the same problems on windows, too!). The osxfs in Docker for Mac is painfully slow and the company behind docker is actually aware of that. At #77 they track the issue at docker-for-mac right now. At the time of writing the ticket is still open.
If you happen to use symfony as PHP framework or in particular composer as package manager for php projects, you end up
with a very big directory called vendor
.
In symfony standard edition I am counting 10259 files and folders.
For this blog post I put a vanilla symfony 3.2.7 project at DracoBlue/symfony-composer-docker-performance-test.
With this commit
I added a docker-compose.yml
and a nginx.conf
to run app.php
on http://app.locahost.me:8080
and
app_dev.php
on http://dev.locahost.me:8080
.
If you run a quick benchmark against app_dev.php:
$ ab -n 1000 -c 16 http://dev.localtest.me:8080/
Concurrency Level: 16
Time taken for tests: 319.303 seconds
Complete requests: 1000
Failed requests: 0
Requests per second: 3.13 [#/sec] (mean)
Time per request: 5108.845 [ms] (mean)
Time per request: 319.303 [ms] (mean, across all concurrent requests)
Transfer rate: 131.33 [Kbytes/sec] received
you end up with response times for a vanilla symfony project of round about 319ms.
The app.php:
$ ab -n 1000 -c 16 http://app.localtest.me:8080/
Concurrency Level: 16
Time taken for tests: 86.140 seconds
Complete requests: 1000
Failed requests: 0
Requests per second: 11.61 [#/sec] (mean)
Time per request: 1378.234 [ms] (mean)
Time per request: 86.140 [ms] (mean, across all concurrent requests)
Transfer rate: 54.50 [Kbytes/sec] received
looks better (86ms), but is not very convenient for development.
Let's tune this with a simple trick down to 46ms (-85%) for app_dev.php and down to 9ms (-88%) for app.php.
The solution
Given that in case of the php-fpm
container the vendor
directory can be considered readonly, why might solve it with
an always up to date in-docker data volume for just the vendor directory.
With this commit I replaced:
php-fpm:
image: exozet/php-fpm:7.1.2
volumes:
- ./:/usr/src/app
with:
php-fpm:
image: exozet/php-fpm:7.1.2
volumes_from:
- lsyncd
lsyncd:
image: dracoblue/lsyncd
environment:
- SOURCES=/mnt/vendor
- DESTINATIONS=/usr/src/app/vendor
- EXCLUDES=.svn:.git:.docker
volumes:
- ./:/usr/src/app
- ./vendor:/mnt/vendor
- /usr/src/app/vendor
in the docker-compose.yml
.
Now I ran the benchmarks again. First in dev mode with app_dev.php:
$ ab -n 1000 -c 16 http://dev.localtest.me:8080/
Concurrency Level: 16
Time taken for tests: 46.269 seconds
Complete requests: 1000
Failed requests: 0
Requests per second: 21.61 [#/sec] (mean)
Time per request: 740.303 [ms] (mean)
Time per request: 46.269 [ms] (mean, across all concurrent requests)
Transfer rate: 906.28 [Kbytes/sec] received
Down from 319ms/request to 46ms/request!
Afterwards the app.php:
$ ab -n 1000 -c 16 http://app.localtest.me:8080/
Concurrency Level: 16
Time taken for tests: 9.688 seconds
Complete requests: 1000
Failed requests: 0
Requests per second: 103.22 [#/sec] (mean)
Time per request: 155.002 [ms] (mean)
Time per request: 9.688 [ms] (mean, across all concurrent requests)
Transfer rate: 484.57 [Kbytes/sec] received
Down from 86ms/request to 9ms/request!
How does it work?
The /usr/src/app
is mounted from the host system (e.g. by docker with osxfs) into php-cli and the php-fpm container.
The relevant filesystem looks like this:
/usr/src/app contains all data + mounted from host with osxfs
After the modification, the subfolder /usr/src/app/vendor
is over-mounted as docker volume (with no reference to the
host). This way the data volume is docker only and very fast.
But at the beginning the vendor folder is empty:
/usr/src/app contains all data (except vendor!) + mounted from host with osxfs
/usr/src/app/vendor contains no data + is not mounted from host
By adding an additional lsyncd container with:
lsyncd:
image: dracoblue/lsyncd
environment:
- SOURCES=/mnt/vendor
- DESTINATIONS=/usr/src/app/vendor
- EXCLUDES=.svn:.git:.docker
volumes:
- ./:/usr/src/app
- ./vendor:/mnt/vendor
- /usr/src/app/vendor
and using
php-fpm:
image: exozet/php-fpm:7.1.2
volumes_from:
- lsyncd
to inherit the volumes from lsyncd
for the php-fpm
container sets up a sync from /mnt/vendor
to /usr/src/app/vendor
.
We have now this setup:
/mnt/vendor is mounted from host with osxfs
/usr/src/app contains all data (except vendor!) + mounted from host with osxfs
/usr/src/app/vendor receives incremental updates from /mnt/vendor + is not mounted from host
Whenever a file is changed on the host in the vendor folder, the lsyncd will sync the files from /mnt/vendor
to
/usr/src/app/vendor
. I tested this on my local box and had a lag of max. 1 second.
If you need two way sync, take a look at docker-sync by Eugen Mayer instead!
Drawbacks
- One-Way-Sync: Whenever you mount the
/usr/src/app/vendor
folder like this, it won't be writeable by the container. So please don't mount it on yourphp-cli
container and use it just forphp-fpm
containers. - Diskspace: The
vendor
directory exists two times, so you use double amount of hdd space - Initial-Sync: The lsyncd is started with
init
configured, so there will be an initial sync whenever you boot up the container (between 5 seconds and 2 minute, depends on your project and hard disk speed).
References
To end up with this solution, I read lots of posts in dockers forum, blogs or on stackoverflow. Additionally I tried most of the existing solutions. That's why I want to share them here, so you might have a starting point of the lsyncd' volume is not sufficient for your usecase.
- mickaelperrin's lsyncd container was a starting point for my [dracoblue/lsyncd] container with init enabled
- there are lots of host-to-docker two-way/one-way sync solutions: (I did not choose them, because they need extra tooling!)
- docker-sync by Eugen Mayer
- hodor
- blog posts / tools on nfs + docker machine (this is NOT docker for mac!)
- forum posts / tickets about the performance issues:
- official documentation and roadmap on this matter
- docker/for-mac#77 issue, which tracks this issue
Update: Other solution in docker >=14.04.0-ce: Suffix the volume with :cached
(Updated 2017/04/07 14:16 GMT+2)
With Version 17.04.0-ce (currently in edge, not yet stable! Thanks to eyeohno for the info) there will be a cached flag for volumes. See 17.04.0-ce release notes for further information.
So you can use:
php-fpm:
image: exozet/php-fpm:7.1.2
volumes:
- ./:/usr/src/app:cached
then.
Results on the same hardware (app_dev.php):
$ ab -n 1000 -c 16 http://dev.localtest.me:8080/
Concurrency Level: 16
Time taken for tests: 84.148 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 42939000 bytes
HTML transferred: 42614000 bytes
Requests per second: 11.88 [#/sec] (mean)
Time per request: 1346.369 [ms] (mean)
Time per request: 84.148 [ms] (mean, across all concurrent requests)
Transfer rate: 498.32 [Kbytes/sec] received
and app.php
$ ab -n 1000 -c 16 http://app.localtest.me:8080/
Concurrency Level: 16
Time taken for tests: 21.034 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 4807000 bytes
HTML transferred: 4572000 bytes
Requests per second: 47.54 [#/sec] (mean)
Time per request: 336.546 [ms] (mean)
Time per request: 21.034 [ms] (mean, across all concurrent requests)
Transfer rate: 223.18 [Kbytes/sec] received
So we end up with stats like these:
file | docker | docker | docker | lsyncd ad-hoc
| 17.03.0-ce | 17.04.0-ce | 17.04.0-ce | container
| | | :cached) |
------------------------------------------------------------------------
app_dev.php | 319ms/req | 312ms/req | 84ms/req | 46ms/req
app.php | 86ms/req | 83ms/req | 21ms/req | 9ms/req
That looks like a decent performance boost from 321ms/req to 84ms/req with :cached
! Even though my lsyncd ad-hoc
container is still slightly faster, it might be an option to go for :cached
once 17.04.0-ce is released stable.