nginx
キャッシュ
Docker

NginxでHTTPレスポンスをキャッシュして、外部APIの制限やレイテンシーの問題をクリアする方法

回数制限のある外部APIにリクエストを送信するとき、一度リクエストした内容をキャッシュしておき、二度目からはそのキャッシュを取得したいケースに遭遇したのでその時の対応を紹介します。

データを永続化するならRDBが候補に上がるかもしれませんが、大量レスポンスをテーブルに貯め込み、またインデックスを貼るとなれば必要以上に容量を逼迫するなどデメリットが目立ってきます。

かといって自前でキャッシュシステムを作ろうにも有効期限の設定などを実装するのは面倒です。そんな時はNginxの便利なキャッシュシステムを使うのがオススメです。


Nginxのリバースプロキシを使う

具体的にどのようにキャッシュするのかというと、以下のような構成になります。


一度目のリクエスト

              ----->             -----> 

HTTPクライアント Nginxサーバー 外部サーバー
<----- <-----


2度目以降のリクエスト

              ----->             

HTTPクライアント Nginxサーバー 外部サーバー
<-----

Nginxにはリバースプロキシと言って外部サーバーへの中継を果たす機能があり、外部サーバーに直接リクエストを送る前にNginxを経由することで、一度目のレスポンスをキャッシュし、二度目以降はそのキャッシュを即座に返すことができます。

実際にNginxを入れたDockerコンテナを使いながら実際の挙動を追っていきましょう。


Dockerを使ってNginxのリバースプロキシとキャッシュを使ってみる

Dockerを使えば手元ですぐにNginxを実行することができます。まずはDockerfileに以下を記述します。


Dockerfile

from nginx:1.15.12-alpine

RUN apk add curl # 実際には不要だが動作検証のためにcurlを入れておく
COPY nginx.conf /etc/nginx/nginx.conf
CMD ["/usr/sbin/nginx", "-g", "daemon off;"]

Dockerfileでnginx.confを上書きするように設定しているので、新たにnginx.confを以下のように作成してください。


nginx.conf

user  nginx;

worker_processes 1;

pid /var/run/nginx.pid;

events {
worker_connections 1024;
}

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

proxy_cache_path /var/cache/nginx keys_zone=cache-zone:1m levels=2:2 max_size=1g inactive=365d;
proxy_temp_path /var/cache/nginx/cache;

server {
server_name localhost;
listen 80;

location ^~ / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;

# キャッシュを行うURLを指定
proxy_pass https://qiita.com/;
proxy_cache cache-zone;
proxy_ignore_headers X-Accel-Redirect X-Accel-Expires Cache-Control Expires Vary Set-Cookie;
proxy_cache_key $host$uri$is_args$args;

# キャッシュを行うステータスコードとその際の有効期限を指定
proxy_cache_valid 200 201 300 301 302 365d;

# キャッシュが有効かどうかをレスポンスヘッダーに付与する
add_header X-Cache-Status $upstream_cache_status;
}
}
}


細かなnginxの設定は後述しますので、とりあえず動かしたい方はそのままコピペしてしまって大丈夫です。

ここまで追加したらDockerイメージを作成し、実際にコンテナの中に入ってみましょう。

docker build -t [イメージ名] .

docker run --rm -it [イメージ名]

# 上記を実行後、「docker ps」でコンテナのIDを取得してから以下を実行
docker exec -it [docker psで表示されたコンテナID] sh

コンテナに入るとnginxが自動起動しており、ポート80番で待ち受けているので、以下のようにcurlを実行するとproxy_passで指定したサイトのレスポンスが返ってきます。

curl localhost -v

-vをつけるとレスポンスの詳細が返ってきますが、この時に以下のようなレスポンスヘッダーが追加されています。

X-Cache-Status: MISS

これは独自に付けたレスポンスヘッダーで、キャッシュがヒットしなかったことを表しています。1回目は「MISS」となり、キャッシュがヒットしませんでしたが、続けて再び同じコマンドを実行するとどうでしょうか?

X-Cache-Status: HIT

見ての通りキャッシュがヒットしていますね👏

次回以降は同じURLにリクエストを送信すると、Nginxにキャッシュされたデータが返ってくるので、外部APIの回数制限やレイテンシーを気にする必要がなくなるでしょう。


キャッシュの設定について説明

まずは実際に動かすことを主眼としたため省略していましたが、Nginxのキャッシュ設定について説明します。(より詳しく知りたい方はNginxの公式ドキュメントを読むと良いでしょう)


proxy_cache_path、proxy_temp_path

proxy_temp_pathはキャッシュを一時的に保存するディレクトリで、その後proxy_cache_pathに設定に沿って移動させます。

proxy_cache_pathには永続化するキャッシュの保存パスを指定するほか、以下のようなオプションを指定することができます。

キー

keys_zone
プロキシキャッシュの名前空間

levels
ディレクトリの階層を指定する。
「2:2」なら頭文字2文字が2階層の親ディレクトリとして指定されます。

max_size
キャッシュとして確保する容量

inactive
キャッシュの有効期限。
1sなら1秒、10dなら10日と単位を指定できます。


proxy_pass

プロキシの行き先を指定します。

例えば「 https://qiita.com 」をproxy_passに指定した場合、「localhost」にアクセスすると「qiita.com」のレスポンスが返ってきます。

また「localhost/hogehoge」のようにパスを指定すると、「qiita.com/hogehoge」と同じパスへと中継されます。

基本的にはそのままアクセスを流す形となりますが、rewriteすることで状況に応じて流すURLを変更することも可能です。


proxy_ignore_headers

プロキシする際に無視するヘッダーを指定します。

レスポンスヘッダーに「Cache-Control: no-cache」が付与されていると、Nginxはデフォルトでキャッシュを無効化しますが、ignore_headersに加えることで、キャッシュを有効化するなど重要な箇所となります。


proxy_cache_valid

キャッシュを有効にするステータスコードとその有効期限を設定します。

エラーが返ってきた場合、もしかしたら相手サーバーの一時的な不具合が原因となっているかもしれません。その場合はキャッシュせずに2度目もリクエストを送ると、成功のレスポンスが返ってくる可能性があります。

proxy_cache_validを使うことで全てのリクエストを一律にキャッシュせず、指定したステータスコードのみ設定を上書きすることができます。


最後に

このようにNginxを使用することで、データベースを使用したり、物理ファイルを置いたりするよりも便利にキャッシュを行うことができます。

またDockerを使えばグローバルに露出することなく、コンテナの中で環境が閉じているので、既にアプリケーションが稼働している中でも心置きなくキャッシュシステムを構築できます。

同様のユースケースに直面した際にはぜひ参考にしてみてください。

ちなみにTwitterをやっているので興味ある方はフォローしてもらえると嬉しいです🙏

https://twitter.com/t_tiger55