12月 09

image filterイメージ先週金曜日(12/2)にクックパッドインフラ勉強会に参加しまして、そこで同社の成田さんから「今日からできるApacheモジュール開発と運用」という発表がありました。
リアルタイム画像変換モジュールの「TOFU」を開発するに至った経緯と、Apacheモジュール開発についてのお話でした。

TOFUは、S3に置かれたマスターとなる画像ファイルを取得し、与えられたパラメータでリアルタイム(オンザフライ)にリサイズ・トリミングを行うモジュール(mod_tofu)です。

実際は、モジュールによる画像取得・変換をベースに、キャッシュや配信までも含めた一連の画像配信システムと言えそうです。

この仕組みをNginxを使って実装できないかと考えて、リアルタイム変換の仕組みをNginxだけで実現する方法を実験してみました。


準備するもの

HttpImageFilterModule

このモジュールはパッケージに同梱されているオプションモジュールの一つで、JPEG、GIF、PNGを変換するフィルターを提供します。
有効にするには、ビルド時にパラメータを指定する必要があります。

./configure --with-http_image_filter_module

ビルドし直したnginxの入れ替えを行う場合は、こちらを参考にしてください。

また、ビルドにはlibgdが必要なのでGDライブラリを別途インストールしておく必要があります

最も簡単な例

/imagesディレクトリにあるjpgファイルを全て150*150のサムネイルに変換したい場合は、以下の設定を書くだけです。

location ~ /images/.*\.jpg$ {
   image_filter  crop  150  150;
}

このままだと元サイズのファイルにアクセスできなかったり、エラー制御がしにくいのですが、このモジュールがいかに簡単に使えるかがわかると思います。

ローカルファイルで実験

前述の最も簡単な例を発展させて、外部からのパラメータに従って、まずはローカルディスクにある画像に対してリアルタイム変換をしてみます。

対象とするファイル

  • momiji.jpg
  • JPEGファイル
  • 1024×683 ピクセル
  • 274,661 バイト

外部からのパラメータ

  • width(幅)
  • height(高さ)
  • type(リサイズの方法)
    • resize(縦横比を変えずにサイズ変換)
    • crop(サイズ変換後に指定されたサイズに切り取る)
  • quality(JPEGの画質≒圧縮率)

要件

/imagesディレクトリにあるJPEGファイル(*.jpg)にパラメータを付けてアクセスをした時、パラメータに従って変換された画像を返す。

結果

以下のように、パラメータを変えるだけで欲しい画像サイズに変換されます。


/images/momiji.jpg?width=150&height=150&type=resize


/images/momiji.jpg?width=200&height=200&type=crop


/images/momiji.jpg?width=400&height=100&type=crop&quality=5

こちらのリンク先でパラメータをいろいろ変更してテストができます。お試し下さい。
http://nginx.labs.cloudrop.jp/images/momiji.jpg?width=150&height=150

設定方法

処理の流れ(ローカル)

  1. /images でパラメータの有無をチェックします。なければ通常の画像を返すようにしています。(何もしない)
  2. パラメータがあれば /image_filter へリライトされ、パラメータから変数に代入し、$arg_typeを元に /resize か /corp へリライトします。
  3. /reize /crop でそれぞれ処理をし、変換された画像が返されます。

※ ImageFilterが処理できない場合は415エラーが発生するので、それを1*1pxの透過GIFに置き換えています。

設定ファイル

location ~ /images/.*\.jpg$ {
    if ($query_string ~ .*=.*) {
      rewrite ^/images/(.*\.jpg)$ /image_filter/$1 last;
    }
}

location ~ ^/image_filter/(.*\.jpg)$ {
    internal;

    set $file $1;
    set $width 150;
    set $height 150;
    set $quality 75;

    if ($arg_width ~ (\d*)) {
        set $width $1;
    }
    if ($arg_height ~ (\d*)) {
        set $height $1;
    }
    if ($arg_quality ~ (100|[1-9][0-9]|[1-9])) {
        set $quality $1;
    }

    if ($arg_type = "resize") {
        rewrite ^ /resize last;
    }
  
    rewrite ^ /crop last;
}

location /resize {
    internal;
    rewrite ^ /images/$file break;
    image_filter  resize  $width $height;
    image_filter_jpeg_quality $quality;
    error_page 415 = @empty;
}

location /crop {
    internal;
    rewrite ^ /images/$file break;
    image_filter  crop  $width $height;
    image_filter_jpeg_quality $quality;
    error_page 415 = @empty;
}

