hero_picture
Cover Image for WordPressを爆速にするnginxのproxyキャッシュを冗長化しても共通で利用できるようにするアーキテクチャ(OpenResty+Redis)のご紹介

WordPressを爆速にするnginxのproxyキャッシュを冗長化しても共通で利用できるようにするアーキテクチャ(OpenResty+Redis)のご紹介

クラウド事業部の原口です。

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です。

https://openresty.org/en/

結局の所は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#AmazonDNS

2.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の挙動を変えれるのは本当に最高です。

それでは。