はじめに
お客: 「静的ページでメンテナンス大変なんだよねぇ」
業者: 「WordPressにしちゃいましょう!!」
(...)
お客: 「高負荷時にページ見れないんだけど…」
業者: 「高負荷時にだけ静的ページに手動で入れ替えましょう!! (nginxとか知らない)」
あなた: 「高負荷になり得るサーバで,何でWordPressとか重いのを入れちゃったの???」
こんなお客の同僚のあなたに朗報です.nginxの設定方法を少しこだわってみました.
概要
- アクセスが多い (数十〜1万リクエスト/秒) ページでWordPress
- OSもパッケージのバージョンが古いCentOS 6
- 運用面からyumで導入可能なパッケージのみで構成
- 運用面からバックエンドでWordPressはapache + mod_phpで構成
- フロントエンドにnginxを置きキャッシュ
- WordPress Super Cache(数倍程度)やopcache(数倍程度),MySQL(0-20%未満)のチューニングよりも効果的
- CPU 1コアの仮想マシン1台でも数千〜1万リクエスト/秒程度の処理性能を実現可能
- 筆者の環境では:
- チューニング前: 0.47リクエスト/秒
- チューニング後: 8137.53リクエスト/秒 (17313.89倍!!!)
- 筆者の環境では:
注意
- 著者はウェブは専門外です.
背景
- CMSとして圧倒的なシェアを誇る(とされている)WordPress
- WordPressを提案してくるWeb屋さんは多い
- WordPressはPHPによる動的ページ
- PHPは圧倒的に遅く,メモリを大量に必要とする
- せいぜいCPU1コア当たり数リクエスト/秒
- 静的ページであればCPU1コア当たり数千リクエスト/秒
- 静的ページと比較すると1コア当たり数千倍単位で遅い
- WordPress Super Cacheは焼け石に水
- apache (prefork) の場合,httpdが大量にメモリを消費する時点で負け
- オペコードやデータベースのキャッシュは焼け石に水
- 実施した方が良いことは良い
- せいぜいCPU1コア当たり数リクエスト/秒
- アクセスが多いページにはWordPressは不向き
- 静的なページを生成する様なWAFやアプリケーションを利用すべき
- それでもWordPressを導入する人はいる
- apache + mod_phpはメンテナンスできるっていう人はいる
- けどnginxはできないっていう…
- apache+mod_phpはウェブ屋さんが,nginxはインフラ屋さんが担当する,っていうケースもアリ? (個人的にはウェブ屋さんはどちらもできないと駄目なんじゃ…?)
- Cent OS 6だとapacheは未だにpreforkがデフォルト
- nginxはnginx開発元によりyumによる管理を可能とするRPMが配布されている
構成と前提条件
- OS: CentOS 6
- メモリ: 2GB
- CPU: 1コア
- パッケージ管理: yum
- バックエンド: apache + mod_php (通常の基本的な設定は既に実施済み)
- フロントエンド: nginx
- アクセス数: 高負荷時に数十〜1万リクエスト/秒以上
- 利用している機能: 予約投稿
- キャッシュの有効期限(動的ページ): 1分 (WordPressの設定変更無しで予約投稿を実現するためだけ…)
- キャッシュの有効期限(静的ファイル): 1週間
- WordPress, apache, PHP,MySQLの修正は加えない,もしくは,必要最低限に留める
apache
現状の把握
現状でどの程度の処理速度であるかab (Apache Benchmark)
で計測しておく.
特に,Requests per second:
の値を確認しておく.
可能であれば,別の物理マシンで計測する方が良い.
おそらく,0.5〜9リクエスト/秒程度の処理速度しか得られない.
-cと-nはサーバの処理能力の限界をちょっと超える程度の値を指定した方が正確な結果を得やすい.
$ ab -c 1000 -n 10000 -s 3600 http://localhost/
- -c: 1度に並行して接続する数
- なかなか終わらなければ,100, 10と減らしていく.多分に10でも厳しい.
- -n: 総試行回数
- なかなか終わらなければ,1000, 100,10と減らしていく.多分に100でも厳しい.
- -s: レスポンスタイムアウト(秒) (デフォルト: 30秒)
- タイムアウトしてしまう場合があるので長めにしておく.
計測結果例
後述の環境では,0.47リクエスト/秒(Requests per second
)の性能しかない.
$ ab -k -c 100 -n 100 -s 3600 http://hogehoge/
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking hogehoge (be patient).....done
Server Software: Apache
Server Hostname: hogehoge
Server Port: 80
Document Path: /
Document Length: 31451 bytes
Concurrency Level: 100
Time taken for tests: 214.958 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Keep-Alive requests: 0
Total transferred: 3166800 bytes
HTML transferred: 3145100 bytes
Requests per second: 0.47 [#/sec] (mean)
Time per request: 214958.318 [ms] (mean)
Time per request: 2149.583 [ms] (mean, across all concurrent requests)
Transfer rate: 14.39 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 4 1.3 4 7
Processing: 6344 182384 46966.2 205816 209095
Waiting: 5854 83952 18922.2 91713 98939
Total: 6345 182388 46965.4 205819 209098
Percentage of the requests served within a certain time (ms)
50% 205819
66% 207392
75% 207631
80% 207911
90% 208271
95% 208737
98% 209024
99% 209098
100% 209098 (longest request)
##Listenポートの80から8080への変更
-Listen 12.34.56.78:80
+Listen 127.0.0.1:8080
##prefork.cの設定変更
<IfModule prefork.c>
- StartServers 8
- MinSpareServers 5
- MaxSpareServers 20
- ServerLimit 256
- MaxClients 256
- MaxRequestsPerChild 4000
+ StartServers 20
+ MinSpareServers 20
+ MaxSpareServers 20
+ ServerLimit 20
+ MaxClients 20
+ MaxRequestsPerChild 500
+ MaxMemFree 1024
</IfModule>
- StartServers,MinSpareServers,MaxSpareServers,ServerLimit,MaxClients
- クライアントのリクエストを処理するhttpdの子プロセスの数を指定
- メモリ容量やコア数,フロントエンド(nginx)の数,アクセス状況に応じて調整
- スワップアウトしない値に調整
- WordPressでは1つのhttpdが50MB〜100MB以上のメモリを消費する
- topコマンドで表示されるRESの値を確認
- 数千リクエスト/秒のアクセスであってもhttpdにアクセスする数は少ない
- メモリがあるからといって大きくしすぎてもコンテキストスイッチやTLBミスによる遅延が大きくなるだけ
- 高負荷時のforkを防止し負荷を削減するために全て同じにする
- ここの設定では常に20個のhttpdが常駐することとなる
- MaxRequestsPerChild
- メモリ消費を抑えるための仕組み
- 指定された数のリクエストを受信した子プロセスを終了させ,新しい子プロセスを生成
- 終了したプロセスが消費していたメモリがOSに返却される
- MaxMemFree
- メモリ消費を抑えるための仕組み
- 単位はKB
- 指定した(ここでは1MB)以上の空きメモリができたら可能であればOSへメモリを返却する
- デフォルト(つまり制限無し)では,OSへ空きメモリが返却されない
##VirtualHostの設定を以下に変更
-NameVirtualHost *:80
+NameVirtualHost *:8080
(snip)
-<VirtualHost *:80>
+<VirtualHost *:8080>
ServerName hogehoge.com
ServerAdmin root@hogehoge.com
DocumentRoot "/var/www/html/hogehoge/"
</VirtualHost>
nginx
nginxのyumリポジトリの追加
CentOS 6や7ではnginxがyumの標準リポジトリには存在しない.
そこで,yumで管理して運用性を向上するため,nginxの開発元のyumリポジトリを追加する.
以下の作業でnginxのyumリポジトリが有効となる.
ただし,nginxの最新バージョンではないので,設定項目に若干の違いがあり得ることに注意する.
$ sudo rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm
nginxのインストール
$ sudo yum -y install nginx
OS起動時のnginxの起動の有効化
$ sudo /sbin/chkconfig nginx on
/etc/nginx/nginx.confの設定
インストールによって最低限の設定がされているが,以下の様に修正する.
user nginx;
# rely on nginx auto detection, and its detection works well.
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
# note that multiple file descriptors are used per connection, and this
# value must be more than 2-4 times of ``worker_connections.''
# also note that this value is related to fs.file-max (/proc/sys/fs/file-max).
worker_rlimit_nofile 16384;
events {
# note that the maximum number of file descriptors is limited to
# net.core.somaxconn by default, and nginx automatically changes
# this limitation to ``worker_connections'' if configured.
worker_connections 4096;
# enable multi_accept but take care because we have only one core and
# multi_accept may be weak agains DoS attack.
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
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" "$upstream_response_time"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}
/etc/nginx/admin.confの作成
管理ページ用の共通設定を作成する.
allow 10.0.0.0/8; # allow accesses from administrators' segment.
deny all;
proxy_no_cache 1;
proxy_cache_bypass 1;
# do not buffer client requests administrators' pages.
proxy_request_buffering off;
proxy_pass http://backend;
/etc/nginx/conf.d/defaultの設定
一部,nginx.confに記述した方が良いものもあるが,見易さのためここでは,conf.d/defaultで記述する.
# allow large file uploads.
client_max_body_size 32m;
# extend buffer in order to avoid warnings on posts.
client_body_buffer_size 64k;
# use tmpfs for client request body.
client_body_temp_path /dev/shm/nginx_client_temp 1 2;
# path for cache.
proxy_cache_path /var/www/nginx_cache levels=1:2 keys_zone=czone:8m
max_size=256m inactive=7d use_temp_path=off;
# path for temporary files.
proxy_temp_path /dev/shm/nginx_tmp;
# allow large file downloads.
proxy_max_temp_file_size 1024M;
# set an identifier key for URL.
proxy_cache_key "$scheme://$host$request_uri$is_args$args";
upstream backend {
# currently, we have only one backend, this is then meaningless.
#ip_hash;
# multiple requests per connection using HTTP KeepAlive improves
# performance.
keepalive 360;
server 127.0.0.1:8080;
}
server {
listen 80;
server_name www.hogehoge.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
#error_page 404 /404.html;
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
sendfile on;
sendfile_max_chunk 256k;
tcp_nopush on;
# this is not real-time application, and performance first.
tcp_nodelay off;
# use TCP RST, not FIN, in case of timeout. this avoids a socket
# to linger FIN_WAIT1 state for long time and immediately free buffers
# for a socket.
reset_timedout_connection on;
# HTTP KeepAlive timeout
keepalive_timeout 10;
# wait for client to send header, body, request.
client_header_timeout 10;
client_body_timeout 10;
send_timeout 10;
# enable gzip compression.
gzip on;
# reuse a compressed file if it is already compressed once.
gzip_static on;
# gunzip for a client that does not support gzip when a gzip cache exists.
gunzip on;
# use defacto standard version.
gzip_http_version 1.0;
# add Vary header.
gzip_vary on;
# compression level from 1 to 9 (9 is the maximum).
gzip_comp_level 9;
# do not gzip when file size is less than this value.
gzip_min_length 1024;
# files that will be gzip-ed.
gzip_types text/plain text/xml text/css text/javascript
application/xhtml+xml application/xml
application/rss+xml application/atom_xml
application/javascript application/x-javascript
application/x-httpd-php;
# old MS IE does not support gzip.
gzip_disable "MSIE [1-6]\.";
# apply zip on proxy as well.
gzip_proxied any;
# these configurations avoid many logs of:
# ``an upstream response is buffered to a temporary file.''
# prepare 16 buffers whose size is 64k per connection to backend.
proxy_buffers 16 64k;
# buffer to store the first part of response from backend.
proxy_buffer_size 64k;
# buffers for busy sending to a client and for reading from backend.
proxy_busy_buffers_size 128k;
# use HTTP 1.1 for backend HTTP KeepAlive.
proxy_http_version 1.1;
# pass ``Host'' to backend for a virtual host.
proxy_set_header Host $host;
# pass more information to backend.
proxy_set_header X-Real-IP $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;
# do not allow end user to send ``Connection,'' in HTTP header.
proxy_set_header Connection "";
# do not compress communications with backend.
proxy_set_header Accept-Encoding "";
# do not expose headers to end users because these headers are for backend.
proxy_hide_header X-Pingback;
proxy_hide_header X-Powered-By;
proxy_hide_header Etag;
proxy_hide_header Vary;
# specify cache zone by name.
proxy_cache czone;
# specify cache key. this might be changed for a dynamic page.
proxy_cache_key "$scheme://$host$request_uri$is_args$args";
# hold caches specified time for HTTP response codes.
proxy_cache_valid 200 301 302 1m;
# hold negative caches as well but relatively shorter.
proxy_cache_valid 404 1m;
# hold caches for other HTTP response.
proxy_cache_valid any 1m;
# send only one request for the same page, i.e., do not forward all
# requests, to backend when the requested page is first accessed and
# not cached yet.
proxy_cache_lock on;
proxy_cache_lock_age 5s;
proxy_cache_lock_timeout 5s;
# use stale cache on errors and cache updates.
proxy_cache_use_stale error timeout invalid_header updating
http_500 http_502 http_503 http_504;
# enable background cache update.
# XXX: supported from nginx 1.11.10.
#proxy_cache_background_update on;
location ^~ /wp-admin { include admin.conf; }
location ^~ /wp-login.php { include admin.conf; }
# do not cache a form page that uses token.
location ^~ /form-example {
proxy_no_cache 1;
proxy_cache_bypass 1;
proxy_pass http://backend;
}
location / {
# do not cache when an administrator is logged in.
set $nocache 0;
set $cache_bypass 0;
if ($http_cookie ~ ^.*(wordpress_logged_in).*$) {
set $nocache 1;
set $cache_bypass 1;
}
proxy_no_cache $nocache;
proxy_cache_bypass $cache_bypass;
proxy_pass http://backend;
# ignore cache control header.
proxy_ignore_headers Cache-Control;
proxy_ignore_headers Set-Cookie;
proxy_ignore_headers X-Accel-Expires;
proxy_ignore_headers Expires;
}
# cache static files longer.
location ~* .*\.(?:jpg|jpeg|gif|png|svg|ico|css|js|htm|html|pdf|ttf|eot)(\?[^/]+)?$ {
proxy_pass http://backend;
# ignore cache control header.
proxy_ignore_headers Cache-Control;
proxy_ignore_headers Set-Cookie;
proxy_ignore_headers X-Accel-Expires;
proxy_ignore_headers Expires;
# caches longer than dynamic pages.
proxy_cache_valid 200 301 302 7d;
}
}
nginxの設定ファイルの確認
設定ファイルの記述が正しいか確認する.
誤りがある場合には/var/log/nginx/error.log
に詳細が出力される.
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
nginxの起動
$ sudo /sbin/service nginx start
処理性能の確認
ab
で性能を確認しておく.
$ ab -c 1000 -n 10000 -s 3600 http://localhost/
性能の良いPCであれば1万リクエスト/秒以上 (Requests per second:
参照) の処理速度を得られる (はず).
それ程性能が良くない仮想マシンでも5000リクエスト/秒以上の処理速度を得られる.
なお,ソケットを開けない旨のエラーメッセージが出力されたら,プロセスが開けるファイル数の限界をulimit -S
の出力で確認する.
小さい様であれば,ulimit -H
で表示されるhard limitを確認する.
そして,/etc/security/limits.d/
以下にファイルを追加してnofileをhard limit限界まで増やす.
計測結果例
1秒間に処理できるリクエストの数(Requests per second
)が0.47から8137.53の実に17313.89倍に!!
$ ab -k -c 4000 -n 100000 -s 3600 http://hogehoge/
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking hogehoge (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests
Server Software: nginx
Server Hostname: hogehoge
Server Port: 80
Document Path: /
Document Length: 30251 bytes
Concurrency Level: 4000
Time taken for tests: 12.289 seconds
Complete requests: 100000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 0
Total transferred: 3068500000 bytes
HTML transferred: 3025100000 bytes
Requests per second: 8137.53 [#/sec] (mean)
Time per request: 491.550 [ms] (mean)
Time per request: 0.123 [ms] (mean, across all concurrent requests)
Transfer rate: 243847.63 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 301 737.6 2 7032
Processing: 29 85 138.4 49 3244
Waiting: 1 52 112.4 28 3229
Total: 31 386 759.5 53 7871
Percentage of the requests served within a certain time (ms)
50% 53
66% 58
75% 251
80% 1044
90% 1057
95% 1261
98% 3058
99% 3066
100% 7871 (longest request)
今後の課題
-
X-Accel-Expires
を用いたキャッシュのクリア- WordPressにnginxとの連携のためのプラグインがある.
- WordPress cronの停止とOSのcronへの移行
- WordPress,PHP,MySQLのチューニング
- 通常のチューニングによってより高性能化が可能
- 効果はnginxのキャッシュに比較すると小さい
- nginxによるキャッシュを利用する場合には,WP Super Cacheは実施しない方が良い
- 動的ページ (コメントや独自のフォームなど) でのキャッシュのコントロール
おわりに
nginxでキャッシュすることにより,動的なページで注意が必要なものの,大幅な処理速度を改善できます.
これ以上の性能を得るのであれば,フロントエンドのnginxの数を増やしたり,データベースサーバをクラスタ化することを検討するのが良いでしょう.