location @empty {
    empty_gif;
}

パフォーマンス

ローカル&no cache(req/s)グラフ

ab(ApacheBench)で100リクエストを並列で一度に送り、1秒あたりのリクエスト処理数を5回測定した平均値。クライアントとサーバーは別でインターネットを経由して測定。

ローカルでの処理なのでネットワークのボトルネックはありません。一方、キャッシュを利用していないので、リクエストごとに変換処理が動くことになります。
無変換を基準に考えると、サイズが大きめの画像へのリサイズは時間がかかりますが、小さくなるに従って無変換よりもパフォーマンスが上がります。
変換に処理時間がかかるものの、Webで利用するサムネイル程度のサイズにすれば十分実用的な速度だと思います。

S3経由で実験

今度はTOFUの仕様に近づくべく、S3(リモートサーバー)にあるファイルを取得するようにします。
実験のサーバーは「さくらのVPS512」でS3は「USスタンダードリージョン」です。ネットワークのレイテンシは150ms程です。

対象とするファイル

  • sanzaru.jpg
  • US Standard リージョン
  • JPEGファイル
  • 1024×683 ピクセル
  • 194,728 バイト

外部からのパラメータ(ローカルと同じ)

  • width(幅)
  • height(高さ)
  • type(リサイズの方法)
    • resize(縦横比を変えずにサイズ変換)
    • crop(サイズ変換後に指定されたサイズに切り取る)
  • quality(JPEGの画質≒圧縮率)

要件

JPEGファイル(*.jpg)にパラメータを付けてアクセスをした時、S3上にある同名ファイルを取得し、パラメータに従って変換された画像を返す。
変換した画像はキャシュし、2回目以降はキャッシュを利用する。

結果


/s3images/sanzaru.jpg?width=150&height=150&type=resize


/s3images/sanzaru.jpg?width=200&height=200&type=crop


/s3images/sanzaru.jpg?width=400&height=100&type=crop&quality=5

こちらのリンク先でパラメータをいろいろ変更してテストができます。お試し下さい。
http://nginx.labs.cloudrop.jp/s3images/sanzaru.jpg?width=200&height=200&type=crop

設定方法

処理の流れ(S3)

  1. 80番ポートで待ち受ける/s3images で8080番ポートで動くバックエンドの/s3images へリクエストをそのまま渡します。
  2. 8080番ポートの/s3imagesでパラメータから変数に代入し、$arg_typeを元に/s3_resize か/s3_corp へ、パラメータがなければ/s3_original へリライトします。
  3. /s3_resize /s3_crop /s3_originalでそれぞれS3からファイルを取得・変換処理をし、画像を80番ポートのフロントエンドへ返します。
  4. 80番ポートのフロントエンドはバックエンドから返ってきたレスポンスをキャッシュし、画像を返します。以降、すでにキャッシュ済みリクエストだった場合は、バックエンドへ送らず、キャッシュを返します。

設定ファイル

server {
    listen 80;
    server_name localhost;
    root /var/www/html;

    location /s3images {
        proxy_pass http://localhost:8080;
        proxy_cache s3cache;
        proxy_cache_key $scheme$host$uri$arg_width$arg_height$arg_type$arg_quality;
        proxy_cache_valid  200 60m;
    }
}

server {
    listen 8080;
    server_name localhost;
    root /var/www/html;

    access_log logs/proxy_access.log;
    access_log logs/error_access.log;

    resolver 8.8.8.8;

    location ~ /s3images/(.*\.jpg)$ {

        set $s3host MY_S3_HOST;
        set $file $1;
        set $width 150;
        set $height 150;
        set $quality 75;

        if ($query_string !~ .*=.*) {
          rewrite ^ /s3_original last;
        }

        if ($arg_width ~ (\d*)) {
            set $width $1;
        }
        if ($arg_height ~ (\d*)) {
            set $height $1;
        }
        if ($arg_quality ~ (100|[1-9][0-9]|[1-9])) {
            set $quality $1;
        }

        if ($arg_type = "resize") {
            rewrite ^ /s3_resize last;
        }

        rewrite ^ /s3_crop last;
    }

    location /s3_original {
        internal;
        proxy_pass http://$s3host/$file;
    }

    location /s3_resize {
        internal;
        proxy_pass http://$s3host/$file;
        image_filter  resize  $width  $height;
        image_filter_jpeg_quality  $quality;
        error_page 415 = @empty;
    }

    location /s3_crop {
        internal;
        proxy_pass http://$s3host/$file;
        image_filter  crop  $width  $height;
        image_filter_jpeg_quality  $quality;
        error_page 415 = @empty;
    }

    location @empty {
        empty_gif;
    }
}

