Zend Framework利用Zend_Cache生成页面缓存(页面静态化)

One of the things I’m always looking for is ways to improve performance with the applications I write. While a few applications are write-heavy, most are read-heavy: that is, reading the database is the predominant behavior (for example, this WordPress blog reads the database far more often than it writes to the database). Additionally, Zend Framework is (comparatively) slow at handling requests, offering a throughput of about 67 requests per second on my machine, while loading static pages came in at a whopping 750 requests per second.*

So, given this performance difference, how do we improve the performance of Zend Framework while still retaining its functionality and ease-of-use? Well, we employ caching, of course!

But not just any caching. One of the beauties of a read-heavy website, especially one that doesn’t change all that often, is that we have the ability to cache entire pages and serve them directly using our web server. In Zend Framework 1.10.0, Zend_Cache_Frontend_Capture and Zend_Cache_Backend_Static were introduced, giving us the ability to take entire pages produced by Zend Framework and cache them. This means that we get the ability to use Zend Framework and all of its framework-y goodness, while still having the ability to enjoy the performance of static HTML pages served by our webserver. Excellent.

When devising my proof of concept, however, I found that implementing these components is more difficult than it looks. This is in part because the documentation is lacking, and also in part that the documentation in some spots is wrong. But after a week of searching and a journey that consisted of reading a Jira ticket, filing one of my own, dealing with imperfect documentation, asking questions in #zftalk on Freenode, bugging Matthew Weier O’Phinney to the point where I’m sure he made a voodoo doll of me, bugging Pádraic Brady about the cache, and good old-fashioned trial and error, I’ve mastered the implementation of static whole-page caching in Zend Framework, and here is a tutorial of how to do it yourself.

Standard Disclaimer
This tutorial implements code found in the latest version of Zend Framework at the time it was written. That means Zend Framework 1.10.3. Future releases of Zend Framework may, from time to time, change the behavior of components discussed here. Any changes should be reviewed in the documentation.

Additionally, where caching is concerned, it’s never a good idea to cache authenticated pages. It’s also never a good idea to cache data that changes a lot. Finally, it’s never a good idea to cache pages that change based on inputs, like pages that you access via POST or PUT requests.

Getting Started
First things first: let’s talk a little bit about Zend Framework’s caching model and how Zend_Cache_Frontend_Capture and Zend_Cache_Backend_Static are different.

With most Zend caches, you can implement them using the factory() method – in fact, the documentation warns against doing it any other way. So, to implement a frontend file cache using an APC backend, you can do the following:
Zend_Cache::factory(‘File’, ‘APC’, $frontOps, $backOps);
With the implementation of the Zend_Cache_Manager in 1.10, you can register your cache with the manager, and then access it directly from your controllers. However, if you try to implement the Zend_Cache_Frontend_Capture or Zend_Cache_Backend_Static caches in this fashion, it blows up entirely, and will ruin your day. This is because these caches (collectively known as the Static Cache) are designed to serve files directly from the webserver once the file is cached; this means two things in particular: first, the static cache’s ID is the request URI (which in turn is turned into hexadecimal to comply with Zend_Cache’s rules on IDs), and second, because in order to capture the data, the cache uses output buffering.

Therefore, implementation of the static cache is done through the application.ini file, as a resource plugin. Developers wishing to implement this cache must include the following lines in their application.ini files:
; Custom Caches (Adjustments To Default CacheManager)

resources.cacheManager.page.backend.options.public_dir = APPLICATION_PATH “/../public/cached”
resources.cacheManager.pagetag.backend.options.cache_dir = APPLICATION_PATH “/../data/cache/tags”
This means that all of the cached data is stored in the public/cached directory; all the cache’s tags are stored in data/cache/tags. These paths are by no means the only paths, but you must specify a path for both in order for the cache to work properly.

Why do you need to specify a separate tags directory? Due to the fact that files are served directly off the web server, rather than through PHP, the tags are stored separately in another cache. This defaults to a file cache, and you must specify another location for the files to be stored. The static cache utilizes an internal cache which is transparent to you in every other way.

