Sun 5 Aug 2007
Using Nginx, SSI and Memcache to Make Your Web Applications Faster
Posted by Scoundrel under Admin-tips , Databases , DevelopmentIf you’d take a look at any web site, you will notice, that almost all of the pages on this given site are pretty static in their nature. Or course, this site could have some dynamic elements like login field or link in the header, some customized menu elements and some other things… But entire page could be considered static in many cases.
When I started thinking about my sites from this point of view, I understood, how great it would be to be able to cache entire page somewhere (in memcache for example) and be able to send it to the user without any requests to my applications, which are pretty slow (comparing to memcache
) in content generation. Then I came up with a pretty simple and really powerful idea I’ll describe in this article. An idea of caching entire pages of the site and using my application only to generate small partials of the page. This idea allows me to handle hundreds of queries with one server running pretty slow (yeah! it is slow even after all optimizations on MySQL side and lots of tweaks in site’s code) Ruby on Rails application. Of course, the idea is kind of universal and could be used with any back-end languages, technologies or frameworks because all of them are slower then memcache in content “generation”.
So, first of all, let me describe tools I use in my solution (but it does not mean, that you must use the same software - it is just for example):
- Memcached - for handling all requests to cached information without generation on every request
- Nginx (with SSI enabled) - for handling all HTTP requests to my site and retrieving information from memcache or from my application back-end.
- Ruby on Rails - as an example of backend application (could be Python, PHP, Perl, Java, etc).
And now, let me describe generic request handling process with all these tools together. Let’s imagine some user hits your site’s page. Browser sends a request to your web-server. Web server has nginx installed there (frontend) and Rails-based application deployed like described in one of my previous posts or in other people’s posts on the net (nginx+mongrel). So, nginx proxies your request to one of mongrel backend instances and mongrel serves the request.
This was generic process of request handling with 2-tier web server deployment (frontend + backend). In this case all your requests would be handled by mongrel and Rails which is pretty slow. So, we need to lighten up this process by removing unnecessary work from Rails application. First of all, we need to change nginx configuration to let it server pages from memcache and ask backend mongrels only of some request result has not been cached yet:
upstream mongrel {
server 127.0.0.1:8150;
server 127.0.0.1:8151;
server 127.0.0.1:8152;
server 127.0.0.1:8153;
}
# Defining web server
server {
listen 216.86.155.55:80;
server_name domain.tld;
# All dynamic requests will go here
location / {
default_type text/html;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
# All POST requests go to mongrel directly
if ($request_method = POST) {
proxy_pass http://mongrel;
break;
}
# Say nginx to try to fetch some key from memcache: "yourproject:Action:$uri", like ""yourproject:Action:/posts/1"
set $memcached_key "yourproject:Action:$uri";
memcached_pass localhost:11211;
proxy_intercept_errors on;
# If no info would be found in memcache or memecache would be dead, go to /fallback location
error_page 404 502 = /fallback$uri;
}
# This location would be called only if main location failed to serve request
location /fallback/ {
# This means, that we can't get to this location from outside - only by internal redirect
internal;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
# Pass request to mongrel
proxy_pass http://mongrel/;
}
# Some static location to serve directly w/o bothering backend
# (here should be more of such static paths or one regex-based location)
location /images/ {
root /rails/lazygeeks/public/current/public;
}
}
Notice: This config is not optimal, but it shows us what we’re going to do here.
So, what do can we see above: nginx tries to fetch a page from memcache first using a key like “yourproject:Action:/some/uri“, then, it this approach failed (no page in cache, or cache is dead), it sends a request to the backend server.
“But how pages would appear in the cache?“, you can ask. And you’d be right - we need to put them there manually from our application when first request comes to us and then all other requests to this specific URI will be served from our cache (I’d left this task for my readers to implement because or you can take a look to Dmytro Shteflyuk’s blog - he is going to post some information about how it could be done).
“But what if our page would be changed a bit later?” - of course, you’d know when it happened and you’d remove a page from he cache ;-). For example, you’d decide to implement this scheme for your /post/* URIs in your ultra-popular blog. Then you’ll need to remove a key “mycoolblog:Action:/post/X” from the cache when something changes on this page (like comment added or page edited).
And last, most interesting question you’d ask: “What if we have absolutely dynamic parts on our pages? Login field in the top part of the pages and ‘comment’ form for logged in users“. To solve this problem, we need to do following:
- Separate this small page partials generation from the main page logic to a separate URIs like /dynamic/login_field and /dynamic/comment_form.
- Add option “ssi on” to both locations in your nginx config: “location /” and “location /fallback” to ask nginx for SSI support in your proxied responses from mongrel and Rails
- Add one more location you your config (not mandatory, but it would work faster):
# This location would be called only from SSI tags
location /dynamic {
# This means, that we can't get to this location from outside - only by internal redirect
internal;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
# Pass request to mongrel
proxy_pass http://mongrel;
} - Replace dynamic parts in your templates with
<!--# include virtual="full_url_to_your_partial" -->
where full_url_to_your_partial is something like http://domain.tld/dynamic/login_field.
So, what we’ll get with all these changes? Now, when we have SSI enabled in our nginx service and we’ve replaced out dynamic partials with an appropriate SSI-tags, nginx will get our pages from backend or from cache and will find these SSI tags there. After this it will ask backend processes (with parallel queries, which is really great for scalability) for these small partials. Your application will be able to spend really small amount of resources to generate tiny partials without any complicated logic there. When nginx will get all info it needs, it will compose final page and send it to a user. That’s it! Your site becomes really fast and could serve tons of requests where before this modification it’d kill your brand new N-CPU server with M Gb of RAM
If you’ll try to implement this schema, you’ll definitely come to an idea of putting all this stuff to some rails plugin and just using some ssi_include some_url helper to output your partials and some simple code to mark, which actions should be cached and which shouldn’t. And, of course, you’ll understand, that you can do all this stuff even without nginx and SSI and without Rails - you can ask for these partials using AJAX requests on clients’ side or even compose both these approaches to fallback to AJAX when you have no SSI or switching between them with simple configuration setting. But anyways, this approach worth a try because it gives you huge performance boost with a pretty small cost of implementation.
- Dog-pile Effect and How to Avoid it with Ruby on Rails memcache-client Patch
- Compiling nginx in RedHat Linux: PCRE library problem
- FastSessions Rails Plugin Released
- Using X-Accel-Redirect Header With Nginx to Implement Controlled Downloads (with rails and php examples)
- Flash Video (FLV) Streaming With Nginx
2007-08-06 at 12.45 pm
This is very similar in concept to ESI. I’d guess the only advantage you might get from using ESI instead of SSI is akami/oracle support, but with the right level of abstraction e.g. the rails helper you mentioned you can easily switch the underlying syntax.
Hitting memcache directly from the proxy server is really a fantastic optimization.
2007-08-06 at 1.27 pm
What version of nginx are you using?
2007-08-06 at 11.58 pm
I don’t remember what version I use
AFAIR, it is something like 0.5.X (stable branch).
2007-08-07 at 12.19 am
[...] Using Nginx, SSI and Memcache to Make Your Web Applications Faster (tags: web nginx memcached tuning sysadmin) [...]
2007-08-19 at 7.55 am
Brilliant, thanks for a clear and detailed explanation.
2007-08-19 at 9.59 am
The idea is brilliant!
2007-08-20 at 9.50 pm
Great writeup. This is awesome to play with, although it reveals how far behind the version of nginx in apt-get is…
I am seeing one strange thing though using this. For some reason, Nginx is seemingly prepending the characters “?! to the value in memcached (I got the value directly from memcached and they don’t seem to be in there). Have you seen this as well or is it just me? I guess I’ll check if there is some weird encoding issue going on…
2007-09-02 at 6.22 am
What about Last Modified, ETag etc. and cache control?
2007-09-05 at 4.02 pm
[...] recently the idea has been catching on. First with Components Are the New Black, and the idea of Nginx, SSI, and Memcache, what I’d like to briefly describe below is a partial page caching strategy that has worked [...]
2007-09-05 at 10.45 pm
Можно перевод сделать, и опубликовать? На http://habrhabr.ru , например.
2007-11-18 at 10.22 pm
Did you try to measure the performance boost?
2007-12-07 at 6.19 am
[...] December 7, 2007 by honewatson You can squeeze some pretty mean juice out of your server with this Nginx, Server Side Includes SSI and Memcache set up described by Kovyrin. [...]
2007-12-27 at 11.17 pm
[...] Using Nginx, SSI and Memcache to Make Your Web Applications Faster :: Homo-Adminus Blog by Alexey Ko… (tags: nginx memcached performance SSI caching Rails web scalability RubyonRails) [...]
2008-02-01 at 3.29 am
Great post. Really helping me squeeze the most out of my cluster.
One problem though.
As a test I’m doing:
CACHE.set(’Action:/index’,render_to_string :action=>:index)
when nginx serves the page from memcached its adding
{{
”b
}}
to the beginning of my cached pages.
Any insight into what is causing this?
Thanks for your great blog,
Dan
2008-02-08 at 11.52 am
we hit the same issue as Dan - investigating we figured out it was ruby’s marshaling of the string included a few extra bytes at the begining.
By doing set(key, text, 60, true) # you get it cached for 60 seconds in raw format
2008-02-14 at 12.39 am
Thanks Jesse your my hero.
2008-02-22 at 8.38 am
[...] http://blog.kovyrin.net/2007/08/05/using-nginx-ssi…; [...]
2008-02-22 at 8.49 am
This article is really great. It was inspiration for me to *finally* implement page caching myself using nginx and SSI. What a difference!
I actually took your ideas and extended into a slightly obsure / CRUD backwater: form POSTS. My URL architecture was such that I have form POSTS and Ajax requests going to the same URL’s as the GET requests that are supposed to be page cached.
So, I implemented a little doo-dad in nginx.conf to by-pass the page cache on form POSTS. I think this is a smart addition to any caching strategy (since in general POST’s should not be cached from a semantic perspective - a POST is the opposite of a cache hit right?)
Anyway I wrote some more detail of this up on my blog, in case you’re interested:
http://www.misuse.org/science/2008/02/22/rails-page-caching-nginx-ssi-ajax-and-form-posts/#more-118
Again - thanks for taking the first big step in this direction. Very helpful! - Science
2008-02-29 at 9.35 am
[...] Using Nginx, SSI and Memcache to Make Your Web Applications Faster :: Homo-Adminus Blog by Alexey Ko… (tags: rails nginx performance memcached) [...]
2008-06-03 at 6.39 pm
[...] [...]