Symfony and Memcached — The ultimate guide for a high performance setup

As an enthusiastic PHP developer, I want to have a high performance Symfony application that is able to scale without limits in a high availability setup, but how does that work? A simple solution is to use a distributed Memcached server pool.

What is Memcached?

Memcached is an in-memory key-value storage that has an easy failover setup. It uses the caching strategy called LRU, which means the least recently used item will be replaced if the memory is used completely and a new item needs to be placed. All the data is kept in RAM to aware a high throughput with a low latency. The main purpose is to use it as a fast caching layer and for a small amount of data that needs to be retrieved really fast. A perfect example is session handling for a user login. So replace your slow filesystem or get the load off your database with Memcached!

The maximum amount of data you can store for a key is 1MB, but of course Memcached supports compression out of the box, so the stored item needs less space. For a distributed Memcached setup you can decide how the distribution should work. The standard algorithm is modula. With the modula algorithm the application builds a hash of the given key and returns an integer to select one of the configured Memcached servers by executing a modulo calculation on the number of registered servers. The cache key is the base to calculate the hash, so in the end the data is always set or fetched from the same host. If the Memcached server list changes, the client will choose a different server, which leads to cache misses. To avoid this you can use the consistent mode, which means that for the same key the same server will be chosen, even if you add new Memcached servers to your registered list.

Modula algorithm to choose a server to set or fetch a key:

$key = 'foo';
$servers = ['memcached1:11211','memcached2:11211'];
$integerHash = hash($key);
$chosenServerIndex = $integerHash % count($servers);
$host = $servers[$chosenServerIndex];

If you choose the consistent mode, Memcached uses the ketama algorithm that works like a clock:

ketama algorithm — consistent mode

A circle will have e.g. 2^16 values(65536), starting with 0 at the 12h position and ends with the value 65535 one tick before the starting point. The algorithm adds the registered Memcached servers randomly at some values between the 16 bit number range. Then a hashing number is generated from the value which needs to be stored. The given numeric number for the value represents a position on the circle as well. Now the decision which key-value pair is saved on which server, is done clockwise with the closest key point to a server point as you can see in the upper picture. So it’s absolutely clear, that adding or removing a server ends with less cache misses than with the modula algorithm, where everything needs to be calculated again.

For session handling you need to use the consistent mode together with the replication setting. To be prepared for failovers you need to set the replication value to the amount of Memcached instances. If one instance dies, your session data still exists on the other instances and your user wont be logged out.

PHP installation with Memcached via Docker

The simplest setup to start working with Memcached and PHP is a Dockerfile. You can easily inherit from the official PHP Docker images and install the Memcached extension additionally.

Install memcached php extension with the official php Docker image

So now let’s start to build the image and access the bash to work with PHP in CLI mode with an enabled memcached extension:

docker build -t php-cli .
docker run --rm -it php-cli bash

Now that we know how to build a Docker image, we can create a docker-compose.yml file and set up a small playground to simply interact with two Memcached servers.

version: '3'
services:
php:
build: .
volumes:
- "./:/opt/"
working_dir: "/opt"
  memcached1:
image: memcached:latest

memcached2:
image: memcached:latest

The volume mount is used to execute a PHP file that we create in the current directory on the host machine that runs the docker container. Now you can run docker-compose.yml up -d to start the Memcached servers. After this you are able to run in multiple terminal sessions the following command to access a Docker container to work with:

docker-compose run --rm php bash

In the session you can execute the following script with php test.php (Same location where the docker-compose.yml file lives):

Set and fetch a key value pair from two Memcached servers

The script is setting a key-value pair(foo=>bar) on both servers(memcached1, memcached2). Afterwards the script tries to fetch the same key. The modula algorithm is used. This is the output for two independent terminal sessions to fetch the key-value pair:

Fetch the same key in two independent sessions from two Memcached servers

You get two times the same key-value pair fetched from “memcached2”.

For fast and dead simple key checks on a single Memcached server you can just run the following:

php -r '$c = new Memcached(); $c->addServer("memcached1", 11211); var_dump( $c->getAllKeys() );'

Memcached support of Symfony

If Symfony is your PHP framework of choice, you’re having a great support out of the box since version 3.3 with Memcached Cache Adapter. Before, you had to deal with an external dependency, the LswMemcacheBundle. This bundle had great support for Symfony 2 and PHP up to version 5.6. I were even able to run it with PHP 7, but since PHP 7.1 I had a lot of issues and you better go with the official Memcached Cache Adapter.

So if you want to speed up your Doctrine ORM, you can easily configure a metadata, query and result cache within two steps. First you need to setup a Memcached client like this in your services.yml:

parameters:
memcached.servers:
- memcached://memcached1:11211
- memcached://memcached2:11211
memcached.config: {distribution: 'consistent', compression: true}
services:
memcached.doctrine:
class: Memcached
factory: Symfony\Component\Cache\Adapter\MemcachedAdapter::createConnection
arguments: ['%memcached.servers%', '%memcached.config%']
    doctrine.cache.memcached:
class: Doctrine\Common\Cache\MemcachedCache
calls:
- [ setMemcached, [ '@memcached.doctrine' ] ]

Your parameters setup can be also handled with ENV variables of course(Dotenv Component). Now you need to pass the configured Doctrine Memcached service to your Doctrine ORM configuration(config.yml):

doctrine:
orm:
entity_managers:
default:
metadata_cache_driver:
type: 'service'
id: doctrine.cache.memcached
query_cache_driver:
type: 'service'
id: doctrine.cache.memcached
result_cache_driver:
type: 'service'
id: doctrine.cache.memcached

With this config, your Doctrine metadata(annotations, proxies, etc…) is stored in the memcached servers and you can use the result and query cache with your QueryBuilder:

return $this
->getEntityManager()
->createQueryBuilder()
->from('Product::class', 'p')
->where('p.price > :price')
->orderBy('p.price', 'ASC')
->setParameter('price', $price)
->getQuery()
->useQueryCache(true)
->useResultCache(true, 3600)
->getOneOrNullResult();

Symfony Session Handling

To set up a working session handler, you need two simple steps.

  1. Setup a Memcached Client instance in your services.yml
parameters:
memcached.servers:
- memcached://memcached1:11211
- memcached://memcached2:11211
memcached.config: {distribution: 'consistent', compression: true}
services:
memcached.session:
class: Memcached
factory: Symfony\Component\Cache\Adapter\MemcachedAdapter::createConnection
arguments: ['%memcached.servers%', '%memcached.session_config%']

session.handler.memcached:
class: Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler
arguments: ['@memcached.session', { prefix: '%memcached.session_prefix%', expiretime: '%memcached.session_expire%' }]

2. Pass the service identifier to the framework in your config.yml

framework:
session
:
handler_id: session.handler.memcached
name: "PHPSESSID"

That was pretty simple!

Summary

After checking the algorithms to deal with your multiple cache instances, you see that Memcached delivers a really easy to handle setup which works for simple tasks described above. Memcached is pretty fast and delivers your key-value pairs in <10 ms. Before you start to build up a much more complicated Redis cluster, check the needs of your setup and start making Symfony really really fast.