Memcached Setup for PHP Applications
A PHP application that hits the database for the same lookups on every request (site settings, menu trees, translated strings, the current user's permissions) spends a lot of its time asking MySQL questions it asked a second ago. Memcached is the simplest fix: an in-memory key-value store on the same host, a few hundred microseconds per lookup, and no persistence to worry about because it is only ever a cache. It also makes a good PHP session store, which matters once you have more than one web server behind a load balancer.
I've added it to enough slow PHP sites that I no longer think twice. This is Ubuntu 18.04 with PHP 7.2 (CentOS 7 differences noted): install, lock to localhost, connect the extension, move sessions over. PHP-FPM should already be working, and 64-256 MB of spare RAM is plenty for most single-site setups.
Install memcached and the PHP extension
Ubuntu 18.04:
sudo apt update
sudo apt install memcached libmemcached-tools php-memcached
sudo systemctl restart php7.2-fpm
CentOS 7 with PHP from the Remi repository:
sudo yum install memcached libmemcached
sudo yum install php-pecl-memcached
sudo systemctl restart php-fpm
There are two PHP extensions with confusingly similar names. php-memcache (no d) is the older one; php-memcached (with d) is built on libmemcached, supports binary protocol, consistent hashing and SASL, and is the one to use. Confirm it loaded:
php -m | grep -i memcached
Configure the daemon
On Ubuntu the options live in /etc/memcached.conf. The defaults are safe but small:
-d
logfile /var/log/memcached.log
-m 64
-p 11211
-u memcache
-l 127.0.0.1
-c 1024
-P /var/run/memcached/memcached.pid
Raise -m to the memory you can spare (in MB). The -l 127.0.0.1 line is the critical one: memcached has no authentication, and an instance listening on a public interface is both a data leak and a well-known DDoS amplifier. If PHP runs on a separate host, bind to the private interface address and firewall it to that host only.
On CentOS 7 the equivalent is /etc/sysconfig/memcached:
PORT="11211"
USER="memcached"
MAXCONN="1024"
CACHESIZE="128"
OPTIONS="-l 127.0.0.1"
Restart and enable:
sudo systemctl enable --now memcached
sudo systemctl restart memcached
Poke it from the command line
echo stats | nc 127.0.0.1 11211 | head -20
printf 'set greeting 0 60 5\r\nhello\r\nget greeting\r\nquit\r\n' | nc 127.0.0.1 11211
The second command stores a five-byte value with a 60-second TTL and reads it back. libmemcached-tools also provides memcstat --servers=127.0.0.1 for a nicer stats output.
Using it from PHP
A minimal cache-aside pattern for an expensive query:
<?php
$mc = new Memcached();
$mc->addServer('127.0.0.1', 11211);
$mc->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
$key = 'site_settings_v1';
$settings = $mc->get($key);
if ($mc->getResultCode() === Memcached::RES_NOTFOUND) {
$settings = $pdo->query('SELECT name, value FROM settings')->fetchAll(PDO::FETCH_KEY_PAIR);
$mc->set($key, $settings, 300);
}
Two habits I picked up the hard way: put a version suffix in key names so you can invalidate everything by changing it, and check getResultCode() rather than testing the return value for false, because false is a legitimate cached value. Frameworks such as Laravel, Symfony and WordPress (via an object-cache drop-in) have memcached drivers, so usually you configure the host in their settings rather than writing this by hand.
Sessions in memcached
Edit the FPM php.ini, /etc/php/7.2/fpm/php.ini on Ubuntu (/etc/php.ini on CentOS):
session.save_handler = memcached
session.save_path = "127.0.0.1:11211"
With several web servers, list all memcached instances separated by commas and set memcached.sess_consistent_hash = On in /etc/php/7.2/mods-available/memcached.ini so a given session always maps to the same server. Restart PHP-FPM. Note that sessions vanish when memcached restarts or evicts them under memory pressure; if that is unacceptable, use Redis or the database for sessions and memcached for caching only.
Is it being used?
Create a test page, load it twice, and watch the hit counters:
echo stats | nc 127.0.0.1 11211 | grep -E 'get_hits|get_misses|curr_items|bytes |evictions'
get_misses should increment on the first load and get_hits on subsequent ones. After enabling session storage, log in to the application and check curr_items increases by one per new session. Keep an eye on evictions: if it climbs steadily, -m is too small for your working set.
Notes on failures
Fatal error: Uncaught Error: Class 'Memcached' not found: the extension is not loaded for FPM (the CLIphp -mcan say yes while FPM says no). Check/etc/php/7.2/fpm/conf.d/for amemcached.inisymlink, or runphpenmod memcached, then restart FPM.Warning: session_start(): Unable to connect to 127.0.0.1:11211: memcached is not running or is bound to another address.ss -tlnp | grep 11211.- Values over 1 MB are silently not stored: memcached's default item size limit. Raise it with
-I 4min the config, or, better, do not cache objects that large. - On CentOS 7, PHP-FPM cannot connect although
nccan: SELinux blocks httpd from making network connections.sudo setsebool -P httpd_can_network_memcache on. SERVER HAS FAILED AND IS DISABLED UNTIL TIMED RETRYfrom the extension: a connection failure marked the server dead forretry_timeoutseconds; check the daemon and look atmemcached.log.
Bind it to localhost, watch evictions, and mostly forget about it.