クラウド事業部の原口です。
nginxは主にリバースプロキシとして使う事が多いと思いますが、バックエンドのプログラム側でのキャッシュ実装が難しい場合などはnginx側でキャッシュしてしまうと、お手軽にインフラのみでパフォーマンスを上げる事ができます。
よく聞くのは「wordpressをプロキシキャッシュで爆速にする!」といった内容ですが、これは本当に早くなります。
キャッシュを使う事で高速になるばかりか、wordpressへアクセスが行かない事で当然データベースなどの検索も走らなくなりサーバー負荷も軽減されます。
もちろん記事を更新してもページが更新されない!などの問題へのハンドリングが必要ですが、wordpress + nginxの実装は広く行われている為、少し検索するとnginxプロキシキャッシュのベストプラクティスが見つかります。そういった情報量の多さからも負荷が高いwordpressサイトへの実装としてはかなりよい手段となります。
目次
冗長化したい
さて、そんな最高のnginxプロキシキャッシュですが、nginxのプロキシキャッシュは「ファイル」でしか保存できない為、サーバー自体の冗長化を行った際に共通で利用できません。これは2台に冗長化すると単純計算でキャッシュヒット率が下がるという事で、WEBサーバーが3台…4台…と増えていくと、よりキャッシュヒット率が減少する事となります。またpurge(キャッシュの破棄)のタイミングも同期的に行う必要もあり、そもそも難易度の高いwordpressの冗長化にさらにものすごく面倒な問題が発生してしまう事となります。
以下のようにAWSであればElastic Load Barancingを利用して冗長化した場合に問題となります。
nginxプロキシキャッシュをredisに保存
nginxはプロキシキャッシュをファイルにしか出力できませんが、これをなんとかredisに保存するようにする事でWEBサーバー郡から共通で利用できるキャッシュストアとする事ができます。
キャッシュが一箇所となる事で、もし問題がおきてもflashする事でキャッシュを全削除できたり、redisの強力なキャッシュ操作でキャッシュのハンドリングも容易になります。
AWSであればAmazon ElastiCache (Redis)を利用する事でマネージドなキャッシュストアを利用できます
OpenRestyとは?
OpenRestyは、nginxにLuaJITとその他いろいろなサードパーティ製モジュールを予め拡張された状態で提供されているOSSです。
結局の所はnginxなのですが、例えば今回のようにRedisに接続するなどのモジュールはOpenRestyでは予め使える状態である為こちらを利用していきます。
OpenRestyでnginxプロキシキャッシュをredisに保存する実装例
インストール
1yum-config-manager --add-repo https://openresty.org/package/amazon/openresty.repo
2yum install -y openresty
設定(全体)
1worker_processes 8;
2user nginx nginx;
3
4events {
5 worker_connections 10000;
6}
7
8http {
9 sendfile on;
10 include mime.types;
11 default_type application/octet-stream;
12 resolver [DNSサーバーのIP];
13
14 upstream app {
15 server [バックエンドサーバの設定];
16 keepalive 1000000;
17 }
18
19 upstream redis_backend {
20 server [redisサーバのエンドポイント]:6379;
21 keepalive 1000000;
22 }
23
24 server {
25 listen 80;
26
27 set $do_not_cache 0;
28 set $do_purge 0;
29
30 if ($request_method = POST) {
31 set $do_not_cache 1;
32 set $do_purge 1;
33 }
34
35 if ($request_uri = "/wp-admin/admin-ajax.php") {
36 set $do_not_cache 1;
37 set $do_purge 0;
38 }
39
40 if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
41 set $do_not_cache 1;
42 }
43
44 if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
45 set $do_not_cache 1;
46 }
47
48 if ($request_uri !~* "(\.php.*|\.css.*|\.js.*|\/[^\.]*)$") {
49 set $do_not_cache 1;
50 }
51
52 if ($http_cookie ~ ^.*(wordpress_logged_in).*$) {
53 set $do_not_cache 1;
54 }
55
56 #などなどキャッシュしない設定条件やキャッシュをpurgeする条件をここに書いていく
57
58 proxy_redirect off;
59
60 proxy_set_header Host $host;
61 proxy_set_header X-Real-IP $remote_addr;
62 proxy_set_header X-Forwarded-Host $host;
63 proxy_set_header X-Forwarded-Server $host;
64 proxy_set_header X-Forwarded-Proto $scheme;
65 proxy_set_header Accept-Encoding "";
66
67 proxy_ignore_headers Cache-Control;
68 proxy_ignore_headers Set-Cookie;
69 proxy_ignore_headers X-Accel-Expires;
70 proxy_ignore_headers Expires;
71
72 location = /redis_fetch {
73 internal;
74 set_md5 $key $args;
75 set $redis_key $host:$key;
76 redis_pass redis_backend;
77 }
78
79 location = /redis_store {
80 internal;
81 set_unescape_uri $exptime $arg_exptime;
82 set_unescape_uri $key $arg_key;
83 set_md5 $key;
84 redis2_query set $host:$key $echo_request_body;
85 redis2_query expire $host:$key $exptime;
86 redis2_pass redis_backend;
87 }
88
89 location / {
90 access_by_lua_block {
91 ngx.log(ngx.INFO, "access_by_lua_block start.")
92 if (ngx.var.do_purge == "0") then
93 return
94 end
95
96 local redis = require "resty.redis"
97 local red = redis:new()
98 local ok, err = red:connect("[redisサーバのエンドポイント]", 6379)
99 if not ok then
100 ngx.log(ngx.ALERT, "failed to connect: ", err)
101 return
102 end
103
104 res, err = red:keys(ngx.var.host..":*")
105 if not res then
106 ngx.log(ngx.ALERT, "failed to keys: ", err)
107 return
108 end
109
110 for i, r in pairs(res) do
111 ngx.log(ngx.INFO, r)
112 red:del(r)
113 end
114
115 red:close()
116 }
117
118 set $key $uri$is_args$args;
119 set_escape_uri $escaped_key $key;
120
121 srcache_store_skip $do_not_cache;
122 srcache_fetch_skip $do_not_cache;
123
124 srcache_fetch GET /redis_fetch $key;
125 srcache_store PUT /redis_store key=$escaped_key&exptime=604800;
126
127 add_header X-Openresty-Cache $srcache_fetch_status;
128 add_header X-Openresty-Cache-STORE $srcache_store_status;
129
130 if ($do_not_cache = 0) {
131 more_set_headers 'Cache-Control: public, no-transform, max-age=604800';
132 }
133
134 proxy_pass http://app;
135 }
136
137 location /cache_purge {
138 default_type text/plain;
139 content_by_lua_block {
140 local redis = require "resty.redis"
141 local red = redis:new()
142 local ok, err = red:connect("[redisサーバのエンドポイント]", 6379)
143 if not ok then
144 ngx.log(ngx.ALERT, "failed to connect: ", err)
145 return
146 end
147
148 res, err = red:keys(ngx.var.host..":*")
149 if not res then
150 ngx.log(ngx.ALERT, "failed to keys: ", err)
151 return
152 end
153
154 for i, r in pairs(res) do
155 ngx.log(ngx.INFO, r)
156 red:del(r)
157 end
158
159 red:close()
160
161 ngx.say("cache purged. host: ", ngx.var.host)
162 }
163 }
164 }
165}
簡単な解説やハマったポイント
基本的には公式のreadmeの
https://github.com/openresty/srcache-nginx-module#caching-with-redisを確認する事で動作が可能でした。
これに加えて上記の実装では
「特定の条件の時はキャッシュしない(BYPASS)」
「POSTメソッドがきたらキャッシュをすべてpurgeする」
「/cache_purge に接続すると接続先hostに対するキャッシュを削除する」
などの実装を行っています。(カワカツ先生に実装いただきました!thx!!)
開発中は
1add_header X-Openresty-Cache $srcache_fetch_status;
2add_header X-Openresty-Cache-STORE $srcache_store_status;
といったヘッダをつける事でgoogle chromeのDevToolsなどでヘッダにてキャッシュから返したか、キャッシュされたか、などのステータスを確認しながら実装できます。
その中で、いくつかハマったポイントがあるのでご紹介します。
1.no resolver defined to resolveエラーが発生する
nginxへの接続はAmazon ElastiCacheを利用していた為、エンドポイントがホスト名だったのですがタイトルのエラーが発生しました。nginxのproxy_passなどの接続先をホスト名とする場合は
resolver [DNSサーバーのIP];
といった形でDNSサーバーを記述する必要があります。
AWSでVPCを利用している場合はDNSサーバは必ず xxx.xxx.xxx.2 となりますのでこれを指定します。
(参考)
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/VPC_DHCP_Options.html#AmazonDNS2.gzipしてるとどうやらキャッシュされない
キャッシュをする上でgzip圧縮されているコンテンツだった場合はキャッシュがうまくいかないようでした。
1proxy_set_header Accept-Encoding "";
と設定する事でキャッシュされました
3. バックエンドからのヘッダにexpireやmax-ageなどがあるとキャッシュされない
1proxy_ignore_headers Cache-Control;
2proxy_ignore_headers Set-Cookie;
3proxy_ignore_headers X-Accel-Expires;
4proxy_ignore_headers Expires;
nginx側でヘッダーを無視する事で回避できました
さいごに
そもそもこういった要件であればamazon cloudfrontでいいのでは、とか、wordpressでもない限り、普通にアプリでredisキャッシュ実装するわ…といった事がほとんどではないかと思うので、なかなかnginxで大味にプロキシキャッシュ!という事はないのかもしれませんが、この構成はかなりお気に入りですので今後もこのようなニーズが来た際はOpenRestyをフル活用したいと思います。
キャッシュだけじゃなくluaでnginxの挙動を変えれるのは本当に最高です。
それでは。