Hardcore Performance Through Socket Servers
Posted by John Kleijn • Saturday, December 29. 2007 • Category: PHPA while ago, one of the moderators over at phpfreaks.com asked me to write a little about an idea I had: to run an entire PHP application as a daemon. This post is about that idea, how it works, why it works, and why it doesn't work.
The Zend engine is pretty smart when it comes to optimizing, and using PHP as an Apache or FastCGI module increases performance by reducing initialization time for the Zend engine and PHP environment, but it doesn't do much for your "userland" application. With large applications, this can become an issue. Some, like Wikipedia, try to solve this by utilizing memory caching, for example using Memcached. While this definitely increases performance, it leaves something to be desired. A Memchached server is in fact a caching daemon, transferring string data over TCP/IP. This means objects have to be serialized/unserialized, which takes both processing power and time. There is also some network overhead, since one request might entail several calls on the caching daemon.
In addition, parsing time can become an issue if you have a relatively large library.
There is something that is both a recipe for the perfect cake as the ultimate disaster: a PHP application socket server. The base ingredients:
- An application designed for efficiency and modularity;
- A socket server bootstrap file;
- A socket client bootstrap file.
That's it. As you can see it is fact stunningly simple.
I know you're thinking something along the lines of "if it's so simple and perfect and all, why isn't everybody doing this?" Well, the fact is, it's not simple, and it's not perfect either. Another reason is that PHP is not designed to operate in such a fashion, so it's not exactly the first thing that comes to mind when thinking about PHP performance. But, PHP socket servers have been around and succeeded for quite some time now, and given you can overcome the hurdles, the benefits may be well worth your trouble. At this point, maybe you're thinking about what's exactly to gain. The shortlist:
- Nullified parsing time;
- Lightning fast object caching.
Indeed a very short list, but with enormous implications.
Products like Zend Platform (and eAccelerator, I think) reduce code parsing time by caching the results of parsing (opcodes), and while the figures look convincing, I am convinced that it is still considerably slower than simply keeping the opcodes in memory across requests. Firstly, there isn't the overhead of a caching mechanism, and secondly with an application daemon there's the possibility of preparsing large amounts of files, or all of them. Naturally that results in higher memory consumptions, but with the amount of memory in servers today, even your "gigantic" 10MB application is not going to make a dent. Maybe I'll provide you with a little benchmark comparison, if I can get my hands on a copy of Zend Platform.
Parsing time doesn't just include parsing of PHP classes and scripts, but also INI and XML configuration files. If you're anything like me, you absolutely despise hard coding configuration options. But parsing XML is an expensive business. One can reduce this cost by caching a resulting object, which avoids repeated parsing of configuration files. This is good advice, normally, but keeping the parsed object probably makes this added step obsolete. This brings me to the second and most prominent advantage of this arguably insane idea: object caching and persistence.
File based object caching is, let's face it, slow. In fact, it is so slow, that you have to put in considerable consideration as to what to cache: sometimes recreating an object is actually more efficient. Memcached is considerably faster, but it still rips the object out of its environment, losing identity. If that last bit has you confused, let me try to explain.
PHP objects cannot exist outside the execution environment, obviously. Serialization creates a string representation of an object, but upon unserialization, you do not create the same object (in al likelyhood that object is no longer in memory), instead you have a cheap copy of the original object. An object seizes to exist as soon as it is dropped from memory.
Now this might not even be noticeable when your flattening just a single object, but objects commonly have member objects. That's when things get a little thorny. Say you've flattened object Foo, with member objects Bar and Meh. Somewhere along the way, you have an object Blah, which also needs to reference Meh. That means you have to unserialize Foo and Bar, just to obtain a reference to Meh. When you serialize Blah and reserialize Foo, you get two copies of Meh. I think you can see just how ugly this can get with large and complicated object structures.
This can be mended with an alternative method of serialization that maintains some identity of objects, with say an Identity Field, but I'll leave that to explore some other time.
Serialization also implies you can't store resource handles, such as file handles. Reinitializing resources: also an expensive business.
I think we've established that serialization, well, sucks. In contradiction, maintaining an object across requests, rules. Now that you are all psyched about this idea, let's explore the downsides to it (I love doing that), and in the process get a better idea of how it works.
We have ingredient no1, the application itself. All the classes and objects it uses are nicely kept in memory. You have to unset objects when they are no longer used, obviously, but that shouldn't be too much trouble. This persistence of objects is accomplished by running the application in a loop in ingredient no2: a socket server. The socket server accepts connections from ingredient no3, the socket client, and lets the application form a response to the data received from the end client (yes, the browser), via the socket client.
Pitfall numero unos: fault tolerance.
Unfortunately, PHP hasn't yet completely evolved beyond regular fatal errors, and there is no way to ensure that a fatal error throws an exception instead of an error. In a normal scenario, every request has its own execution environment, and if one dies, the others, normally, remain unaffected. When using an application daemon, if the daemon dies, your application dies.
Solution: don't throw fatal errors.
Obviously that's a short sounding solution for a whole heap of trouble. But it is a must; your application must be rock solid and always die gracefully. Your own code should only throw exceptions, and be defensively programmed.
Pitfall: high concurrency
To your Web server, your application daemon is just a single request, likely contained in just a single thread. Well, that sucks, obviously. They've gone through all that trouble to optimize for concurrent requests, and you screw it all up by running it all in a single thread.
Solution: none, really
Maybe, just maybe, this can be sorted with the pcntl_* functions. I don't have enough experience with them to tell you if this is the case though. If you have any thoughts on this, be sure to leave a message.
In any case, I doubt the lack of threading will quickly weigh up against even just the saved initialization time. With very high concurrency, you will probably start to feel this though. Basically this idea is best applied to heavy applications with moderate concurrency. If your site gets a hundred hits per second (for the mathematically challenged, that's 6000 hits per minute), best look for a solution elsewhere.
There is however another way that both increases stability and benefits from multi threading: a daemon pool. This makes the recipe more complicated by adding a gateway daemon between number 2 and 3 of the original recipe. This means you would be able to spawn multiple number two's, but also means you would need a third daemon (remember the gateway's the second) that handles the "global" application data. You could still let the application daemon handle session data (just be sure to maintain affinity between the end client and the application daemon), but application data (such as an object cache) would have to be flattened to be able to send it over the virtual wire. Basically a Memcached server would be suited for this job, and I just explained the limitations of using it. In short, it encompasses a lot of extra complexity, and might very well not be worth it performance wise.



ShareThis