Zend Framework Cache Backend Libmemcached + Session Cache

by Mike Willbanks on October 24th, 2010

If I haven’t said it before, I suppose I will say it now, I love memcached; it is a wonderful tool to have in your tool belt. However, Zend Framework does not currently have an official cache adapter for Memcached (it has one for the extension memcache but not memcached). The ZF 1.x trunk now has a Libmemcached adapter thanks to Marc Bennewitz for implementing the changes required to my patch and for everyone that helped to get this in. I believe it will become a part of the 1.11 Zend Framework release.

The Libmemcached adapter implements the PHP “memcached” extension. If you are currently using the current Zend Framework “Memcached” cache adapter, I urge you to take a peak at the new one from the link above as there are a few options that have been added and it supports all of the client options from the Memcached PHP extension.

From here, it gets to be a little more fun… About 6-9 months ago, I had implemented a Session_SaveHandler_Cache (although it seems some others have as well), to plug this all in together and keep things consistent in a ZF application. It now enables us to utilize the consistent hashing from the Libmemcached adapter by using the PHP Memcached extension. I’ll go over this a little more below.

Overview

When looking to improve performance and you are utilizing PHP sessions, where are they currently stored? Did you know it is extremely hard to scale if you put them on a database server? What about an NFS mount or even separating load based on the visitor. All of these are bad practice as it is really not meant to store your sessions.

In comes memcached – memcached can handle a great deal more and you can spread around your sessions utilizing consistent hashing. This means that if one server was to go down you would still have most of your other sessions available. The speed increase that comes from this is very high certainly if you stored them in the database before. In our experience, the database load dropped over 100% after moving sessions out of there and into the memcached servers.

Storing Sessions in Memcached with Zend Framework

Zend Framework has an excellent API for sessions in order to build out your own session save handler. By taking this approach I can still leverage the cache manager and share the same connection to memcached for other things in my application. If you need to expire contents during a deployment this can still handle it utilizing the “clone” keyword.

On to the code! See the link above for the Libmemcached implementation if you do not have the latest RC release. Next we need to build out the session save handler. We will name this Zend_Session_SaveHandler_Cache as it will support any cache that you would like to throw at it. The benefit of this really comes down to the lower environments and the ability to simply store sessions in APC for a time or even going to a simple file based for development / testing.

Zend_Session_SaveHandler_Cache

You will want to store this file in library/Zend/Session/SaveHandler/Cache.php. This handles the main setup for doing the Cache save handler.

 
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-webat this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Session
 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id$
 */
 
/**
 * @see Zend_Session
 */
require_once 'Zend/Session.php';
 
/**
 * @see Zend_Config
 */
require_once 'Zend/Config.php';
 
/**
 * @see Zend_Cache
 */
require_once 'Zend/Cache.php';
 
/**
 * Zend_Session_SaveHandler_Cache
 *
 
 * @category   Zend
 * @package    Zend_Session
 * @subpackage SaveHandler
 * @copyright  Copyright (c) 2010 CaringBridge. (http://www.caringbridge.org)
 */
class Zend_Session_SaveHandler_Cache
    implements Zend_Session_SaveHandler_Interface
{
 
    /**
     * Zend Cache
     * @var Zend_Cache
     */
    protected $_cache;
 
    /**
     * Destructor
     *
     * @return void
     */
    public function __destruct()
    {
        Zend_Session::writeClose();
    }
 
    /**
     * Set Cache
     *
     * @param Zend_Cache_Core $cache
     * @return Zend_Session_SaveHandler_Cache
     */
    public function setCache(Zend_Cache_Core $cache)
    {
        $this->_cache = $cache;
    }
 
    /**
     * Get Cache
     *-
     * @return Zend_Cache_Core
     */
    public function getCache()
    {
        return $this->_cache;
    }
 
    /**
     * Open Session
     *
     * @param string $save_path
     * @param string $name
     * @return boolean
     */
    public function open($save_path, $name)
    {
        $this->_sessionSavePath = $save_path;
        $this->_sessionName     = $name;
 
        return true;
    }
 
    /**
     * Close session
     *
     * @return boolean
     */
    public function close()
    {
        return true;
    }
 
    /**
     * Read session data
     *
     * @param string $id
     * @return string
     */
    public function read($id)
    {
        if (!$data = $this->_cache->load($id)) {
            return null;
        }
        return $data;
    }
 
    /**
     * Write session data
     *
     * @param string $id
     * @param string $data
     * @return boolean
     */
    public function write($id, $data)
    {
        return $this->_cache->save(
            $data,
            $id,
            array(),
            Zend_Session::getOptions('gc_maxlifetime')
        );
    }
 
    /**
     * Destroy session
     *
     * @param string $id
     * @return boolean
     */
    public function destroy($id)
    {
        return $this->_cache->remove($id);
    }
 
    /**
     * Garbage Collection
     *
     * @param int $maxlifetime
     * @return true
     */
    public function gc($maxlifetime)
    {
        return true;
    }
 
}

Setting up the Application

To set up the application to work correctly, you will need to modify the application.ini file if you are using Zend_Application as well as modifying the bootstrap process to set the cache.

Application Configuration

This file is located at application/configs/application.ini

