Nginx + 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["HTTPS"]) && strtolower($_SERVER['HTTPS']) == "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 の設定を追い込めば対応可能だと思いますよ。