2011/08/18

PECL-Memcache standard loadbalancer for Nginx

In a previous post i have already introduced my Nginx loadbalancer, which is compatible with the PECL-Memcache consistent hashing. Unfortunately we discovered that the implementation of the consistent hashing on the PHP side is quite inefficient since it rebuilds the hashring on every request, and in the case of our PHP framework even for each Memcache server on each request. So we had to decide between a Memcache proxy like Moxi to implement the consisten hashring, or just forget about the advantages of a consistent hash and save the additional hop of a moxi. The conclusion was to just use the standard PECL-Memcache loadbalancer, since this is probably the most resource efficient way to implement a cache, and then just live with having to regenerate some caches in case we add/remove Memcache instances.
So in order to make the Nginx find cache values which have been stored by PHP into Memcache, i wrote another Nginx loadbalancer which is simply copying the behaviour of the PECL-Memcache standard loadbalancer.
Example:
 70 upstream profilememcache {
 71     hash_key $memcached_key;
 72     server 10.20.0.27:11211;
 73     server 10.20.0.26:11211;
 74 }

-----

 570         location /profile {
 570         set $memcached_key $uri;
 571         error_page      500 404 405 = /fallback_php;
 572         memcached_pass  profilememcache;
 573         }
In the above example you see that the memcached_pass can be used just as always and in the upstream definition you simply need to add the parameter "hash_key" together with based on what the loadbalancer should do the hashing on.
Unfortunately its really important that the order of the Memcache servers in the upstream definition is the same as the order in which the Memcache servers are being added to the Memcache object in the PHP code. If the positions of the servers are not the same the whole thing won't work.

2011/06/22

PHP session parser in production

Last year I have posted about the Nginx ngx_http_php_session module that i wrote and which is able to parse php sessions and extract certain values from them. Unfortunately this took quite long time, but now i can finally announce that i am actually using this on a production system and so far it seems stable.
In this current case where I'm using the module in production now, it is used to optimize the way how Nginx is using the Memcaches. Since quite long time I already had a Memcache in production which is fed plain HTML by PHP and then read out and delivered by the Nginx. This works well as long as the content is more or less static and independent on the user which accesses it, and exactly this second requirement is now not necessary anymore. Using the ngx_http_php_session module the Nginx can check if a requesting user has a valid session, and if he does, it can check for things like payment status, gender, his preferences or whatever your site content is depending on. After this is known to the Nginx, the correct cache can then be retrieved accordingly. As a result there are many more pages that are cachable in a very efficient way.
Here i show an example how this module is used to decide if the Nginx should use the profile cache key for paying users or the one for non-paying users.
location /

    if ($cookie_session_id = "") {
        rewrite . /login redirect;
    }

    eval $session {
        set $memc_key $cookie_session_id;
        memc_pass session_memcache;
    }

    php_session_parse $authenticated $session "symfony/user/sfUser/authenticated";

    if ($authenticated != "b:1") {
        rewrite . /login redirect; #the user is logged out
    }

    php_session_parse $is_paying $session "symfony/user/sfUser/attributes|s:10:\"subscriber\";s:9:\"is_paying\"";
    php_session_parse $user $session "symfony/user/sfUser/attributes|s:10:\"subscriber\";s:11:\"getNickname\"";

    php_session_strip_formatting $stripped_user $user;

    if ($is_paying = "i:1") {
        rewrite . profile_cache$uri:paying last; # the user is paying
    }

    rewrite ^/(.+)$ $1;
    set $tmp_compare "$stripped_user:$uri";

    if ($tmp_compare ~* "^(.*):\1$") {
        rewrite . profile_cache$uri:paying last; # the user is viewing his own profile
    }

    rewrite . profile_cache$uri:non-paying last; # the user is non-paying
}

location profile_cache {
    default_type    text/html;
    add_header      "Content" "text/html; charset=utf8";
    lower           $uri_low $uri;
    set $memc_key $uri_low;
    memcached_next_upstream error timeout not_found;
    error_page      500 404 405 = /fallback_to_php;
    memc_pass  profilememcache;
}
In the previous example you can also see the directive php_session_strip_formatting. This is used because a string which is extracted from a PHP session is formatted, like for example s:7:astring. By using the php_session_strip_formatting directive the actual string astring will be extracted and the formatting will be stripped away.
Another directive from this example is lower. This directive is coming from another module that i wrote with the name lower_upper_case. It simply uppercases or lowercases a string in the Nginx config, and stores the result in another variable. In the above example I'm using it in to make the memcache keys case insensitive. To achieve this i just lowercase all keys on the Nginx, and also on the PHP side when it is filling the Memcache.
Both modules can be retrieved from github where I'm hosting the repositories:
https://github.com/replay/ngx_http_lower_upper_case
https://github.com/replay/ngx_http_php_session