; setup the cache
resources.cachemanager.memcached.frontend.name                            = Core
resources.cachemanager.memcached.frontend.options.automatic_serialization = On
resources.cachemanager.memcached.backend.name                             = Libmemcached
resources.cachemanager.memcached.backend.options.servers.one.host         = localhost
resources.cachemanager.memcached.backend.options.servers.one.port         = 11211
resources.cachemanager.memcached.backend.options.servers.one.persistent   = On
; session savehandler class
resources.session.name = phpsessionname
resources.session.saveHandler.class        = Zend_Session_SaveHandler_Cache
resources.session.gc_maxlifetime           = 7200

Setting up the Bootstrap

This file is located at: application/Bootstrap.php. Since the Zend_Session_SaveHandler_Cache requires a cache to be set, we need to place this in the bootstrap, also you will likely want to ensure no possibility of session hijacking in the bootstrap as well.

    /** 
     * Initialize the Session Id
     * This code initializes the session and then
     * will ensure that we force them into an id to
     * prevent session fixation / hijacking.
     *
     * @return void
     */
    protected function _initSessionId()
    {   
        $this->bootstrap('session');
        $opts = $this->getOptions();
        if ('Zend_Session_SaveHandler_Cache' == $opts['resources']['session']['saveHandler']['class']) {
            $cache = $this->bootstrap('cachemanager')
                          ->getResource('cachemanager')
                          ->getCache('memcached');
            Zend_Session::getSaveHandler()->setCache($cache);
        }
        $defaultNamespace = new Zend_Session_Namespace();
        if (!isset($defaultNamespace->initialized)) {
            Zend_Session::regenerateId();
            $defaultNamespace->initialized = true;
        }
    }

Sharing Libmemcached between Content and Sessions

Say we want to share the cache within our code base but utilize an SVN revision number to keep a prefix for the cache but not the sessions. Here is an implementation to do so in the Bootstrap.php file based on the above configuration.

    /**
     * Initialize the Cache Manager
     * Initializes the memcached cache into
     * the registry and returns the cache manager.
     *
     * @return Zend_Cache_Manager
     */
    protected function _initCachemanager()
    {
        $cachemanager = $this->getPluginResource('cachemanager')
                             ->init();
 
        // fetch the current revision from svn and use it as a prefix
        // why: we do not want to restart memcached, or you will lose sessions.
        if (!$appVersion = apc_fetch('progsite_version')) {
            $dir = getcwd();
            chdir(dirname(__FILE__));
            $appVersion = filter_var(`svn info | grep "Revision"`, FILTER_SANITIZE_NUMBER_INT);
            chdir($dir);
            unset($dir);
            if (!$appVersion) {
                $appVersion = mt_rand(0, 99999); // simply handles an export instead of checkout
            }
            apc_store('progsite_version', $appVersion);
        }
 
        $memcached = $cachemanager->getCache('memcached');
        $memcached->setOption('cache_id_prefix', APPLICATION_ENV . '_' . $appVersion);
 
        return $cachemanager;
    }
 
    /**
     * Initialize the Session Id
     * This code initializes the session and then
     * will ensure that we force them into an id to
     * prevent session fixation / hijacking.
     *
     * @return void
     */
    protected function _initSessionId()
    {
        $this->bootstrap('session');
        $opts = $this->getOptions();
        if ('Zend_Session_SaveHandler_Cache' == $opts['resources']['session']['saveHandler']['class']) {
            $cache = $this->bootstrap('cachemanager')
                          ->getResource('cachemanager')
                          ->getCache('memcached'); 
            $cache = clone $cache;
            $cache->setOption('cache_id_prefix', APPLICATION_ENV);
 
            Zend_Session::getSaveHandler()->setCache($cache);
        }
        $defaultNamespace = new Zend_Session_Namespace();
        if (!isset($defaultNamespace->initialized)) {
            Zend_Session::regenerateId();
            $defaultNamespace->initialized = true;
        }
    }

Note that the above code clones the current memcached adapter. We do this so that we can keep 1 instance that utilizes the cache_id_prefix with a number for the revision number and then sessions do not need that. This allows us to see content changes at deployment when the web server is restarted but we do not need to lose our session states.

Conclusion

Memcached is a handy tool to have, it also works well to integrate into the Zend Framework. I hope someone else also finds the Session_SaveHandler_Cache useful. This has been running out in a production environment for quite a while and has been extremely stable. Why not take it for a run yourself.

From PHP

4 Comments
  1. very nice idea indeed :)

    I would not necessarily say that db backed sessions wont scale, you can do consistent hashing in the session handler and scale as much as you want too. But still we use memcahced for sessions too :)

    thanks for a nice post, code and idea to prefix objects with revision :)

    cheers

  2. i tried Session SaveHandler with Memcache a while ago(i think 6 months or so)
    worked like a charm on WIN machine (memcached is on local virtual freeBSD server).
    but when i moved to prod servers(freeBSD) nothing worked.
    so i just switched to php.ini with session.save_handler = memcache

    might give it a new try in a while.

  3. Pv Ledoux permalink

    Hi,

    nice post! I was waiting for memcached implementation for a while.
    Just a question, why do you put your class in the Zend structure. Is it not safer to keep it in your application namespace?

  4. @PvLedoux
    The Memcached implementation is actually now in the latest Zend Framework (called Libmemcached).
    I actually put my specific classes where I generally submit them back to ZF into the Zend folder for a few reasons:
    1. Writing Tests
    2. I utilize PEAR and then have a local override folder by include paths ~ so my upgrades go through PEAR first and then locally second.

    If you were on shared hosting it would likely be a different story for you but you could still add in an additional folder for that.

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS