Wednesday, September 15, 2010

reliable multicast memcached

PHP has a major issue with clustering sessions. Namely, reliability and scalability in a clustered environment. Network caching like this has been solved in Java land for ages now. ehcache+jgroups does a beautiful job of doing this. However, because PHP doesn't have a VM running all the time, you have to resort to outside solutions.

Thus, the only options I found are:

a) Store the sessions in a database which is really slow and a potential single point of failure.
b) Put them into memcached. This requires duplicating session data over the network to multiple memcached servers in case one of the servers goes down (repcached only allows replication between two servers).

The optimal solution is to use a reliable multicast publish/subscribe system where PHP would write out the data onto a multicast network where memcached is listening. There would be a memcached instance on the same server as PHP as well as several 'remote' memcached servers available on different hardware. If one memcached server goes down, PHP should just failover to another one in its configured list. All of the memcached servers would have the same exact data in their caches, but with multicast, only one message needs to go out on the network.

The issue with this is that multicast can be very hard to implement. Thankfully, the people over at zeromq have made this easy. Just a few lines of python and a big portion of the problem has been solved. The subscriber code below listens on the multicast network and stuffs data into memcached. Nice and simple. zeromq takes care of the reliability of multicast and makes it trivial to send messages from a publisher.

import time
import zmq
import memcache

def main ():
 ctx = zmq.Context(1)
 s = ctx.socket(zmq.SUB)
 s.connect("epgm://eth0;239.192.1.1:5555")
 s.setsockopt(zmq.SUBSCRIBE,'')
 mc = memcache.Client(['127.0.0.1:11211'], debug=0)

 try:
  while True:
   msg = s.recv()
   print msg
   split = msg.split("|")
   mc.set(split[0], split[1])
   time.sleep(0.1)
 except KeyboardInterrupt:
  pass
  
if __name__ == "__main__":
    main ()

Obviously, there is more work to be done here, such as what happens when a memcached server is restarted (how do you populate its cache?), and I'll approach that soon. But, for a hacked together proof of concept, things are going pretty well so far. I've really missed hacking up prototypes like this.