10 Minutes to a Leaner LAMP stack

For this quick article I am only going to quickly cover the A and the M of the LAMP stack.

The Setup

Assumptions: You have root on the server in question or you at least have enough server access to effect the configuration of Apache and MySQL.

The test server setup:

  • A Virtual Private server running on Rackspace cloud
  • Ubuntu 9.04 (Jaunty Jackalope)
Few quick commands and viola, you have a LAMP server...
$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo locale-gen en_US.UTF-8
$ sudo apt-get install apache2-mpm-prefork php5 php5-dev mysql-server-5.0 mysql-client-5.0 php5-mysql
$ sudo a2enmod rewrite
  • Installed latest Wordpress and added some test posts

Baseline Performance:

Before you start changing anything be sure to record the baseline performance ot the server.  Place the server under load.  I used http_load but anything similar can be used

The Test URL's that http_load will draw from

$ cat pdxphp-test
http://67.23.47.195/
http://67.23.47.195/2010/02/test-post-1/
http://67.23.47.195/2010/02/test-post-2/
http://67.23.47.195/2010/02/test-post-3/
http://67.23.47.195/2010/02/test-post-4/
http://67.23.47.195/2010/02/test-post-5/

Run the test

$ http_load -parallel 5 -seconds 20 pdxphp-test
83 fetches, 5 max parallel, 1.74373e+06 bytes, in 20.0009 seconds
21008.8 mean bytes/connection
4.14982 fetches/sec, 87182.7 bytes/sec
msecs/connect: 80.6013 mean, 99.26 max, 71.974 min
msecs/first-response: 795.394 mean, 5495.6 max, 225.29 min
HTTP response codes:
  code 200 -- 82
[caption id="attachment_318" align="alignnone" width="661" caption="web server under load"]
[/caption] To see how much RAM MySQL actually wants, we restart it with Apache stopped [caption id="attachment_315" align="alignnone" width="641" caption="MySQL running on it's own, gobbles up 21M RAM"]
[/caption]

Apache

Reduce the number of modules loaded

You can start with this.  It will typically not give you incredible results but you will save some resource and it is a good security policy. You can list your loaded modules with
apache2ctl -t -D DUMP_MODULES
Loaded Modules:
 core_module (static)
 log_config_module (static)
 logio_module (static)
 mpm_prefork_module (static)
 http_module (static)
 so_module (static)
 alias_module (shared)
 auth_basic_module (shared)
 authn_file_module (shared)
 authz_default_module (shared)
 authz_groupfile_module (shared)
 authz_host_module (shared)
 authz_user_module (shared)
 autoindex_module (shared)
 cgi_module (shared)
 deflate_module (shared)
 dir_module (shared)
 env_module (shared)
 mime_module (shared)
 negotiation_module (shared)
 php5_module (shared)
 rewrite_module (shared)
 setenvif_module (shared)
 status_module (shared)
Choosing to unload any of these modules is completely dependent of how you are using the server.  But for many PHP developers can safely unload cgi.
$ sudo a2dismod cgi
Module cgi disabled.
Run '/etc/init.d/apache2 restart' to activate new configuration!
sam@pdx-test:/etc/apache2/mods-enabled$ sudo apache2ctl start

Turn off htaccess

Explanation from Apache.  So all you need to do is take the contents of the .htaccess file in your site's webroot and place that in the corresponding vhost.  Then turn off htaccess files with AllowOverride None and rm the .htaccess file in the web root after restarting apache.
sudo vi /etc/apache/sites-enabled/wordpress
<Directory /var/www/wordpress> <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] </IfModule> AllowOverride None </Directory>
rm /var/www/wordpress/.htaccess

Now, how to really affect apache

This is all well and good and Apache will be better off for it but if you had a server that was paging and dying under load, after making these changes, that will probably not change. The bigger issue shown in the top output above is that, for the amount of physical RAM, there are just too many 20+MB processes the apache conf file controls these settings (/etc/apache2/apache2.conf on Ubuntu) The stock settings:
<IfModule mpm_prefork_module>
    StartServers          5
    MinSpareServers       5
    MaxSpareServers      10
    MaxClients          150
    MaxRequestsPerChild   0
</IfModule>
Possible more appropriate settings for a little personal blog site.  Overall you need to lower MaxClients to the amount you can run without paging.  If this is too few, you probably need more RAM.
<IfModule mpm_prefork_module>
    StartServers          1
    MinSpareServers       1
    MaxSpareServers       5
    MaxClients            5
    MaxRequestsPerChild   1000
</IfModule>

The Results thus far

[caption id="attachment_332" align="alignnone" width="651" caption="Top output with Apache Trimmed and under load"]
[/caption]

MySQL

Now onto MySQL.  We saw above that MySQL is staring up and grabbing ~21M of RAM.  A low to medium volume LAMP server probably doesn't need all that but, again every situation is different.  Here though are some notes of how to tune the Low Hanging Fruit of MySQL

If you don't use it, Turn it off

Same as with Apache modules, turn off what you do not need.  Storage engines are a great place to start.  Very few frameworks (including WP),  utilize InnoDB by default.  Most use good old MyISAM. Stopping MySQl from loading the InnoDb engine (and its predecessor:bdb) is quite simple, the lines are in my.cnf, just uncomment them
skip-innodb
skip-bdb
Now there are many, many MySQL config options that affect the amount of resource it uses.  I'll just cover a couple here, the key buffer and the query cache The stock settings for these are
# used for holding built indexes
key_buffer              = 16M
# largest query that is cache-able
query_cache_limit       = 1M
# used for caching queries
query_cache_size        = 16M
For many types of sites,  these can be a bit large,  for small sites, some suggested settings might be:
# used for holding built indexes
key_buffer                = 4M
# largest query that is cache-able
query_cache_limit       = 500K
# used for caching queries
query_cache_size        = 8M
To see what the runtime values are concerning the query cache and key buffer use (once server has been under load)
mysql> SHOW STATUS WHERE Variable_name LIKE 'Key_blocks_%' OR Variable_name LIKE 'Qcache_%';
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Key_blocks_not_flushed  | 0        |
| Key_blocks_unused       | 13369    |
| Key_blocks_used         | 27       |
| Qcache_free_blocks      | 4        |
| Qcache_free_memory      | 16326632 |
| Qcache_hits             | 5969     |
| Qcache_inserts          | 95       |
| Qcache_lowmem_prunes    | 0        |
| Qcache_not_cached       | 410      |
| Qcache_queries_in_cache | 46       |
| Qcache_total_blocks     | 106      |
+-------------------------+----------+
11 rows in set (0.00 sec)
As you reduce key_buffer and query_cache_size, check the output of the SHOW STATUS command shown above while the server is under load to verify that you are not running low on Key_blocks_unused or Qcache_free_memory

The Final result of our tinkering

So now that we have trimmed Apache and MySQL lets see what the top output looks like with the server under load
Our CPU load has reduced (idle is up to ~65%). More importantly we are no longer paging and we still have a bit of RAM left over.  MySQL is running with ~8M of RAM, down from ~21M. Also as seen below, we've roughly doubled our performance.  Up to 6.85 req/sec and first response in down to 276ms.
$ http_load-parallel 5 -seconds 20 pdxphp-test
137 fetches, 5 max parallel, 4.69493e+06 bytes, in 20 seconds
34269.6 mean bytes/connection
6.85 fetches/sec, 234747 bytes/sec
msecs/connect: 80.6284 mean, 96.353 max, 73.043 min
msecs/first-response: 276.766 mean, 447.707 max, 225.995 min
HTTP response codes:
  code 200 -- 137

Links:

Tagged PHP cleanup