When discussing the scalability of Web services there seems to be a tendency for some developers to overly focus on the framework being used, based on the mistaken assumption that smaller means faster, rather than considering the architecture of the application as a whole. We’ve seen several cases of developers making these assumptions before they start building their API, and either discounting Django as not being fast enough for their needs, or deciding to not use a Web API framework such as Django REST framework because they ‘need something lightweight’.
I’m going to be making the case that Web API developers who need to build high performance APIs should be focusing on a few core fundamentals of the architecture, rather than concentrating on the raw performance of the Web framework or API framework they’re using.
Using a mature, fully featured and well supported API framework gives you a huge number of benefits, and will allow you to concentrate your development on the things that really do matter, rather than expending energy reinventing the wheel.
Much of this article is Django/Python specific, but the general advice should be applicable to any language or framework.
Dispelling some mythsBefore we get started, I’d like to first counter some bits of what I believe are misguided advice that we’ve seen given to developers who are writing Web APIs that need to service thousands of requests per second.
Remember that these figures are really only intended as a rough insight into these relative timings of the various components. Views with more complex database lookups, paginated results, or write actions will all have very different characteristics. The important thing to point out is that the database lookup is the limiting factor.
In our basic un-optimized case your application will run pretty much at the same rate as any other simple Django view. We’ve not run ApacheBench against these setups, but you might reasonably expect to be able to achieve a rate of a few hundred requests per second on a decent setup. For the cached case you might be looking at closer to a few thousand requests per second.
The sweet spot on the above chart is clearly the third bar from the left. Optimising your database access is by far the most important thing to get right: after that, removing core pieces of the framework provides diminishing performance returns.
The next step: ETags and network level cachingIf performance is a serious concern for your API eventually you’ll want to stop focusing on optimizing the Web server and instead focus on getting your HTTP caching right.
The potential for HTTP caching will vary greatly across APIs, depending on the usage patterns and how much of the API is public and can be cached in a shared cache.
In our example above we could place the API behind a shared server-side cache, such as Varnish. If it’s acceptable for the user list to exhibit a few seconds lag, then we could set appropriate cache headers to that the cache would only re-validate responses from the server after a small time of expiry.
Getting the HTTP caching right and serving the API behind a shared server-side cache can result in huge performance increases, as the vast majority of your requests are handled by the cache. This depends on the API exhibiting cacheable properties such as being public, and dealing mostly with read requests, but when it is possible the gains can be huge. Caches such as Varnish can happily deal with tens or hundreds of thousands of requests per second.
You can already use Django’s default caching behavior with Django REST framework, but the upcoming REST framework 2.4.0 release is intended to include better built-in support.
Scope for improvementAs with any software there is always scope for improvement.
Although the database lookup will be the main performance concern for most REST framework APIs, there is potential for improvement in the speed of serialization. Inside the core of REST framework one other area that could be tweaked for small improvement gains is the content negotiation.
SummarySo, what’s the take home?
1. Get your ORM lookups right.
Given that database lookups are the slowest part of the view it’s tremendously important that you get your ORM queries right. Use .select_related()
and .prefetch_related()
on the .queryset
attribute of generic views where necessary. If your model instances are large, you might also consider using defer()
or only()
to partially populate the model instances.
2. Your database lookups will be the bottleneck.
If your API is struggling to cope with the number of requests it’s receiving your first code improvement should be to cache model instances appropriately.
You might want to consider using Django Cache Machine or Johnny Cache. Alternatively, write the caching behavior into the view level, so that both the lookup and the serialization are cached.
For example:
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4