8080ポート側にresolverの設定が入っています。NginxはOS標準の名前解決方法を利用してくれないため、ネームサーバーをresolverに設定する必要があります。
ここではGoogle Public DNSが設定されていますが、利用するサーバーの最寄り(通常同じDC内)のネームサーバーを指定します。

パフォーマンス

S3経由&cache(req/s)グラフ

ab(ApacheBench)で100リクエストを並列で一度に送り、1秒あたりのリクエスト処理数を5回測定した平均値。クライアントとサーバーは別でインターネットを経由して測定。

S3からファイルを取得する処理を含めると、ネットワークのボトルネックがあり、1リクエスト2秒程度かかります。それを含めてしまうと、ネットワークに引きずられて正確な値がでないので、キャッシュに対して測定しています。

キャッシュからの応答になるので、純粋にファイルサイズの大きさに比例していきます。マスターの画像が変更されないことがわかっている場合、もしくは、変更があった場合にハッシュ値などを使ってキャッシュを書き換えるなどすると、実用的な速度で利用できることがわかります。

まとめ

細かい設定(例えば、左上から右へ10px下へ20pxを起点に切り取るなど)はできませんし、pngからjpgなどのフォーマット変換もできないので、多くの機能を望む場合は、他のプログラムを利用するか、Nginxモジュールを書く事になると思います。

とは言っても、モジュールをインストールして、設定を書くだけで利用できるので、プロトタイプ開発の用途には使えますし、リモートサーバーのファイル自体をキャッシュしたり、Proxy Cacheを適切に設定したり、多段にキャッシュを用意したりすれば、用途に限りがありますが、プロダクションでも使えると思います。

Nginxモジュール開発の参考

Tagged with:
11月 25

Nginxを使ったWordPressのチューニングといえば、フロントエンドのNginxとバックエンドのNginx(もしくはApache)に分けてproxy cacheを効かせるのが王道です。
さらにWP Super Cacheプラグインを利用してなるべくPHPやMySQLにアクセスさせないようにすると、手軽で絶大なパフォーマンスアップが可能です。

今回はそこからもう一歩進めたチューニングについて書きたいと思います。

二段階層を廃したシンプルな構成

まずは、図をご覧ください。

構成図

前述の王道チューニングの構成はA図となります。

proxy cacheはNginxがバックエンドのサーバーに処理を回し、返ってきたレスポンスをキャッシュして、Nginx自身がキャッシュを返すことでパフォーマンスを上げる仕組みです。
A図-1がキャッシュの無いアクセス、A図-2がキャッシュが効いているアクセスを表しています。
A図-3は静的なファイル(css, js, jpgなど)をフロントのNginxが直接返すことを表しています。

fastcgi cache(HttpFcgiModule)

proxy cache自体はバックエンドのサーバーがHTTPでコンテンツを返してくれれば、どんなアプリケーションでも問わない汎用的なキャッシュ機能です。つまり、proxy cacheを利用しようとするともうひとつHTTPサーバーが必要になり、A図のようにHTTPのレイヤーが二段構成になります。

Nginxにはproxy cacheと似た機能でfastcgi cacheという機能(HttpFcgiModule)があります。これはバックエンドのFastCGIのレスポンスをキャッシュする機能です。
これを利用すると、proxy cacheを利用したのと同じ効果をHTTPサーバーなしに実現できます。
A図の8080ポートのサーバーが必要なくなり、B図の構成になります。
無駄な通信と処理がなくなり、シンプルです。

WP Super Cacheを最大限利用する

WP Super Cacheは、アクセスごとに動的に生成される記事を、静的なHTMLファイルとして保存(キャッシュ)して、それを返すように振舞うプラグインです。
キャッシュが効いている間はMySQLにアクセスすることが無いのでパフォーマンスが上がります。

.htacessの落とし穴

WP Super Cacheを利用すると、.htaccessに以下の設定が追加されます。
(利用しているプラグインによって多少変わります)

# BEGIN WPSuperCache

