03 Jan 2025 - by 'Maurits van der Schee'
You can store PHP sessions in Memcache or Redis. High traffic websites with multiple application nodes choose either sticky sessions with file session storage (recommended) or centralized Memcache or Redis session storage. If you choose Memcache or Redis you should NOT rely on your favorite framework's implementation (see: "A session locking test suite for PHP"). But even if you rely on native implementations in PHP extensions you may run into wrong default values. This post explains you how to configure PHP session storage in Memcache and Redis and test that it locks properly.
The default session locking as implemented by the "files" save_handler has no expiration. This means that the lock is held as long as the request is executed. If you use Memcache or Redis for session storage, then the lock is automatically released after a certain amount of seconds. I don't know the exact reasoning for this automatic release, but I guess it is for robustness. In the case that a PHP process crashes the "flock" locks that the "files" handler uses are automatically released, while lock keys in Memcache or Redis are not automatically removed. Below are the maximum session lock times (on a Debian based Linux):
These defaults wildly vary and that does not make much sense. Before explaining what would be a sensible time, lets explain something about PHP's maximum execution time first.
Find the max execution time:
sudo grep -R max_execution_time /etc/php
On a modern Debian based Linux servers this will show:
/etc/php/8.3/cli/php.ini:max_execution_time = 30
/etc/php/8.3/apache2/php.ini:max_execution_time = 30
This means that PHP's maximum execution time is 30 seconds.
The set_time_limit() function and the configuration directive max_execution_time only affect the execution time of the script itself. Any time spent on activity that happens outside the execution of the script such as system calls using system(), stream operations, database queries, etc. is not included when determining the maximum time that the script has been running. This is not true on Windows where the measured time is real.
- source
So any script can take longer than the maximum execution time, by for instance firing a slow database query (see also this RFC). The user will only receive an error when the script runs longer than Apache wants to wait, as defined in the Apache config:
grep -R ^Timeout /etc/apache2/
By default it says:
/etc/apache2/apache2.conf:Timeout 300
Note that the PHP script is not terminated when Apache breaks the connection after 300 seconds. Note that in Nginx this variable is called "fastcgi_read_timeout
" and defaults to 60 seconds.
I feel that any request that takes more than 1 second server render time is taking too long. But if you have API requests that do expensive queries AND that use (even just read) the session, then you may want to have a lock timeout that reflects your actual request runtime to avoid session write losses in other requests for the same session. This may seem counter-intuitive, but read my other posts on this topic to fully understand why this is true.
Install using:
sudo apt install memcached php-memcache
How to configure "php.ini":
session.save_handler="memcache"
session.save_path="tcp://localhost:11211"
What you may want to set in "/etc/php/8.3/mods-available/memcache.ini":
memcache.lock_timeout=300
Memcache defaults to 15 (seconds) of maximum lock time.
Install using:
sudo apt install memcached php-memcached
How to configure "php.ini":
session.save_handler="memcached"
session.save_path="localhost:11211"
What you may want to set in "/etc/php/8.3/mods-available/memcached.ini":
memcached.sess_lock_retries=2000
The default here is 5 retries, leading to 750 milliseconds as the maximum lock time. By increasing the value factor 400 to 2000 retries we get 300 seconds of maximum lock time.
Install using:
sudo apt install redis php-redis
How to configure "php.ini":
session.save_handler="redis"
session.save_path="tcp://localhost:6379"
What you may want to set in "/etc/php/8.3/mods-available/redis.ini":
redis.session.locking_enabled=1
redis.session.lock_wait_time=150000
redis.session.lock_retries=2000
Redis defaults to disabled, a wait time of 2000 (2ms) with 10 retries leading to 20 milliseconds of max lock time. Changing the values makes the session lock work as expected.
In order to test the execution, you may install Apache2 using:
sudo apt install apache2 libapache2-mod-php php curl
In the file '/var/www/html/test.php' we put:
<?php
//ini_set("session.save_handler", "memcache");
//ini_set("session.save_path", "tcp://localhost:11211");
//ini_set("memcache.lock_timeout","300"); // default 15 (15s)
//ini_set("session.save_handler", "memcached");
//ini_set("session.save_path", "localhost:11211");
//ini_set("memcached.sess_lock_wait_min","150"); // default 150 (150ms)
//ini_set("memcached.sess_lock_wait_max","150"); // default 150 (150ms)
//ini_set("memcached.sess_lock_retries","2000"); // default 5 (try 5x)
//ini_set("session.save_handler", "redis");
//ini_set("session.save_path", "tcp://localhost:6379");
//ini_set("redis.session.locking_enabled","1"); // default 0 (disabled)
//ini_set("redis.session.lock_wait_time","150000"); // default 2000 (2ms)
//ini_set("redis.session.lock_retries","2000"); // default 10 (try 10x)
session_start();
if (isset($_SESSION['var'])) {
echo $_SESSION['var'] += 1;
sleep(1);
} else {
$_SESSION['var'] = 0;
}
Now we simply run 'test.sh':
#!/bin/bash
# set up cookie jar with session cookie
curl -c cookies.txt http://localhost/test.php
# run processes and store pids in array
pids=()
for i in {1..5}; do
curl -b cookies.txt http://localhost/test.php &
pids[${i}]=$!
done
# wait for all pids
for pid in ${pids[*]}; do
wait $pid
done
# remove cookie jar
rm cookies.txt
echo
Now you can run:
bash test.sh
And it should output:
12345
The script fires 5 threads that use the same session and print an increasing counter. If the threads print '12345' your session locking works and if it prints '11111' or even '1' then session locking has failed.
You can check the '/var/log/apache2/error.log' for errors and you'll find that when the session locking fails an error is reported there. The php-memcache extension would show the worrying:
AH00051: child pid 72055 exit signal Segmentation fault (11), possible coredump in /etc/apache2
The php-memcached extension would show:
PHP Warning: session_start(): Failed to read session data: memcached (path: localhost:11211) in /var/www/html/test.php on line 19
And the php-redis extension shows on a failing session lock:
PHP Warning: PHP Request Shutdown: Failed to write session data (redis). Please verify that the current setting of session.save_path is correct (tcp://localhost:6379) in Unknown on line 0
All in all you would probably find out what the problem is and that the default values configured in these plugins are not what they should have been. I hope this post helped you understand and solve session storage issues.
Enjoy!
PS: Liked this article? Please share it on Facebook, Twitter or LinkedIn.