Caching Pitfalls with Chrooted PHP-FPM

Why does my Wordpress talk like Roundcube?

Posted on Sun 28 May 2017

Using PHP-FPM in combination with Nginx to serve dynamic web pages is a very common combination today. Enabling chroot in the FPM pool configuration to add a layer of isolation (I intentionally avoid the term security here), is always a good idea but one needs to pay attention when it comes to caching, namely Zend OPcache and APC(U):

With FPM, the cache is held by the master process, not the pool workers. While this might seem handy to improve hit rates, it does not play well with chrooted workers: the index used for the cache is the file path as seen by the workers, i.e. the chroot-relative path! Having two workers for different web applications that both offer an identically named file (e.g. login.php) leads to identical chroot-relative paths and therefore to false cache hits:

/var/www/webapp1
└── htdocs
    └── login.php         --> cache index: /htdocs/login.php
├── log
└── session
/var/www/webapp2
└── htdocs
    └── login.php         --> cache index: /htdocs/login.php
├── log
└── session

This can make a Wordpress instance try to include a file that is actually part of a MediaWiki instance handled by a different pool, causing random HTTP-505s:

[24-May-2017 08:38:57 Europe/Berlin] PHP Fatal error:  require_once(): Failed opening required '/htdocs/includes/templates/NoLocalSettings.php' (include_path='.:/usr/share/php:/usr/share/pear') in /htdocs/includes/WebStart.php on line 129

A simple fix for this would be ensuring that all paths are unique amoung all pool chroots, e.g. by including the pool name in the document root:

/var/www/webapp1
└── htdocs-app1
    └── login.php         --> cache index: /htdocs-app1/login.php
├── log
└── session
/var/www/webapp2
└── htdocs-app2
    └── login.php         --> cache index: /htdocs-app2/login.php
├── log
└── session

I personally don't like this approach as it introduces information redundancy and requires more attention when configuring the document roots for the virtual servers.

A cleaner solution is to give each pool its own php-fpm master process, which separates all caches and has the side effect that the pools can be started or stopped independently (great for maintenance).

The following systemd unit makes this really easy:

# /etc/systemd/system/php5-fpm@.service

[Unit]
Description=The PHP FastCGI Process Manager (%i)
After=network.target

[Service] 
Type=notify
PIDFile=/var/run/php5-fpm-%i.pid
ExecStart=/usr/sbin/php5-fpm --nodaemonize --fpm-config /etc/php5/fpm/php-fpm-%i.conf
ExecReload=/bin/kill -USR2 $MAINPID

[Install]
WantedBy=multi-user.target

Using this unit pools can be managed as easy as systemctl start php5-fpm@pool1.

tags: linux, server, php, chroot | category: linux-stories


Comments !