logo

Personal tools
Vasudeva Documentation Articles Plone Speed Up
Document Actions

Plone Speed Up

Using Apache, make Zope as reverse Proxy (from: http://poked.org/entries/needforspeed)


One of the major points that people raise about Plone is speed. There's no denying it, if you're using Plone for the first time, it feels slow.

In the following article, all benchmarks have been performed on a 1.8GHz Intel Celeron server with 128MB of RAM. The server runs a base Redhat 7.3 (Valhalla) distribution. Zope has been compiled and installed with Python 2.3.3 from a precompiled binary RPM available from Python.org. The version of Plone used is Plone 2.0 rc3.

Out of the box, Plone is not the fastest sytem in the world. For a start, it's built using Python, which is an interpreted language, not compiled. Whilst this adds an enormous amount to the flexibility of the language, it doesn't make for the fastest applications around.

On top of this, Plone is a very complex system. Whereas a flat HTML site might only take a hundredth of a second to load from a server (discounting network conditions, obviously), Plone's main page is fully dynamically rendered for each and every request. This might mean it takes up to 5 or 6 seconds to load a page. Adding on top the fact that, including javascript and css the page size exceeds 100KB and can take more than 30 seconds to download, this can make for a pretty slow experience. Obviously, if you've got more than a couple of users on your site at once, it's going to be unusable.

That's the bad news.

However, it's not all bad. The Zope application which underlies Plone was never meant to be a web server. Fortunately, we can use Apache as a front-end web server to handle all of the tedious connections from the web through a process called reverse proxying.

To setup a reverse proxy is pretty easy. First, configure your Zope so that it's not running on port 80. You can do this by either changing your start script in Zope 2.5/2.6, or by editing the base port parameter in Zope 2.7. In the following example, the base port becomes 8000, which means that Zope will be running on port 8080.

Test this by accessing your site at http://yoursite.com:8080

Now, you need to add a Virtual Host Monster to the root folder of your Zope installation. Call it anything you want, but it must be added to the root of your Zope, and not the root of your Plone site.

You will also need to edit the portal_skins/plone_templates/global_cache_settings template to prevent Plone from sending out a Pragma: no-cache HTTP header. By default, Plone is setup to disable all HTTP caching. Simply browser to the template in your Plone site through the ZMI, and customise it into your custom skin folder. Now, edit it so that the contents are:

 <metal:cacheheaders define-macro="cacheheaders">
<metal:block tal:define="dummy python:request.RESPONSE.setHeader('Content-Type', 'text/html;;charset=%s' % charset)" />
<metal:block tal:define="dummy python:request.RESPONSE.setHeader('Content-Language', lang)" />
<metal:block tal:define="dummy python:request.RESPONSE.setHeader('Expires', 'Sat, 1 Jan 2000 00:00:00 GMT')" />
</metal:cacheheaders>

Okay, now install apache. (I use Apache 1.3, but this should work for Apache 2 with a few minor adjustments.)

Okay, so now apache should be running on port 80, so you can access a beautiful default page at http://yoursite.com

Now, you'll need to edit your apache configuration file, and there are some things you'll need to check:

  1. Ensure that mod_proxy is enabled. Search the list of loaded modules for libproxy.so, and ensure that the following lines are not commented out:
     LoadModule proxy_module modules/libproxy.so
    AddModule mod_proxy.c
  2. Setup a VirtualHost for your domain name that uses a reverse-proxy in order to pass through requests to Zope. The following example can be cut and pasted into your /etc/httpd/conf/httpd.conf file at the bottom, and you'll only need to edit a couple of lines:
     <VirtualHost *>
    # A sample VirtualHost section for using Apache as a webserver
    # instead of Zope.

    # ServerName is the url of your website.
    ServerName yoursite.com

    # Add serverAlias lines for other doamin names that should
    # point to this website. They will be rewritten by Apache to
    # the ServerName, so that anyone going to www.yoursite.com
    # will be invisibly redirected to yoursite.com in their browser.
    ServerAlias www.yoursite.com

    # ServerAdmin is your email address, which shows up on error
    # pages when Apache cannot connect to Zope.
    ServerAdmin webmaster@yoursite.com

    # The ProxyPass and ProxyPassReverse lines are the magic
    # ingredients. They rewite requests to http://yoursite.com and
    # pass the entire request rhough to Zope on
    # http://yoursite.com:8080. The VirtualHostBase ensures that
    # when the page goes back to the browser, it goes out through
    # Apache, and appears to have come from http://yoursite.com.

    # The line is made up from:
    # ProxyPass or ProxyPassReverse

    # / is the url at http://yoursite.com that you wish to use to
    # point to the Zope site. You could keep http://yoursite.com as a
    # flat HTML site in Apache, and replace / with /zope to make

    # http://yoursite/com/zope point to your zope site.

    # http://yoursite.com:8080 is the address that your zope is
    # running on.

    # /VirtualHostBase/http/yoursite.com:80 makes sure that zope
    # *thinks* it is running at http://yousite.com instead of at
    # http://yoursite.com:8080. You don't have to do anything else
    # in Zope to make this work.

    # /yourplonesite is the location of your Plone Site within Zope.
    # If you added a Plone Site into the root of your Zope with an id
    # of 'mysite', then you just change this bit to /mysite

    # /VirtualHostRoot/ makes your Plone site think it is the root of the site.

    ProxyPass / http://yoursite.com:8080/VirtualHostBase/http/yoursite.com:80/yourplonesite/VirtualHostRoot/
    ProxyPassReverse / http://yoursite.com:8080/VirtualHostBase/http/yoursite.com:80/yourplonesite/VirtualHostRoot/
    </VirtualHost>

Now restart apache, and you should find that http://yoursite.com is now your Plone website. (If you have any problems, make sure that libproxy.so is present in your /etc/httpd/modules/ directory.)

Now you've got a fairly respectable setup: Apache is serving web requests, and Zope is a backend server. Free into the bargain, you also have a complete virtual hosting setup, so you can run multiple different sites with multiple different domains on a single server with a single IP address. All you need to do is dupliate and edit that VirtualHost section for each site.

Now, you might want to make a note of how fast this setup is running. Note that, up to this point, we haven't gained any real speed advantage, we've just laid the foundations for it. In a terminal, use the Apache Benchmark application to test the speed of your site. The application is normally in '/usr/sbin/ab':

 /usr/sbin/ab -n 100 http://yousite.com/

This will time 100 consecutive requests to your server. You should note that requests all take about 0.5 to 1.0 seconds, with not a great deal of variance. Whilst this might not seem too bad for a dynamic page, remember that this is just the HTML. For a whole page with CSS, Javascript, and images, the processing time can be a lot longer.

Typical output might look like this:

 Benchmarking yoursite.com (be patient).....done
Server Software: Zope/(unreleased
Server Hostname: yoursite.com
Server Port: 80

Document Path: /
Document Length: 32560 bytes

Concurrency Level: 1
Time taken for tests: 68.901 seconds
Complete requests: 100
Failed requests: 0
Broken pipe errors: 0
Total transferred: 3293500 bytes
HTML transferred: 3256000 bytes
Requests per second: 1.45 [#/sec] (mean)
Time per request: 689.01 [ms] (mean)
Time per request: 689.01 [ms] (mean, across all concurrent requests)
Transfer rate: 47.80 [Kbytes/sec] received

So, what can we do to make it faster?

The first thing we can do is to allow Apache to cache the results of pages. This can happen because in configuring Apache to be the front-end server, we've actually created a caching reverse-proxy, meaning that all of the pages that Zope produces are now going back out to the browser through Apache, and can be cached to dramatically increase performance. It's just that we haven't told Apache to cache anything yet.

To let Apache know that we wish to cache content with a certain expiry time, mod_expires must be installed. Check your Apache modules directory (normally /etc/httpd/modules) for mod_expires.so, and then make sure that you have the following lines in your Apache configuration file:

 LoadModule expires_module modules/mod_expires.so
AddModule mod_expires.c

Now, at the end of your VirtualHost section, just before the </VirtualHost> add the following lines:

 # CacheRoot is the location on the filesystem to store files that 
# Apache caches. This directory must be created, and the user that
# Apache runs as must have full write permissions to it.
CacheRoot "/tmp/proxy/yoursite.com"

# CacheSize determines how big this cache can get in KB. It's a
# good idea that this number is about 30% less than the available
# space in the CacheRoot directory. Here we choose to cache 10MB
# of data, which is enough for a personal website, but not for
# anything larger.
CacheSize 10000

# CacheGcInterval specifies how often (in hours) to examine the
# cache and delete obsolete files.
CacheGcInterval 2

# CacheLastModifiedFactor allows the estimation of an expiry date
# for a page if it doesn't have an expiry-date specified in the
# HTTP headers returned from Zope. This is based on (time since
# last modification * CacheLastModifiedFactor), so that content
# that is ten hours old would be given an expiry date of 1 hour in
# the future.
CacheLastModifiedFactor 0.1

# CacheDefaultExpire sets a default expiry time of 1 hour into the
# future for cached pages.
CacheDefaultExpire 1

# CacheDirLength sets the number of characters used in directory
# names for subdirectories of CacheRoot
CacheDirLength 2

# The following definitions set expiry times for various content
# types. In this list, each content type defined is cached for a
# maximum period of 1 hour (3600 seconds) before it must be checked
# again. Non-listed content types are not cached.

ExpiresActive On
ExpiresByType image/gif A3600
ExpiresByType image/png A3600
ExpiresByType image/jpeg A3600
ExpiresByType text/css A3600
ExpiresByType text/javascript A3600
ExpiresByType application/x-javascript A3600
ExpiresByType text/html A3600
ExpiresByType text/xml A3600

Once you've finished adding this to the VirtualHost section, save the config file. Now, go into your /tmp folder, and create the directory defined as the CacheRoot in the configuration that you have just edited, and make it writable by the apache user. In the case of our example, this would be:

 mkdir /tmp/proxy
mkdir /tmp/proxy/yoursite.com
chown -R apache:apache /tmp/proxy/yoursite.com

Now restart Apache to ensure that these changes take effect.

Up until this point, although Apache is capable of performing caching, none of your pages are actually being cached. We can test this by using wget, and outputting the HTTP Response headers:

 wget -sS --delete-after http://yoursite.com/

--03:16:51-- http://yoursite.com/
=> `index.html'
Resolving yoursite.com... done.
Connecting to yoursite.com[127.0.0.1]:80... connected.
HTTP request sent, awaiting response...
1 HTTP/1.1 200 OK
2 Date: Mon, 19 Jan 2004 03:16:51 GMT
3 Server: Zope/(unreleased version, python 2.3.2, linux2) ZServer/1.1 Plone/2.0-RC3
4 Vary: Accept-Encoding
5 Content-Length: 32560
6 Content-Language:
7 Expires: Mon, 19 Jan 2004 04:16:52 GMT
8 Etag:
9 Cache-Control: max-age=3600
10 Content-Type: text/html;charset=utf-8
11 X-Cache: MISS from yoursite.com
12 Connection: close

Notice the X-Cache header showing a cache MISS. Whilst we expect this the first time we hit a page (as it has not yet already been cached), we would expect a HIT from subsequent requests to that page if aching is working properly.

The next step to take before Apache is useful as a cache is to set appropriate HTTP headers for your content, so that Zope can tell Apache what to cache and what not to cache. Unfortunately, due to an as-yet-unresolved bug in Zope's Accelerated HTTP Cache Manager, this is a little more complex that it should be.

The first thing you'll need to do is to edit the source of the Accelerated HTTP Cache Manager. This can be found in your Zope directory, in lib/python/Products/StandardCacheManagers/AcceleratedHTTPCacheManager.py

Modify the end of the ZCache_set function (around line number 101) so that it reads:

 # Set HTTP Expires and Cache-Control headers
seconds=self.interval
expires=rfc1123_date(time.time() + seconds)

# note that in the original, this line was commented out.
RESPONSE.setHeader('Last-Modified',rfc1123_date(time.time()))

RESPONSE.setHeader('Cache-Control', 'max-age=%d' % seconds)
RESPONSE.setHeader('Expires', expires)

This will ensure that Zope returns a valid Last-Modified HTTP header. Without it, caches will assume that your content has been dynamically generated and is not suitable for caching.

Now, restart Zope for this change to take effect.

Your system is now at the point where it is fully capable of caching content, and all that remains is to tell your Plone site's Accelerated HTTP Cache Manager what it should cache.

Login to your Zope site as a user with Manager rights, and navigate to your Plone Site in the ZMI. Click the HTTPCache, and then look at the Associate tab. Notice that no items are currently associated with it.

When associating items with the HTTPCache, you're not associating content items, instead you associate the different views available. This means that you can ensure that your view template is cached, but edit templates are not. Ensuring that the options All for Locate Cacheable Objects and All for Of the type(s), and Search Subfolders are all selected, click the Locate button. After a few moments, you should get a list of all the skin items in your site back.

Pay close attention to which items you select from this list. You can safely cache all images, all files, all CSS, and all Javascripts. In addition, you should ensure that the view templates for all of your available content types are being cached. (For example, newsitem_view for News Items.) Be especially sure to check that any items that you have customised into a different skin folder have also been selected.

Now you should have a very long list of items to cache, so just click to save your changes.

Assuming that you have done this correctly, you should now find that Apache is starting to cache your pages. We can test this by using wget, and outputting the HTTP response:

 wget -sS --delete-after http://yoursite.com/

--03:28:46-- http://yoursite.com/
=> `index.html'
Resolving yoursite.com... done.
Connecting to poked.org[217.199.181.94]:80... connected.
HTTP request sent, awaiting response...
1 HTTP/1.1 200 OK
2 Date: Mon, 19 Jan 2004 03:28:46 GMT
3 Server: Zope/(unreleased version, python 2.3.2, linux2) ZServer/1.1 Plone/2.0-RC3
4 Vary: Accept-Encoding
5 Content-Length: 32125
6 Expires: Mon, 19 Jan 2004 04:28:43 GMT
7 Last-Modified: Mon, 19 Jan 2004 03:28:43 GMT
8 Cache-Control: max-age=3600
9 Content-Type: text/html;charset=utf-8
10 Age: 4
11 X-Cache: HIT from yoursite.com
12 Connection: close

Notice that now the X-Cache header is showing a cache HIT. This is exactly the result that we want.

Now that Apache is caching the results of your Plone pages, you can test to see what kind of difference this has made. Once again, using the Apache Benchmark utility, we can test the site:

 /usr/sbin/ab -n 100 http://yoursite.com/

Benchmarking poked.org (be patient).....done
Server Software: Zope/(unreleased
Server Hostname: yoursite.com
Server Port: 80

Document Path: /
Document Length: 32125 bytes

Concurrency Level: 1
Time taken for tests: 0.116 seconds
Complete requests: 100
Failed requests: 0
Broken pipe errors: 0
Total transferred: 3252200 bytes
HTML transferred: 3212500 bytes
Requests per second: 862.07 [#/sec] (mean)
Time per request: 1.16 [ms] (mean)
Time per request: 1.16 [ms] (mean, across all concurrent requests)
Transfer rate: 28036.21 [Kbytes/sec] received

Note particulary the time taken for this test: 0.116 seconds, or a much healtier 0.0016 seconds per request! This gives some indication of how fast this site will be capable of running.

Using Apache Benchmark to conduct 100 consecutive requests is hardly indicative of the true speed at which your site is running, however. To put this setup through its paces, try using the concurrency feature, which allows you to run requests simultaneously. In this example, ab runs 100000 requests with 100 request concurrency. This is the equivalent of an entireprise-level site under medium to heavy load:

 /usr/sbin/ab -n 100000 -c 100 http://yoursite.com/

Benchmarking poked.org (be patient)
Server Software: Zope/(unreleased
Server Hostname: poked.org
Server Port: 80

Document Path: /
Document Length: 32125 bytes

Concurrency Level: 100
Time taken for tests: 115.732 seconds
Complete requests: 100000
Failed requests: 0
Broken pipe errors: 0
Total transferred: -1041856680 bytes
HTML transferred: -1081567796 bytes
Requests per second: 864.07 [#/sec] (mean)
Time per request: 115.73 [ms] (mean)
Time per request: 1.16 [ms] (mean, across all concurrent requests)
Transfer rate: -9002.32 [Kbytes/sec] received

These results show that the combination of Apache and Zope is capable of serving 100000 requests in just under 2 minutes. It's worth pointing out again that this is on a 1.8GHz Intel Celeron with only 128MB of RAM, running a basic Redhat 7.3 setup, which is very, very slow for a production server. On a typical Dual Intel Xeon 2.4GHz server with 1GB of RAM, expect a speed increase of over 100%.

It is worth pointing out that there is are limitations to this approach to caching. In setting the Last-Modified header to the time of the page being rendered, and setting the max-age to 3600 seconds, all content on the site has a lifetime of 1 hour. This means that in the worst-case scenario, an update to the site could take an hour to appear. This can be resolved by forcing a cache invalidation for a particular page, which can be done by simply CTRL-refeshing the page in most browsers.

Another limitation is that, if you are using your main domain name to login and edit your Plone site, you may experience some strange behaviour in browsers that do not properly respect caching headers (such as Internet Explorer). An easy way to work around this is to add a new subdomain for users who are editing your site, such as cms.yoursite.com. Then, in your Apache config file, simply add a new VirtualHost section for your CMS view, without any of the caching directives:

 <VirtualHost *>
ServerName cms.yoursite.com
ServerAdmin webmaster@yoursite.com
ProxyPass / http://cms.yoursite.com:8080/VirtualHostBase/http/cms.yoursite.com:80/yourplonesite/VirtualHostRoot/
ProxyPassReverse / http://cms.yoursite.com:8080/VirtualHostBase/http/cms.yoursite.com:80/yourplonesite/VirtualHostRoot/
</VirtualHost>

Thus, anyone who needs to edit the site will bypass the caching entirely.

Created by achilles
Last modified 2004-01-22 03:04
You can add comments to this item if you log in.

Which files to cache

Posted by dthomas218 at 2004-01-19 05:12
Thanks for your great write-up on caching with Apache-Plone combo.

"Pay close attention to which items are selected from this list."
Which files should you make sure are NOT cached? In this long list, how do you tell which files are which? Can you Select All and then deselect a few?

Thanks again! Dan

mod_expires

Posted by martinb at 2004-01-19 02:43
You probably want to document that mod_expires needs to be loaded and added.

Also, I'm not getting the X-Cache header out of wget. This is what's happening::

1 HTTP/1.0 200 OK
2 Server: Zope/(unreleased version, python 2.3.2, darwin) ZServer/1.1 Plone/2.0-RC3
3 Date: Mon, 19 Jan 2004 14:12:36 GMT
4 Content-Length: 25976
5 Content-Language:
6 Expires: Mon, 19 Jan 2004 15:12:36 GMT
7 Last-Modified: Mon, 19 Jan 2004 14:12:36 GMT
8 Etag:
9 Pragma: no-cache
10 Cache-Control: max-age=3600
11 Content-Type: text/html;charset=utf-8

 
page created by admin last modified 2004-07-30 11:53