Recently we’ve been working with one of our clients to build application for use with AppNexus. We were faced with a challenge which required a bunch of different technologies to all come together and work together. Below I’ll try to list out how we approached it and what additional challenges we faced.
First came the obvious challenge: How to handle at least 25,000 requests per second. Our usual language of choice is PHP and knew it was not a good candidate for the project. Instead we wanted to do some benchmarks on a number of other other languages and frameworks. We looked at Rusty/Nginx/Lua, Go, Scala, and Java. After some testing it appeared that Java was the best bet for us. We initially loaded up Jetty. We knew that this had a bit more baked in than we needed, but it was also the quickest way to get up and running and could be migrated away from fairly easily. The idea overall was to keep the parsing of the request logic separate from the business logic. In our initial tests we were able to get around 20,000 requests a second using Jetty, which was good, but we wanted better.
Jetty was great at breaking down the incoming HTTP requests to easily work with, it even provided an out of the box general statistics package. However, we didn’t need much heavy lifting on the HTTP side, what we were building required very little complexity on with regards to HTTP protocol. Jetty in the end was spending too many CPU cycles for what we needed. We looked to Netty next.
Netty out of the box is not as friendly as Jetty as it is much lower level. That said, it wasn’t too much work to get Netty up and running responding to HTTP request. We ported over most of the business logic from our Jetty code and were off to the races. We did have to add our own statistics layer as Netty didn’t have an embedded one for what we were looking for. After some fine tuning with Netty we were able to start to handle over 40,000 requests per second. This part of the puzzle was solved.
On our DB side we had heard great things about Aerospike in terms of performance and some of its features. We ended up using this on the backend. When we query Aerospike we have the timeout set at 3ms. We’ll get around one or two request timeouts per second, or about 0.0025% of the time we’ll timeout, not too shabby. One of the nice features of Aerospike is the XDR function of the enterprise version. With this we can have multiple Aerospike clusters which all stay in sync from a master cluster. This lets us load our data onto one machine, which isn’t handling all the requests, and then it is replicated to the machines which are handling all the requests.
All in all we’ve had a great experience with the Netty and Aerospike integration. We’re able to consistently handle around 40,000 requests a second with the average response time (including network time) of 4ms.
Recently we’ve been working on a new project that requires caching of both views and database queries. One of the problems I came across I wanted to Result Cache an query I was using for a pager. This caused a couple of problems, one being I needed to be able to clear the cache by its prefix so we would never have a stale cache. Doctrine has a built in deleteByPrefix call for this, however on a pager how do I get it so that it will use a result cache, but still use different indexes for different pages? The following code would not work:
Well here the problem is everything is being cached as as the ‘comment_index’ cache, so if you passed that query to a pager, and told it to be on the second page, it’d see the ‘comment_index’ cache exists, and use that. A simple way around this is:
// Query build... ->useResultCache(true,sfConfig::get('app_comment_cache'),'comment_index_'.$page);
In this example page is the parameter you are passing the query and the Doctrine pager to tell it what page cache to look at.
Then a very weird problem was occurring, I was getting more queries if I USED the cache than if I didn’t. Very weird. It seemed that one of the joins object did not seem to be getting stored in the cache. The join looked something like this:
The problem was the profile object was not getting stored in the result cache and thus causing a query each time it was called from the user object. After much hunting around, a long time in #doctrine, and a few leads from a couple of people, it turns out, by default, Doctrine will only serialize the immediate relation to the main object (in this case ‘sc’). However, you can make it so that it will serialize objects further down the line by overriding the function serializeReferences to return true in the class you want to serialize references from. In my example this is the User class. Since our application will never only need the ‘User’ class to be serialized on a result cache I completely overrode the function and made it always return true
Of course you can set this on a per object instance via $user->serializeReferences(true). Overriding the method the way I did you need to be careful as you could potentially waste a ton of storage space in your result cache.
Hope this saves someone some head banging and confusion on how using a cache could actually cause more queries if not stored properly.
Posted In: Doctrine