Sessions are one of the main building blocks of a login-based web application. We’re going to see what they are, what Symfony offers us to handle them and how to scale when you have ton of them.
What’s a session?
As you know the HTTP protocol is stateless: each request is independent from each other. Most of our applications need to maintain a state between different requests for the same user. A session is a way to achieve it. As an example consider a user logging in. Once logged in, the server creates a session for this user (assigning a unique number to it) and sends a cookie or a token back to the client. On the following requests the client sends this cookie or token to the server: this way the server can use the stored session and every data previously stored in it.
As you can see, we’re talking about storing data and referring to them on each request. This means we need both a storage and retrieval mechanism. Each server-side language abstracts for us the sessions management details: API and configuration values help us to deal with them without much pain.
To know more:
https://en.wikipedia.org/wiki/Session_(computer_science)
https://www.owasp.org/index.php/Session_Management_Cheat_Sheet
What does PHP offer to us?
It offers a lot: http://php.net/manual/en/book.session.php
As you can find into the extensive documentation, PHP abstracts session creation, storage, retrieval and deletion. Many settings are available to fine tune how sessions are stored and managed: http://php.net/manual/en/session.configuration.php
Ok, but I have a Symfony app… what does Symfony offer to us?
Symfony is a wonderful PHP framework and one of its main goal is to help you, as a developer, stay away from nitty-gritty stuff and instead focusing on what really matters to your domain.
As described very well in the Symfony doc the HttpFoundation component provides a clean and simple API to deal with sessions.
Creating and using a session is nothing more than:
1 2 3 4 5 6 7 8 9 |
<?php namespace Symfony\Component\HttpFoundation\Session; $session = new Session(); $session->start(); // storing data to session $session->set('name', 'Greg'); |
Symfony let us configure how we use sessions: http://symfony.com/doc/current/reference/configuration/framework.html#session
Many of the settings provided are overrides for the php.ini ones. You should always try to leverage your framework: it does the heavy work for you.
Wow! Symfony does everything for us… what else then?
Sessions are well-known for many years and they seem to be quite simple to use. Due to their simplicity developers tend to forget often two critical aspects: security and scalability / performance.
Security relates to sessions a lot. As explained by OWASP, many kind of attacks involve sessions: we don’t go further ahead on security here but I advise you to pay attention and have a look to this article: https://www.owasp.org/index.php/Session_Management_Cheat_Sheet
Scalability, according to Wikipedia, is “is the capability of a system, network, or process to handle a growing amount of work, or its potential to be enlarged in order to accommodate that growth”.
When traffic increases, usually the first thing to do is adding another web-server behind a load balancer. Database, shared storage, sessions: they are shared assets from every web-server and hence they have to be taken out and put somewhere else.
Finding a central place for our sessions: database? NAS? somewhere else?
Session files are not an option anymore (and, even with a single server, session files should be avoided because disks are terribly slow).
But wait… can we share a file-system between our servers and continue to store sessions on files? No, this is not an option. NAS is not an option! At least if you don’t want to get crazy and hurt yourself. Although feasible, shared file-system are tricky to set up and the worst option in our hands.
Storing sessions inside your main database (often a relational one) can be an option but why using a relational database to store something that’s more suitable to a key-value store? If you think for a moment, inside session you store scalar data but also complex data, from arrays to serialized objects.
Two most popular databases used for session storage are Memcached and Redis.
Memcached, Redis… cool but… how talk to them? Seems much work to do!
Nothing more wrong! Why? Symfony FrameworkBundles provides us a setting named “session.handler_id”. It let us choose how we want to handle our sessions. The default value is “session.handler.native_file”, meaning that our sessions will be saved on files. But we have two wonderful news:
- HttpFoundation component gives us (for free!) some handlers: PdoSessionHandler, MemcachedSessionHandler, MongoDbSessionHandler and some others.
- You can write your own!
Ok, seems simple: show me how to use Memcached
First of all you need to tell Symfony to use the Memcached session handler. In your config.yml simply put:
1 2 3 |
framework: session: handler_id: session.handler.memcached |
This handler is nothing more than a PHP class inside your vendor directory: “vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php”
Its constructor accepts two options: “prefix” and “expiretime”. The first one let you define a custom prefix for your sessions in order to avoid collisions with some other keys stored into your Memcached instance. The second one is for setting up a session time to live: by default your session will be deleted after 86400 seconds of inactivity.
It’s advisable to expose your Memcached server ip, port, session prefix and time to live as parameters. This way you can set up different environments with different configurations.
1 2 3 4 5 |
parameters: memcached_host: 127.0.0.1 memcached_port: 11211 memcached_prefix: custom_key_ memcached_expire: 14400 |
In order to pass “memcached_prefix” and “memcached_expire” to “MemcachedSessionHandler” you need to override the service definition:
1 2 3 4 |
services: session.handler.memcached: class: Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler arguments: [ "@session.memcached", { prefix: '%memcached_prefix%', expiretime: '%memcached_expire%' } ] |
We’ve almost done: just configuring our “session.memcached” service.
1 2 3 4 5 6 7 |
services: session.memcached: class: Memcached arguments: - '%memcached_prefix%' calls: - [ addServer, [ %memcached_host%, %memcached_port% ]] |
With the previous configuration there’s a problem: the Memcached class doesn’t check if duplicates connections get opened to Memcached server. To avoid this we need to create a little wrapper around Memcached class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<?php namespace Madisoft\AppBundle\Utils; class MemcachedWrapper extends \Memcached { /** * Prevent adding of new servers as duplicates. We're persistent! * * @param array $servers * * @return bool */ public function addServers(array $servers) { if (0 == count($this->getServerList())) { return parent::addServers($servers); } return false; } /** * Prevent adding of new server as duplicate. We're persistent! * * @param string $host * @param int $port * @param int $weight * * @return bool */ public function addServer($host, $port, $weight = 0) { foreach ($this->getServerList() as $server) { if ($server['host'] == $host && $server['port'] == $port) { return false; } } return parent::addServer($host, $port, $weight); } } |
Doing so we need to refactor the previously declared “session.memcached” service:
1 2 3 4 5 6 7 |
services: session.memcached: class: Madisoft\AppBundle\Utils\MemcachedWrapper arguments: - '%memcached_prefix%' calls: - [ addServer, [ '%memcached_host%', '%memcached_port%' ] ] |
Now you have set up a fully-functional Memcached handler! Is it cool, isn’t?
Wow… what else?
Let’s point out some other thing:
- We haven’t changed our php.ini. Of course we need to tell PHP to use Memcached by installing the “memcached” extension for PHP: it’s very easy to do with your operating system package manager of choice.
- You should install Memcached server in a centralized machine. You should consider setting up replicas to proper manage failover.
- Memcached is very fast but it’s not persistent. If you have a single Memcached instance and it stops, you lose every data. Depending on your app it could be critical or only frustrating.
Conclusion
Putting Symfony sessions on Memcached is very easy to set up. Memcached is fast, well-known and reliable. In a following article we’re going to see how to leverage Redis as session handler and we’re going to compare it to Memcached. Stay tuned!
Works! 🙂 Thank you.
good tutorial, do you have a tutorial for Symfony 3.3 for memcache?
thanks. No, I don’t because for some months we’ve been using Redis instead on Memcached (primarily because we also use Redis for general caching and so we get rid of the Memcached dependency)
it works locally but not on google app engine, I know that it’s great already what you have done, but I cannot find any clue to make it work on the google cloud and it is all the point of the scaling