RewriteEngine On
RewriteBase /
AddDefaultCharset UTF-8
RewriteCond %{REQUEST_METHOD} !POST
RewriteCond %{QUERY_STRING} !.*=.*
RewriteCond %{HTTP:Cookie} !^.*(comment_author_|wordpress_logged_in|wp-postpass_).*$
RewriteCond %{HTTP:X-Wap-Profile} !^[a-z0-9\"]+ [NC]
RewriteCond %{HTTP:Profile} !^[a-z0-9\"]+ [NC]
RewriteCond %{HTTP_USER_AGENT} !^.*(2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800).* [NC]
RewriteCond %{HTTP_user_agent} !^(w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-).* [NC]
RewriteCond %{HTTP_USER_AGENT} !^(DoCoMo/|J-PHONE/|J-EMULATOR/|Vodafone/|MOT(EMULATOR)?-|SoftBank/|[VS]emulator/|KDDI-|UP\.Browser/|emobile/|Huawei/|IAC/|Nokia|mixi-mobile-converter/)
RewriteCond %{HTTP_USER_AGENT} !(DDIPOCKET;|WILLCOM;|Opera\ Mini|Opera\ Mobi|PalmOS|Windows\ CE;|PDA;\ SL-|PlayStation\ Portable;|SONY/COM|Nitro|Nintendo)
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html.gz -f
RewriteRule ^(.*) "/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html.gz" [L]

RewriteCond %{REQUEST_METHOD} !POST
RewriteCond %{QUERY_STRING} !.*=.*
RewriteCond %{HTTP:Cookie} !^.*(comment_author_|wordpress_logged_in|wp-postpass_).*$
RewriteCond %{HTTP:X-Wap-Profile} !^[a-z0-9\"]+ [NC]
RewriteCond %{HTTP:Profile} !^[a-z0-9\"]+ [NC]
RewriteCond %{HTTP_USER_AGENT} !^.*(2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800).* [NC]
RewriteCond %{HTTP_user_agent} !^(w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-).* [NC]
RewriteCond %{HTTP_USER_AGENT} !^(DoCoMo/|J-PHONE/|J-EMULATOR/|Vodafone/|MOT(EMULATOR)?-|SoftBank/|[VS]emulator/|KDDI-|UP\.Browser/|emobile/|Huawei/|IAC/|Nokia|mixi-mobile-converter/)
RewriteCond %{HTTP_USER_AGENT} !(DDIPOCKET;|WILLCOM;|Opera\ Mini|Opera\ Mobi|PalmOS|Windows\ CE;|PDA;\ SL-|PlayStation\ Portable;|SONY/COM|Nitro|Nintendo)
RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html -f
RewriteRule ^(.*) "/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html" [L]


# END WPSuperCache

これは、すでにWP Super Cacheによって静的なファイルが生成されている場合、PHPを介さずにApacheが直接リクエストを返すようにする設定です。
WP Super Cacheの推奨設定では、index.htmlとそれを圧縮したindex.html.gzが用意されるので、冗長な設定になっています。

Nginxにとって.htaccessはただのテキストファイルですから、上記RewriteRuleをNginxの設定ファイルに移植しないと、WP Super Cacheは本来のパフォーマンスを発揮しません。
この移植をすることで、B図-4のように、fastcgi cacheに到達することもなく、静的ファイルと同じ処理、パフォーマンスになります。

HttpGzipStaticModule

NginxのHttpGzipStaticModuleは、同じディレクトリ内にあらかじめ圧縮ファイル(.gz)が用意されている場合に、クライアント判定を行なって自動的に圧縮ファイルを返してくれるモジュールです。(正確には、Gzip圧縮に対応しているクライアントからリクエストが来て、同ディレクトリに同タイムスタンプで同名+.gzという名前のファイルがある場合)

このモジュールを導入するメリットは、gzファイル分の.htaccessを移植する手間がなくなることです。ただし、コアモジュールではないので、コンパイル時にオプションで指定する必要があります。

./configure --with-http_gzip_static_module

コンパイルし直したnginxの入れ替えを行う場合は、こちらを参考にしてください。

NginxにはコアモジュールでHttpGzipModuleがあり、設定をオンにすることで特定のContent-TypeにGzip圧縮をかけてレスポンスを返すことができます。こちらはリクエストごとにオンザフライで圧縮をかけるので、余分に処理がかかります。あらかじめcssやjsなどの圧縮ファイルが用意できる場合は、同階層に設置してHttpGzipStaticModuleの恩恵を受けたほうがいいと思います。

サンプル設定ファイル

前述の項目を網羅して、かつ可能な限り正常に動いている環境の設定ファイルを載せています。

nginx.conf

user  nginx nginx;
worker_processes  2; # サーバーのコア数に合わせて

pid  /var/run/nginx.pid;

events {
    worker_connections  1024;
    use epoll; # Linuxの場合
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    # 最後の$request_timeは処理時間(ms)
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" $request_time';

    sendfile        on;
    tcp_nopush     on;
    tcp_nodelay on;

    keepalive_timeout  10;

    connection_pool_size 256;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 2k;
    request_pool_size 4k;
    if_modified_since before;
    ignore_invalid_headers on;
    server_tokens off;

    gzip  on;
    gzip_min_length 0;
    gzip_buffers 4 8k;
    gzip_types text/plain text/xml application/x-javascript text/css;
    gzip_disable "msie6";
    gzip_vary on;
    # HttpStaticGzipModuleをオンに
    gzip_static on;

    output_buffers 1 32k;
    postpone_output 1460;

    # fastcgi cacheの設定(httpディレクティブ内のみ有効)
    fastcgi_cache_path      /usr/local/nginx/cache levels=1:2 keys_zone=wpcache:10m max_size=50M inactive=30m;

    server {
        listen       80;
        server_name  localhost;
        charset utf-8;

        location / {
            return 403;
        }

        location /nginx_status {
            stub_status on;
            access_log off;
            allow 127.0.0.1;
            deny all;
        }  

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

    include ./wordpress.conf;
}

wordpress.conf

# 設定を変えやすいようにupstreamにまとめておく
upstream phpfpm {
    # ローカルの場合はUNIXソケットで
    server unix:/var/run/php-fpm/www.sock;
}

server {
    listen       80;
    server_name  example.com;
    root /var/www/wordpress;
    access_log logs/access.log main;

    location / {
        # 静的なファイルの場合は処理をとめる
        # リクエストの度にファイルの存在をチェックするのは無駄だという意見もありますが、
        # どのURLの時index.phpに渡すのか、プラグインを含めて全仕様が分からないため、
        # これが最も安全だと思います。
        if (-f $request_filename) {
            break;
        }

        # ここからWP Super Cacheの設定(少しfastcgi cacheの設定も)
        # モバイルからのアクセスはキャッシュさせないようにする変数
        set $nocache "";
        set $supercache_file $document_root/wp-content/cache/supercache/${http_host}${uri}/index.html;
        set $supercache_uri "";
        if (-f $supercache_file) {
            set $supercache_uri /wp-content/cache/supercache/${http_host}${uri}/index.html;
        }

        if ($request_method = "POST") {
            set $supercache_uri "";
            set $nocache "1";
        }

        if ($query_string ~ .*=.*) {
            set $supercache_uri "";
        }

        if ($http_cookie ~ ^.*(comment_author_|wordpress_logged_in|wp-postpass_).*$) {
            set $supercache_uri "";
            set $nocache "1";
        }

        if ($http_x_wap_profile ~ ^[a-z0-9\"]+) {
            set $supercache_uri "";
            set $nocache "1";
        }

        if ($http_profile ~ ^[a-z0-9\"]+) {
            set $supercache_uri "";
            set $nocache "1";
        }

        if ($http_user_agent ~ ^.*(2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800).*) {
            set $supercache_uri "";
            set $nocache "1";
        }

        if ($http_user_agent ~ ^(w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-).*) {
            set $supercache_uri "";        
            set $nocache "1";
        }

        if ($http_user_agent ~ ^(DoCoMo/|J-PHONE/|J-EMULATOR/|Vodafone/|MOT(EMULATOR)?-|SoftBank/|[VS]emulator/|KDDI-|UP\.Browser/|emobile/|Huawei/|IAC/|Nokia|mixi-mobile-converter/)) {
            set $supercache_uri "";
            set $nocache "1";
        }

        if ($http_user_agent ~ (DDIPOCKET\;|WILLCOM\;|Opera\ Mini|Opera\ Mobi|PalmOS|Windows\ CE\;|PDA\;\ SL-|PlayStation\ Portable\;|SONY/COM|Nitro|Nintendo)) {
            set $supercache_uri "";
            set $nocache "1";
        }

        if ($supercache_uri) {
            rewrite ^ $supercache_uri last;
            break;
        }

        rewrite ^ /index.php last;
    }

    location ~ \.php {
        # 存在しないPHPファイルをシャットアウト
        if (!-f $request_filename) {
            return 404;
            break;
        }

        # fastcgi とfastcgi cacheの設定
        include ./fastcgi.conf;
        fastcgi_pass          phpfpm;
        fastcgi_cache         wpcache;
        fastcgi_cache_key     "$scheme://$host$request_uri";
        fastcgi_cache_valid   200 10m;
        fastcgi_cache_valid   404 1m;
        # $nocache = "1"の時、fastcgi cacheが無効になる
        fastcgi_cache_bypass  $nocache;
    }

    # よくアクセスされる静的ファイルにブラウザキャッシュが効くように設定
    location ~ \.(jpg|png|gif|swf|jpeg)$ {
        log_not_found off; # 404の時にerror_logに書き込まないようにする設定
        access_log off;
        expires 3d;
    }

    location ~ \.ico$ {
        log_not_found off;
        access_log off;
        expires max;
    }

    location ~ \.(css|js)$ {
        charset  UTF-8;
        access_log off;
        expires 1d;
    }

    # ドット始まりのファイルはアクセスできないように
    location ~ /\. {
        deny all;
        log_not_found off;
        access_log off;
    }

    # リライトされたWP Super Cacheのファイル
    location ~ /wp-content/cache/supercache/${http_host}${uri}/index\.html(\.gz)?$ {
        charset  UTF-8;
        internal; # この指定をしておくとURLを指定して直接アクセスできなくなる
    }

    location ~ /wp-admin/$ {
        rewrite ^/wp-admin/$ /wp-admin/index.php last;
    }
}

処理の解説

Nginxのlocationディレクティブの評価順番は、wordpress.confを例にとると、すべてのアクセスが最初に “location /”ディレクティブを通ります。(記述順番は関係ありません。)

静的ファイルのチェック

このディレクティブの先頭でリクエストされた静的ファイルが存在するかのチェックを行なっています。
静的ファイルとして存在した場合、breakで”location /”ディレクティブの処理が終わります。次に、マッチするlocationディレクティブがあればそちらが実行されます。
例えば、cssファイルだった場合は、”location ~ \.(css|js)$”ディレクティブへ処理が続いていきます。これがB図-3の流れです。

WP Super Cacheのチェック、fastcti cacheのチェック

静的ファイルとして存在しなかった場合、処理は下へ進みます。
次の処理ではWP Super Cacheが生成したキャッシュファイルの存在をチェックし、キャッシュファイルを利用するかどうかを下に続く条件で確認していきます。各条件では同時にfastcgi cacheを利用するかどうかのチェックも行なっています。

ここでは、$supercache_uriが””(空文字)に書き換えられると、WP Super Cacheの静的キャッシュファイルが使われず(下の判定で利用されます)、$nocacheが”1″に書き換えられると、fastcgi cacheが使われません( ~ \.phpディレクティブで利用されます)。

$supercache_fileが存在し、$supercache_uriが空でない場合は、”location ~ /wp-content/cache/supercache/${http_host}${uri}/index\.html(\.gz)?$”ディレクティブへ処理が続きます。これがB図-4の流れです。

全てを受け止めるindex.php

静的ファイルのチェック、WP Super Cacheのチェックが終わっても処理が続く場合は、すべてにリクエストがindex.phpへと渡され、”location ~ \.php”ディレクティブへ処理が続きます。
すでにキャッシュされていて期限が有効な場合は、fastcgi cacheが使われ(B図-2)、キャッシュがなかったり、期限が切れていた場合は、FastCGI(PHP)へ処理が渡されます(B図-1)

今回はステータスコード200の時は10分、404の時は1分キャッシュするように設定してあります。

404がキャッシュされない

目に見える記事部分のキャッシュが効いて応答が良くなると忘れてしまいがちですが、WordPressが返す404 Not Foundは無視できないくらい重い処理です。
404に対してキャッシュが作られないと、存在しない記事にアクセスがくる度にPHPとMySQLが仕事をしてしまいます。

そこで、fastcgi cacheの設定で “fastcgi_cache_valid 404 1m;” を指定しているわけですが、そのままだとキャッシュが効ききません(WordPress 3.2.1)。
調べてみると、WordPressは404の時、ヘッダーに “Cache-Control:no-cache, must-revalidate, max-age=0” を付けてレスポンスを返すことがわかりました。
このヘッダーがあるのでfastcgi cacheはキャッシュを作らないようなのです。

もっとスマートな方法があると思いますが、今回はWordPressのソースを少しいじって対応することにしました。

wp-includes/class-wp.php

function handle_404() {
    global $wp_query;

    if ( !is_admin() && ( 0 == count( $wp_query->posts ) ) && !is_404() && !is_robots() && !is_search() && !is_home() ) {
        // Don't 404 for these queries if they matched an object.
        if ( ( is_tag() || is_category() || is_tax() || is_author() || is_post_type_archive() ) && $wp_query->get_queried_object() && !is_paged() ) {
            if ( !is_404() )
                status_header( 200 );
            return;
        }
        $wp_query->set_404();
        status_header( 404 );
        //nocache_headers(); # ここをコメントアウト
    } elseif ( !is_404() ) {
        status_header( 200 );
    }
}

これで404もキャッシュされるようになりました。

おわりに

WordCamp Tokyo 2011弊社はWordPessのイベント、WordCamp 2011 Tokyoに微力ながらスポンサーとして協力させて頂きました。

ブログにもWordPressの記事が少なく、弊社がWordPressにコミットしている感じが伝わっていないと思ったので、開催前に何とか間に合って良かったです。

参考サイト

Tagged with:
5月 23

はじめに

nginxは頻繁に保守されていてどんどんバージョンが上がっていく一方で、UbuntuやWindows以外ではソースコードからビルドする方法が一般的だと思います。

nginxのバージョンアップ頻度

  • 2011/05/10 1.0.2
  • 2011/05/03 1.0.1
  • 2011/04/12 1.0.0
  • 2011/04/04 0.9.7
  • 2011/03/21 0.9.6

nginx CHANGESより

LinuxやBSDの多くのディストリビューションではNginxがパッケージリポジトリに含まれており、通常のソフトウェアインストール手法でインストールする事ができます。(Debianにおけるapt-getや、Gentooにおけるemerge、FreeBSDにおけるports、Fedora の yum、Vine Linux の apt-get など)

たまにこれらのパッケージは古いものであったりすることがあるので気をつけてください。最新の機能やバグ修正版を求めるなら、ソースコードからビルドすることをお勧めします。…
http://wiki.nginx.org/InstallJaより

上記のようにどのディストリビューションでもパッケージは用意されているものの、バージョンが古いことが多いですし、何よりも各機能を追加するモジュールがApache(httpd)と違ってスタティックな方式を採用しているので、用途に合わせてモジュールを追加したい場合は自分でビルドするしかありません。

そうすると、自分でバイナリを上書きしてアップグレードすることになります。
メンテナンス時間を設けてサービスを止めて保守出来る場合はいいですが、積極的に止めることは避けたいサービスの場合、以下の方法でリクエストを止めることなくアップグレードができます。

オン・ザ・フライで新しいバイナリにアップグレードする

今回は、nginx0.7.67を最新版(1.0.2 2011/5/23現在)にアップグレードした時の手順の確認と結果の検証を行いました。

準備

現在稼動しているバージョンのビルドオプションを確認する

-Vオプションで確認することが出来ます。

$ /usr/local/nginx/sbin/nginx -V
nginx version: nginx/0.7.67
built by gcc 4.1.2 20080704 (Red Hat 4.1.2-48)
TLS SNI support disabled
configure arguments: --with-http_stub_status_module --with-http_ssl_module

configure argumentsがオプション部分です。
サードパーティ製のモジュールを入れている場合(–add-module)は、そのモジュールが新しいバージョンのnginxに対応しているか、対応しているモジュールのバージョンがリリースされていないか、確認する必要があります。

最新バージョンをビルドする

先ほど確認したビルドオプションで新しいバージョンをビルドします。

$ wget http://nginx.org/download/nginx-1.0.2.tar.gz
$ tar vfxz nginx-1.0.2.tar.gz 
$ cd nginx-1.0.2

# ここで先のオプションを使う
$ ./configure --with-http_stub_status_module --with-http_ssl_module
$ make

# 成功すると./objs 以下に新しいバイナリが出来上がるので確認
$ ./objs/nginx -V
nginx: nginx version: nginx/1.0.2
nginx: built by gcc 4.1.2 20080704 (Red Hat 4.1.2-50)
nginx: TLS SNI support disabled
nginx: configure arguments: --with-http_stub_status_module --with-http_ssl_module

現行の設定ファイルのままで問題がないか確認する

仕様の変更などで利用できなくなるディレクティブなどがあるかもしれません。現行の設定ファイルでconfig testが通るかチェックします。

$ sudo ./objs/nginx -t -c /usr/local/nginx/conf/nginx.conf
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

アップグレード

バイナリをコピーする

単純にコピーnginxファイルを上書きするか、make installで上書きします。

$ sudo cp objs/nginx /usr/local/nginx/sbin/nginx

or

$ sudo make install

プロセスを確認する

いよいよ本題です。現在動いているnginxのマスタープロセスのpidを確認します。

psコマンド確認する方法
$ ps aux |grep nginx
root      5559  0.0  0.0  40976  1000 ?        Ss   16:36   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
nginx     5560  0.0  0.2  42572  3052 ?        S    16:36   0:00 nginx: worker process                                          
nginx     5561  0.0  0.2  42572  3044 ?        S    16:36   0:00 nginx: worker process                                          
nginx     5562  0.0  0.2  42572  2992 ?        S    16:36   0:00 nginx: worker process                                          
nginx     5564  0.0  0.2  42572  2940 ?        S    16:36   0:00 nginx: worker process  
pidファイルで確認する方法

nginx.confにpidディレクティブを設定している場合は設定したパスにpidファイルがあります。

$ cat /var/run/nginx.pid;
5559

pidは5559です。
ワーカープロセス数は、confファイル"worker_processes"の設定に依存します。

新しいバイナリを実行する(USR2シグナル)

USR2シグナルをマスタープロセスに送ります。

$ sudo kill -USR2 5559

すると、古いバージョンのプロセスと新しいバージョンのプロセスが共存する状態になります。

$ ps aux |grep nginx
root      5559  0.0  0.0  40976  1068 ?        Ss   16:36   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
nginx     5560  0.0  0.2  42572  3052 ?        S    16:36   0:00 nginx: worker process                                          
nginx     5561  0.0  0.2  42572  3044 ?        S    16:36   0:00 nginx: worker process                                          
nginx     5562  0.0  0.2  42572  2992 ?        S    16:36   0:00 nginx: worker process                                          
nginx     5564  0.0  0.2  42572  2940 ?        S    16:36   0:00 nginx: worker process                                          
root      7962  2.0  0.2  41024  3084 ?        S    19:29   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
nginx     7963  0.0  0.2  42732  3060 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7964  0.0  0.2  42732  2860 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7965  0.0  0.2  42732  2860 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7966  0.0  0.2  42732  2852 ?        S    19:29   0:00 nginx: worker process 

古いバージョンのワーカーを止める(WINCHシグナル)

WINCHシグナルを古いマスタープロセスに送ります。

$ sudo kill -WINCH 5559

すると、既に受け持っているリクエストの処理が終わり次第、古いワーカープロセスが順次終了していきます。古いプロセスはマスタープロセスだけになり、リクエストは新しいワーカープロセスが受け持っています。

ps aux |grep nginx
root      5559  0.0  0.0  40976  1072 ?        Ss   16:36   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
root      7962  0.0  0.2  41024  3084 ?        S    19:29   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
nginx     7963  0.0  0.2  42732  3060 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7964  0.0  0.2  42732  2860 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7965  0.0  0.2  42732  2860 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7966  0.0  0.2  42732  2852 ?        S    19:29   0:00 nginx: worker process  

古いバージョンのマスタープロセスを止める(QUITシグナル)

QUITシグナルを古いマスタープロセスに送ります。

$ sudo kill -QUIT 5559

古いマスタープロセスが終了し、新しいマスタープロセスとワーカープロセスだけになりました。

$ ps aux |grep nginx
root      7962  0.0  0.2  41024  3084 ?        S    19:29   0:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
nginx     7963  0.0  0.2  42732  3060 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7964  0.0  0.2  42732  2860 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7965  0.0  0.2  42732  2860 ?        S    19:29   0:00 nginx: worker process                                          
nginx     7966  0.0  0.2  42732  2852 ?        S    19:29   0:00 nginx: worker process 

検証

検証方法

http_loadを使ってドキュメントルートに置いたテキストファイルをアップグレード作業中にクライアントから毎秒10リクエスト行う。

結果

作業中の2分間、1199リクエストが全て正常に終了したので、問題なくアップグレードができたと思われます。

./http_load -rate 10 -seconds 120  -verbose url_file
--- 60.0062 secs, 600 fetches started, 599 completed, 1 current
--- 120.006 secs, 1200 fetches started, 1199 completed, 1 current
1199 fetches, 1 max parallel, 3597 bytes, in 120.006 seconds
3 mean bytes/connection
9.99115 fetches/sec, 29.9735 bytes/sec
msecs/connect: 0.417418 mean, 0.717 max, 0.248 min
msecs/first-response: 0.353556 mean, 0.605 max, 0.253 min
HTTP response codes:
  code 200 -- 1199
Tagged with:
preload preload preload