Nginx で WordPress を流行の構成で動作させる

スポンサーリンク

 さて、やっと思うように Nginx 上 で WordPress を動作させるに至ったので、設定やら書いていこうと思う。
 仕様としては Web Server に Nginx。ReverseProxy を用いて Proxy Cache で高速化を図る。尚かつ Let’s Encrypt で無料のサーバー証明書を用い SSL/TLS 暗号化通信しつつ HTTP/2 にも対応させる。流行の物をかなり盛り込む形をとった。

動作環境

 CentOS 7 上で Nginx Mainline 版を install していること。PHP 7 と PHP-FPM も設定が済んでいる前提とする。もちろん DB として MariaDB も 10.1 系で良いだろう。

Nginx の設定

 /etc/nginx/nginx.conf で行う設定。実際にぶっちろぐを動作させている物とは違い、多少ぼかした物としている。
 33~34 行目の real_ip を設定する事により、バックエンドに渡る remote_addr がフロントエンドに入ってきた物と同一になる。バックエンド側で location ディレクティブを用いてアクセス制御を行う為にも重要な記述と思う。
 40 行目で定義する keys_zone は cache という名前にしているが、これは任意で構わない。だが、後述するフロントエンドに於ける設定と必ず合わせる必要がある。
 54~57 行目でバックエンドサーバーの待ち受けているポートを指定している。変更する場合、合わせてバックエンド側の Listen ポートを変更するように。

user  nginx;
worker_processes  4;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
    multi_accept on;
    use epoll;
}

