We can easily enable HTTP caching on our app server by using the built-in nginx fastcgi cache. It’s pretty simple to do, just add the following to your nginx server configuration.
1 # Set the path where the cache is stored; Set the zone name (my_app), 2 # total size (100m), and max lifetime (60m)
3 fastcgi_cache_path /tmp/cache levels=1:2 keys_zone=my_app:100m inactive=60m; 4
5 # Set the cache key used, in this case: httpsGETtest.com/somepage.html 6 fastcgi_cache_key "$scheme$request_method$host$request_uri"; 7 8 server { 9 listen 80; 10 root /u/apps/my_app; 11 12 location ~ \.php$ { 13 fastcgi_pass http://127.0.0.1:8080; 14
15 # Use the zone name we defined on line 1 16 fastcgi_cache my_app;
17
18 # Only cache HTTP 200 responses (no error pages) 19 fastcgi_cache_valid 200 60m;
20
21 # Only enable for GET and HEAD requests (no POSTs) 22 fastcgi_cache_methods GET HEAD;
23
24 # Bypass the cache if the request contains HTTP Authorization 25 fastcgi_cache_bypass $http_authorization;
26
27 # Bypass the cache if the response contains HTTP Authorization 28 fastcgi_no_cache $http_authorization;
29
30 # Add a debugging header to show if the page came from cache 31 add_header X-Fastcgi-Cache $upstream_cache_status;
32 } 33 }
Case Study: HTTP Caching and the Nginx Fastcgi Cache 171 This configuration sets up a fastcgi cache infront of our PHP-FPM server. We define the location of the cached files withfastcgi_cache_path, setting a 100MB cache limit, with an automatic 60 minute purge for inactive items.
The cache key is defined withfastcgi_cache_key— this can be changed depending on your use- case. For instance, notice how$schemeis included in the cache key? The$schemevariable holds the HTTP scheme (i.e, http or https), which by default will cache the pages differently depending on if they are accessed over SSL. This may not be what you want. An example cache key would look something likehttpsGETmy_app.com/index.php?foo=bar.
Next up, we define what types of HTTP responses are valid withfastcgi_cache_valid— just 200 responses. We don’t want to cache error pages. Likewise, withfastcgi_cache_methods, we limit caching to onlyGETandHEADrequests. Generally, it doesn’t make sense to cache the response of aPOST, which is more often than not going to have dynamically generated content.
Lastly, we put in some definitions forfastcgi_cache_bypassandfastcgi_no_cache. These two settings allow you to either bypass checking the cache or skip storing something in the cache, depending on the presence of an aribitrary cookie, header, or nginx variable. In this case, we are skipping the cache if the HTTP HeaderAuthorizationis present.
Remember, though— nginx will respect and obey yourExpiresand Cache-Controlheaders. If a page is sent from PHP-FPM with theCache-Control: private; no-storeheader set, it will not be stored in the nginx Cache.
Notice we also added a dynamic header,X-Fastcgi-Cache, which is really helpful for
debugging. This header will displayHITif the file was served from the cache andMISS
if it was not and was served from PHP. You can log this in youraccess_logand use it
to calculate cache hit ratios.
PHP-FPM with and without a proxy cache
I quickly benchmarked this on an i2.8xlarge EC2 server really simply— I setup a blank laravel application (no database), with one page (/public) coded to always return Cache-Control: public and the other (/private) setup to always return Cache-Control: private. I ran ab (Apache Benchmark) and benchmarked 100,000 requests— here are the number of requests per second:
1 /public - 24690 requests per second, 4ms per request 2 /private - 3253 requests per second, 30ms per request
The takeaway here is— it is much, much, much faster to skip PHP and serve files directly from an HTTP cache if at all possible. The results are incredibly visible with the most basic of PHP scripts, and we’d see an even more substantial difference if the PHP code was doing more complex work like hitting the database multiple times. That being said, sometimes, it’s just not possible to do if the data is too dynamic. But for the pages where it does make sense— boy this can be a huge win.
Case Study: HTTP Caching and the Nginx Fastcgi Cache 172
**How is the fastcgi cache stored on disk?
The cached responses that the nginx fastcgi cache saves are stored on disk and can be easily manipulated. The output of the response from PHP-FPM is captured and stored in fastcgi_cache_path (/tmp/cache in our example). The
filename is predictable and derived from the derived cache key of the re- quest. For instance, for a GET request to https://domain.com/index.html, the
derived cache key is httpsGETdomain.com/index.html. The response body will
be stored in a file that is named md5(httpsGETdomain.com/index.html), or d5c94ba8b944742f64c115fa8c8e65ea.
The cache files aren’t just stored at the base of/tmp/cache, though. Nginx uses a two
level folder structure to avoid having millions of files in a single directory which is bad for performance. The last three characters of the filename are used to build the folder structure, so for the example request above, the response body will be stored in/tmp/cache/a/5e/d5c94ba8b944742f64c115fa8c8e65ea. Why is this useful? If you
delete this file, it will remove it from the fastcgi cache and force nginx to re-pull the page next time it’s requested.