DB-유

Memcached Setup for PHP Applications

· linux, memcached, php, performance

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 CLI php -m can say yes while FPM says no). Check /etc/php/7.2/fpm/conf.d/ for a memcached.ini symlink, or run phpenmod 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 4m in the config, or, better, do not cache objects that large.
  • On CentOS 7, PHP-FPM cannot connect although nc can: SELinux blocks httpd from making network connections. sudo setsebool -P httpd_can_network_memcache on.
  • SERVER HAS FAILED AND IS DISABLED UNTIL TIMED RETRY from the extension: a connection failure marked the server dead for retry_timeout seconds; check the daemon and look at memcached.log.

Bind it to localhost, watch evictions, and mostly forget about it.