http {
    server_tokens off;

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    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';

    access_log  /var/log/nginx/access.log  main;

    client_max_body_size 10M;
    client_body_buffer_size 256k;

    sendfile       on;
    tcp_nopush     on;

    keepalive_timeout  15;

    set_real_ip_from 127.0.0.1;
    real_ip_header   X-Forwarded-For;

    proxy_buffering     on;
    proxy_buffer_size   64k;
    proxy_buffers       8 32k;
    proxy_max_temp_file_size 1024M;
    proxy_cache_path    /var/cache/nginx/ proxy_temp levels=1:2 keys_zone=czone:15m max_size=512m inactive=7d;
    proxy_temp_path     /var/tmp/nginx 1 2;
    proxy_cache_valid   200 302 2h;
    proxy_cache_valid   301     4h;
    proxy_cache_valid   any     1m;
    proxy_cache_use_stale  error timeout invalid_header updating http_500 http_502 http_503 http_504;
    proxy_set_header   Host                $host;
    proxy_set_header   X-Real-IP           $remote_addr;
    proxy_set_header   X-Remote-Addr       $remote_addr;
    proxy_set_header   X-Forwarded-Host    $host;
    proxy_set_header   X-Forwarded-Server  $host;
    proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;

# ===== backend
    upstream backend {
        ip_hash;
        server 127.0.0.1:8080;
    }

# ===== GZIP
    gzip on;
    gzip_http_version 1.0;
    gzip_vary on;
    gzip_static on;
    gzip_proxied any;
    gzip_comp_level 2;
    gzip_types   text/plain
                 text/xml
                 text/css
                 application/xml
                 application/xhtml+xml
                 application/rss+xml
                 application/atom_xml
                 application/javascript
                 application/x-javascript;
    gzip_disable "MSIE [1-6]\." "Mozilla/4";
    gzip_buffers 16 8k;

    include /etc/nginx/conf.d/*.conf;
}

フロントエンドの設定

 ここで WordPress をホストするフロントエンドの設定を行う。ホスト名は例として blog.example.com としている。ドキュメントルートもパスを適当に記述している。
 1~26 行目では PC 用とモバイル用、携帯用でキャッシュを分ける為の値を map ディレクティブを用いてマッピングしている。
 29~36 行目の server ブロックは 80/TCP で Listen させている。Let’s Encrypt のサーバー証明書更新用でもあり、HTTP でアクセスして来た場合には HTTPS にリダイレクトさせる目的もある。
 38 行目の server ブロックは 443/TCP で Listen している。ssl, http2 としているので HTTP/2 非対応であればフォールバックして HTTP/1.1 の SSL/TLS 通信を行う。
 51 行目からは HTTPS の設定を行っている。コメント通り「Mozilla SSL Configuration Generator」を用いた設定をしている。手っ取り早く的確な設定が行えるので重宝している。サーバー証明書は Let’s Encrypt のスクリプトが設置してくれるパスを参照している。
 98~109 行目ではキャッシュ制御用フラグを記述している。GET メソッド以外はキャッシュせず、コメントをしたユーザーとログイン中ユーザーはキャッシュしない、保護中の記事表示もキャッシュさせない。WPtouch でデスクトップ表示中には PC 用キャッシュを参照させる。
 飛んで 133 行目。ここで Nginx の Proxy Cache で用いる zone の名前を与える事。
 148 行目ではいわゆる直リンを禁止したい場合に有効な記述となるが、全てを拒否していてもなんだかなーなので、大手検索プロバイダの画像検索と大手 RSS リーダーからは直接の参照を許可する方向としている。不要であればこの Location ブロックを削除。尚、「$invalid_referer」は Nginx の組み込み変数なので変更不可能な事に注意。

map $http_user_agent $is_mobile {
    default 0;
    ~*Googlebot-Mobile 1;
    ~*iPhone           1;
    ~*iPod             1;
    ~*Android.*Mobile  1;
    ~*Windows.*Phone   1;
    ~*BB10             1;
    ~*webOS            1;
    ~*BlackBerry       1;
    ~*Opera\ Mini      1;
    ~*PlayBook         1;
    ~*Xoom             1;
    ~*P160U            1;
    ~*SCH-I800         1;
    ~*Nexus\ 7         1;
    ~*Touch            1;
    ~*Mobile.*Firefox  1;
    ~*DoCoMo           3;
    ~*UP.Browser       3;
    ~*Softbank         3;
    ~*WILLCOM          3;
    ~*emobile          3;
    ~*J-PHONE          3;
    ~*MOT-             3;
}

# for redirect to https.
server {
    listen 80;
    server_name blog.example.com;
    root /path/to/documentroot;
    index index.php;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name blog.example.com;
    root /path/to/documentroot;
    index index.php;

    charset UTF-8;

# Logging. =====
    access_log  /var/log/nginx/blog.example.com/frontend.access.log main;
    error_log   /var/log/nginx/blog.example.com/frontend.error.log warn;

# HTTPS Setting by mozilla generator. =====
    ssl on;
    ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache   shared:SSL:50m;
    ssl_session_tickets off;
    # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
    ssl_dhparam         /etc/nginx/dhparam.pem;

    # modern configuration. tweak to your needs.
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
    ssl_prefer_server_ciphers on;

    # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
    add_header Strict-Transport-Security max-age=15768000;

    # OCSP Stapling ---
    # fetch OCSP records from URL in ssl_certificate and cache them
    ssl_stapling         on;
    ssl_stapling_verify  on;

    ## verify chain of trust of OCSP response using Root CA and Intermediate certs
    ssl_trusted_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;

    resolver 8.8.8.8 valid=300s;
    resolver_timeout 10s;

# ===== Do not logging.
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

# ===== Error pages.
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

# ===== Cache flags.
    set $do_not_cache 0;
    if ( $request_method != "GET" ) {
        set $do_not_cache 1;
    }
    if ( $http_cookie ~ ^.*(comment_author_|wordpress_logged_in|wp-postpass_).*$ ) {
        set $do_not_cache 1;
    }

    # Mesure for WPtouch desktop view.
    if ( $http_cookie ~* "wptouch(_switch_cookie=normal|-pro-view=desktop)" ) {
        set $is_mobile 0;
    }

# ===== Headers
#    add_header Vary User-Agent;

# ===== Browser cache
    location ~* ^.+\.(jpg|jpeg|gif|css|png|js|ico)$ {
        expires 10d;
    }

# ===== Request to backend server.
    location ~ \.php$ {
        proxy_set_header Host                   $host;
        proxy_set_header X-Real-IP              $remote_addr;
        proxy_set_header X-Remote-Addr          $remote_addr;
        proxy_set_header X-Forwarded-Proto      https;
        proxy_set_header X-Forwarded-Host       $host;
        proxy_set_header X-Forwarded-Server     $host;
        proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
        proxy_pass http://backend;
    }

    location / {
        proxy_pass         http://backend;
        proxy_cache        czone;
        proxy_no_cache     $do_not_cache;
        proxy_cache_bypass $do_not_cache;
        proxy_cache_key    $scheme://$host$request_uri$is_mobile;
        proxy_set_header Host                   $host;
        proxy_set_header X-Real-IP              $remote_addr;
        proxy_set_header X-Remote-Addr          $remote_addr;
        proxy_set_header X-Forwarded-Proto      https;
        proxy_set_header X-Forwarded-Host       $host;
        proxy_set_header X-Forwarded-Server     $host;
        proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
        proxy_cache_valid 200 1d;
    }

    # deny direct access to image files.
    location ~* \.(jpg|gif|png|ico|mp4)$ {
        valid_referers none server_names
            *.google.co.jp
            *.google.com
            *.yahoo.co.jp
            *.yahoo.com
            *.bing.com
            *.feedly.com;

        if ( $invalid_referer ) {
            return 403;
        }
}

バックエンドの設定

 フロントエンドで受けたリクエストのうち、キャッシュされなかったリクエストを捌くのがバックエンド。
 最初の nginx.conf で指定したバックエンドの Listen ポートで待ち受けている。
 特にこれと言った特長ある設定ではないが、セキュリティ面を考慮して wp-login.php、wp-config.php、wp-admin/ 以下への制限を行っている。アクセスを許可するプライベートアドレス空間が異なるのであれば合わせて書き換えよう。
 PHP-FPM は UNIX Domain Socket で通信しているが、TCP で通信を行っているのであれば fastcgi_pass を書き換える事。
 全体で gzip が有効になっている状態だから、バックエンドでは圧縮しないようにしておく。これを忘れると同じ URI であってもキャッシュファイルが複数生成されてしまう。

server {
    listen       8080;
    server_name  blog.example.com;
    root /path/to/documentroot;
    index        index.php;

    charset UTF-8;

# ===== gzip off.
    gzip off;
    gzip_vary off;

# ===== Logging.
    access_log  /var/log/nginx/blog.example.com/access.log main;
    error_log   /var/log/nginx/blog.example.com/error.log warn;


# ===== Do not logging.
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

# ===== for WordPress
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location /wp-admin {
        allow 192.168.1.0/24;
        deny all;
    }

    location /wp-admin/admin-ajax.php {
        allow all;
    }

    location = /xmlrpc.php {
        deny all;
    }

    location = /wp-config.php {
        deny all;
    }

    location = /wp-login.php {
        allow 192.168.1.0/24;
        deny all;
        fastcgi_index  index.php;
        fastcgi_pass   unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

# ===== Error Page.
    #error_page  404              /404.html;
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

# ===== PHP-FPM
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_index  index.php;
        fastcgi_pass   unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        client_max_body_size 512M;
        include        fastcgi_params;
    }

# ===== Access Controls.
    # deny access to .htaccess files.
    location ~ /\.ht {
        deny  all;
    }
}

wp-config.php の書き換え

 ReverseProxy を用いた WordPress で HTTPS によるアクセスを行うなら、wp-config.php に次の 2~8 行目を追記しよう。
 要は HTTPS でフロントエンドにアクセスされていたら、バックエンドの WordPress は HTTPS で動いている物として応答する。
 この設定を記述しないと管理者画面などでループが発生するので注意。

<?php
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
    && $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
  $_SERVER['HTTPS'] = 'on';
}

define('FORCE_SSL_LOGIN', true);
define('FORCE_SSL_ADMIN', true);

/**
 * WordPress の基本設定

WPtouch の対応

 もし、モバイル端末向けに WPtouch を使用中であれば「Vary: User-Agent」を出力しないように修正を加える。

$ diff -u wp-content/plugins/wptouch/core/class-wptouch-pro.php.bak wp-content/plugins/wptouch/core/class-wptouch-pro.php
--- wp-content/plugins/wptouch/core/class-wptouch-pro.php.bak   2016-01-09 17:11:52.160504381 +0900
+++ wp-content/plugins/wptouch/core/class-wptouch-pro.php       2016-01-09 17:12:03.786999547 +0900
@@ -352,7 +352,7 @@
                        $this->setup_mobile_theme_for_viewing();
 
                        // For Google Best Practices
-                       header( 'Vary: User-Agent' );
+//                     header( 'Vary: User-Agent' );
                } else {
                        remove_action( 'wp_enqueue_scripts', 'wptouch_foundation_load_framework_styles', 1 );
                        add_action( 'wp_footer', array( &$this, 'handle_desktop_footer' ) );

 代わりにフロントエンドから「Vary: User-Agent」を出力するので、上記フロントエンドの設定から「add_header Vary User-Agent;」行のコメントアウトを外す。これをやらないとモバイル端末の User-Agent 毎にキャッシュファイルが生成される事になり、キャッシュ効率が落ちるしキャッシュの Purge で困る事になる。

ベンチマークをしてみる

 予め比較用データを取得していたので、結果を貼ってみる。ベンチに用いるツールは h2load になる。h2 と付いている名の通り HTTP/2 対応だから最適。ApacheBench や httpress では非対応だし ApacheBench に至っては HTTP/1.0 を喋るから話にならないかなと。
 h2load のパラメータは -n 1000 -c 10 とした。
 WordPress 自体でも WP-SuperCache を用いているので、現状ではこれがうちのベストパフォーマンスか。

starting benchmark...
spawning thread #0: 10 total client(s). 1000 total requests
TLS Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-GCM-SHA256
Application protocol: h2
progress: 10% done
progress: 20% done
progress: 30% done
progress: 40% done
progress: 50% done
progress: 60% done
progress: 70% done
progress: 80% done
progress: 90% done
progress: 100% done

finished in 964.55ms, 1036.75 req/s, 65.95MB/s
requests: 1000 total, 1000 started, 1000 done, 1000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 1000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 66705490 bytes total, 270000 bytes headers (space savings 4.26%), 66354000 bytes data
                     min         max         mean         sd        +/- sd
time for request:     3.81ms     16.16ms      8.56ms      2.06ms    77.30%
time for connect:    96.14ms    103.92ms     98.95ms      2.72ms    70.00%
time to 1st byte:   107.08ms    114.97ms    110.98ms      2.53ms    60.00%
req/s (client)  :     103.79      105.82      104.56        0.65    70.00%
starting benchmark...
spawning thread #0: 10 total client(s). 1000 total requests
TLS Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-GCM-SHA256
Application protocol: h2
progress: 10% done
progress: 20% done
progress: 30% done
progress: 40% done
progress: 50% done
progress: 60% done
progress: 70% done
progress: 80% done
progress: 90% done
progress: 100% done

finished in 641.12ms, 1559.77 req/s, 98.62MB/s
requests: 1000 total, 1000 started, 1000 done, 1000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 1000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 66296499 bytes total, 292000 bytes headers (space savings 2.99%), 65950000 bytes data
                     min         max         mean         sd        +/- sd
time for request:      608us     38.55ms      5.37ms      1.41ms    89.60%
time for connect:    96.54ms    104.35ms     99.32ms      2.77ms    70.00%
time to 1st byte:   104.65ms    134.45ms    111.32ms      8.14ms    90.00%
req/s (client)  :     156.15      157.77      157.09        0.50    60.00%

 結果、ReverseProxy を用いずとも 1036.75 req/s と、結構なパフォーマンスがあったが 1559.77 req/s にまで上昇した。
 WordPress は PHP で動作する場面が多いので、キャッシュされる割合はそう多い物でもないはずだが丁度 1.5 倍のリクエストを捌けるようになった。
 ただ、見ての通り帯域が 98.62MB/s となっているので我が家の光回線では外部との通信に於いて、回線がボトルネックになっている。果たして設定した意味はあるのか無いのか分からない所。もちろん ReverseProxy 設定前でもギリギリか届かないかな~という位に帯域がある。
4945940908
 とは言え、個人でここまでやると言うのは勉強の為でもあるし自己満足の為でもあるから良しとしよう。

おわりに

 自分自身、Nginx に触れてから日も浅いこともあり、分からないことだらけの中でなんとか安定動作にこぎ着けた。
 この記事に掲載した設定方法は「とある 1 つの方法」でしかなく、Nginx 的に不適切な物もあるかも知れないことを予めご了承願いたい。まだまだ自分も勉強して今後もチューニングしていかねば……

 あ、あと書き忘れていたが、Proxy Cache が効いた状態で検証してもパージしないと変な動作をする事がある。/var/cache/nginx 以下全てを削除するなりしてから再検証しないとドツボに嵌まるから要注意。自分はそれで嵌まってしまい、凄い時間を無駄にしたなんて間抜けな話しもある。

記事更新履歴

2016/01/08 追記

 バックエンドの設定にて、gzip を切る設定の記述を忘れていたので追記をおこなった。
 フロントでもバックでも gzip が有効になっているとキャッシュ周りで動作がおかしくなるので注意。この見落としでかなりの時間を無駄にしてしまった……

2016/01/09 追記

 モバイル端末のキャッシュ制御で抱えていた問題を解消したので、この記事の記述を見直して修正を行った。
 WPtouch の扱いに関しても追記。

2016/01/10 追記

 ブラウザキャッシュの設定をバックエンドからフロントエンドに持っていった。全てのアクセスが必ず到達するフロントエンドに書いた方が良い。これはここ数日間、設定を弄っていて分かってきた事。
 フロントバックの 2 枚構成は面倒臭さも 2 倍だなぁ… 凄い速くなるからやり甲斐あるけど。

スポンサーリンク