Nginx + memcached で WordPress をキャッシングしてみる

nginxNginx + memcached が気になったので試してみました。元ネタは、下記です。
CakePHPとnginx+memcachedで手軽にキャッシュを活用する – Shin x blog

Nginx では、HttpMemcachedModule を用いることで memcached にキャッシュがあれば、それをそのままレスポンスとして返すことができます。
ただし memcached に保存するキャッシュデータは、アプリケーション側で生成してやらなければなりません。

一方、WordPress では advanced-cache.php というドロップインを作成することでキャッシュデータの制御を行うことができるので、これを作って url をキーとしたキャッシュデータを memcached に保存し、これを Nginx の HttpMemcachedModule に使わせてみました。

WordPress で memcached にキャッシュする

まずは、こんな感じで advanced-cache.php を作ってみます。

<?php
/*
* MemCached に Cache する advanced-cache.php
*
*/

// memcached server name & port
if ( !defined('MC_MEMCACHED_SERVER') )
	define('MC_MEMCACHED_SERVER', 'localhost');
if ( !defined('MC_MEMCACHED_SERVER_PORT') )
	define('MC_MEMCACHED_SERVER_PORT', 11211);

// cache data expires (sec.)
if ( !defined('MC_MEMCACHED_EXPIRES') )
	define('MC_MEMCACHED_EXPIRES', 600);

// cache data compress
if ( !defined('MC_MEMCACHED_COMPRESS') )
	define('MC_MEMCACHED_COMPRESS', false); // or MEMCACHE_COMPRESSED

class AdvancedMemCache {
	const   PLUGIN_NAME = 'AdvancedMemcache';
	private $memcache;
	private $scheme = 'http://';
	private $user_agent;