There is one additional INI setting we must employ. In order to operate properly, the static cache employs output buffering and captures that output, writing it to disk and then serving it to the end user. Zend Framework also employs output buffering, which if not turned off, will interfere with the static cache. This was a hangup for me, since it’s not mentioned anywhere, and was something I discovered quite by accident. In order to turn off Zend Framework’s standard output buffering, we need to include the following INI directive:
resources.frontController.params.disableOutputBuffering = true
This directs the front controller to turn off output buffering, which allows the static cache to handle it.

The last thing we need to do is create the directories where the cache will store its files, and make them owned by the web server user. While the static cache will create its own directory to store the cached static files (if it doesn’t exist), the file cache will throw an exception.

At this point, the file cache is ready to go. It’s configured, we’ve created the directories, we’ve turned off output buffering, and we’re not ready to get into caching files.

Caching Output

Zend Framework now has a built in cache helper which we’ll use to cache our static content. This needs to be done in the init() method (from my tests), and should list all the actions on the page you want to cache. Your controller should look similar to this:
< ?php class IndexController extends Zend_Controller_Action { public function init() { $this->_helper->cache(array(‘index’), array(‘indexaction’));
$this->_helper->cache(array(‘viewpage’), array(‘viewpageaction’));

public function indexAction()

public function viewpageAction()

public function logoutAction()
The argument list for the cache plugin is simple: first, an array of the actions we’re caching, followed by an array of the tags associated with those actions. I’ve listed index and viewpage separately, with different tags, but you can tag multiple actions with the same tags, or break it out as I have. As you develop your application, you’ll want to be careful to not cache actions that are being executed on a POST request, which you can do by using the request object’s isPost() method. Also, in this example, logoutAction() is never cached; this is because we obviously don’t want to cache the results of a log out; we actually want PHP to unset the user’s identity.

Occasionally you may wish to invalidate the cache and remove old files. To do so, you search by tag. For this example, let’s purge the “indexaction” tagged files from the cache:
The “indexaction” tagged pages will be invalidated and re-cached on the next request.

Directing Apache To Serve Cached Files
The whole point of this process is to serve the cached files at a significant performance improvement, so now we need to make some edits to our .htaccess file’s rewrite rules. The documentation’s rules are slightly incorrect, so let’s devise our own scheme.

My read-heavy sites are usually fairly simple, serving static HTML files rather than XML, OPML, or JSON. Therefore, I need only have a rule for HTML. Additionally, I want to make sure the web server only serves cached files on GET requests, so I’ll include a rewrite condition to help with that.
RewriteCond %{DOCUMENT_ROOT}/cached/index.html -f
RewriteRule ^/*$ cached/index.html [L]

RewriteCond %{DOCUMENT_ROOT}/cached/%{REQUEST_URI}\.html -f
RewriteRule .* cached/%{REQUEST_URI}\.html [L]

RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ – [NC,L]
RewriteRule ^.*$ index.php [NC,L]
These rules do a few things: first, if the request goes to the index controller with no arguments (www.example.com/) then it tries to load the index.html cached file. Second, if there are arguments it tries to load the cached file based on the request URI. Finally, for anyone used to using Zend Framework, the last five lines are the same five lines we start with in our Zend Framework default project; these we leave alone to ensure that we run the application if there is nothing in our cache to serve.

Final Notes On Caching
Now that we’ve gotten the cache set up and verified that it works, we can develop our application. However, we obviously want to avoid caching during development, so we can turn caching off by adding the following to our [development : production] section of application.ini:
resources.cacheManager.page.backend.options.disable_caching = true
And that’s it! We can now develop our application with full page caching, getting the performance of a static web server and the flexibility of Zend Framework in the same package.

Good luck!

* The benchmarks cited were performed in the following way: I used Apache Bench, with 3000 requests (none concurrently), to test a stock Zend Framework project, and a flat file of the stock index.php of a Zend Framework project. Both times the files were named with the PHP extension. I did not benchmark the performance of any Zend components. The tests were executed on the same server as the webserver. Apache was restarted after each test. As usual, the standard disclaimers apply and the point of these benchmarks is simply to illustrate a well-known fact: flat HTML files serve faster than parsed PHP code.