	function __construct() {
		if ( isset($_SERVER&#91;"HTTPS"&#93;) && strtolower($_SERVER&#91;'HTTPS'&#93;) == "on" )
			$this->scheme = 'https://';
		$this->user_agent =
			isset($_SERVER["HTTP_USER_AGENT"])
			? $_SERVER["HTTP_USER_AGENT"]
			: '';
	}

	public function is_cache() {
		if ( $this->is_cache_agent() || $this->is_user_logged_in() || $this->is_commenter() || $this->rejected_user_agents() )
			return false;
		else
			return true;
	}

	public function output_content($url = '') {
		$response = $this->get_content($url);
		foreach ( $response['headers'] as $val ) {
			@header( $val );
		}
		echo $response['body'];
	}

	private function get_content($url = '') {
		if ( empty($url) )
			$url = $this->scheme.$_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"];

		if ( !isset($this->memcache) )
			$this->memcache_init();

		if ( ($response = $this->memcache_get($url)) === false ) {
			$response = $this->remote_get($url);
			if ( !isset($response['headers']) )
				$response['headers'] = array();
			if ( isset($response['body']) && !empty($response['body']) && $this->cache_control($response['headers']) ) {
				$cache_create = gmdate("D, d M Y H:i:s") . ' GMT';
				$response['body'] .= "\n<!-- cache created at {$cache_create} -->";
				$this->memcache_set($url, $response);
			}
		}

		return $response;
	}

	private function cache_control($headers) {
		$cached = true;
		foreach ($headers as $header){
			if ( preg_match('/^(expires: |cache-control: |pragma: )/i',$header) ) {
				$cached = false;
				break;
			}
		}
		return $cached;
	}

	private function is_cache_agent() {
		if ( isset($_SERVER['HTTP_X_CACHE_AGENT']) && $_SERVER['HTTP_X_CACHE_AGENT'] == self::PLUGIN_NAME )
			return true;
		return false;
	}

	private function is_user_logged_in() {
		$is_user_logged_in = false;
		foreach ($_COOKIE as $key => $val ) {
			if ( preg_match('/^(wordpress_logged_in_|wordpress_|wordpressuser_)/', $key) ) {
				$is_user_logged_in = true;
				break;
			}
		}
		return $is_user_logged_in;
	}

	private function is_commenter() {
		$is_commenter = false;
		foreach ($_COOKIE as $key => $val ) {
			if ( preg_match('/^comment_author_/', $key) ) {
				$is_commenter = true;
				break;
			}
		}
		return $is_commenter;
	}

	private function rejected_user_agents() {
		if ( preg_match('/^'.preg_quote(self::PLUGIN_NAME. '/').'/i', $this->user_agent) )
			return true;
		return false;
	}

	private function memcache_init() {
		$this->memcache = memcache_connect(MC_MEMCACHED_SERVER, MC_MEMCACHED_SERVER_PORT);
	}

	private function memcache_get( $url ) {
		$response = array( 'headers' => array(), 'body' => '' );
		if ( ($response['headers'] = $this->memcache->get('headers-'.$url)) === false )
			return false;
		if ( ($response['body'] = $this->memcache->get($url)) === false )
			return false;
		return $response;
	}

	private function memcache_set($url, $val) {
		$this->memcache->set('headers-'.$url, $val['headers'], MC_MEMCACHED_COMPRESS, MC_MEMCACHED_EXPIRES);
		$this->memcache->set($url, $val['body'], MC_MEMCACHED_COMPRESS, MC_MEMCACHED_EXPIRES);
	}

	private function parse_str($string, &$array) {
		parse_str($string, $array);
		if ( get_magic_quotes_gpc() )
			$array = stripslashes_deep( $array );
	}

	private function parse_args($args, $defaults = '') {
		if ( is_object($args) )
			$r = get_object_vars($args);
		elseif ( is_array($args) )
			$r =& $args;
		else
			$this->parse_str($args, $r);

		if ( is_array($defaults) )
			return array_merge($defaults, $r);
		return $r;
	}

	private function remote_get($url, $args = array()) {
		$defaults = array('method' => 'GET');
		$args = $this->parse_args($args, $defaults);
		if ( !isset($args['headers']) )
			$args['headers'] = array();
		$args['headers']['X-Cache-Agent']   = self::PLUGIN_NAME;

		return $this->request($url, $args);
	}

	private function request($url, $args = array()) {
		$defaults = array(
			'method' => 'GET',
			'user-agent' => self::PLUGIN_NAME.'; '.$this->scheme.$_SERVER['HTTP_HOST'],
			'headers' => array(),
		);

		// Pre-parse for the HEAD checks.
		$args = $this->parse_args($args);

		// stream context create
		$args = $this->parse_args($args, $defaults);
		$opts = array( 'http' => array(
			'method'     => $args['method'] ,
			'user-agent' => $args['user-agent'] ,
			) );
		$header = "User-Agent: {$args['user-agent']}\r\n";
		foreach ( $args['headers'] as $key => $val ) {
			$header .= "{$key}: {$val}\r\n";
		}
		$opts['http']['header'] = $header;
		$context = stream_context_create($opts);

		$content = file_get_contents($url, false, $context);
		return array(
			'headers' => $http_response_header,
			'body'    => $content,
			);
	}
}

$advanced_memcache = new AdvancedMemCache();
if ( $advanced_memcache->is_cache() ) {
	$advanced_memcache->output_content();
	die();
}

※実験用に作った簡易的なモノなんで、このままでは正常に動作しないかもしれません。実環境では使わない方が良いと思うよ。

これで memcached 上に url をキーとしたキャッシュデータが無い場合は、キャッシュデータを取得して保存してくれるはずです。
このままでも、一応 memcached にキャッシュデータが有る場合は、WordPress がキャッシュデータからレスポンスしてくれます。

Nginx + memcached

次に nginx.conf を、こんな感じで設定します。

# バックエンド
upstream backends {
    server 127.0.0.1:8080;
}

http {
    #(snip)

    server {
        #(snip)

        location / {
            # POSTはそのままバックエンドへ
            if ($request_method = POST) {
                proxy_pass http://backend;
                break;
            }

            # memcachedにキャッシュがあればキャッシュを
            # 無ければバックエンドへ
            set $memcached_key "$scheme://$host$request_uri$is_args$args";
            memcached_pass     127.0.0.1:11211;
            default_type       "text/html; charset=utf-8";
            add_header         "X-Advance-Cache" "cached";
            error_page         404 502 = @fallback;
        }

        location @fallback {
            proxy_pass         http://backend;
        }
    }
}

via. CakePHPとnginx+memcachedで手軽にキャッシュを活用する – Shin x blog

これで memcached 上に url をキーとしたデータがあれば、それを nginx が直接レスポンスとして返してくれます。

パフォーマンス

さくらのVPS512 上にインストールした WordPress で ab -n 1000 -c 100 で計測してみました。
ネットワークによるボトルネックを排除するため ab は同一サーバ上で実行。
advanced-cache.php 無し(通常)と、advanced-cache.php 有り(WordPress がキャッシュデータを返す)、advanced-cache.php + Nginx memcached(Nginx が直接キャッシュデータを返す)の3パターンで計測しています。

構成 Requests per second
advanced-cache.php 無し 13.85 100%
advanced-cache.php 有り 882.83 6374%
advanced-cache.php + Nginx memcached 2059.78 14872%

圧倒的じゃ無いか 😛

問題点

ただ、ちょっと問題があって、このままの設定だと memcached のデータを直でレスポンスとして返してしまうので、gzip 圧縮転送とかできないんすよね。
あと、ここに例示した advanced-cache.php だと Ktai Style とかで UA ごとに表示を切り替えているサイトに対応できなかったり、ログイン時にも memcached にキャッシュが有る場合は、非ログイン時に表示されるモノが表示されたり
まぁ、今回のは実験用の簡易的な実装ということで。

これらの問題点も、きちんと advanced-cache.php を作り込んで、nginx の設定を追い込めば対応可能だと思いますよ。

コメントを残す

メールアドレスが公開されることはありません。