nginxでcacheの部分的なpurgeをしてみる (あまり使えない)

18th Aug, 2010 | nginx

ちょっと調べてみたのだけど、現実的にあまり使えない気がする。でもせっかく調べたのでメモ。

まず、このブログは Sinatra+MongoDBをnginx+thin で動かしているのだけど、非力なVPSサーバなので、Sinatra+MongoDB+thinをなるべく使わないように できるだけnginxにcacheをさせている 。そのため更新時にはcacheを消さないといけない(トップページとか)。今はデータ更新時に、すべてのキャッシュを消している。ただ、どう考えてもださいので更新したコンテンツだけpurgeできる方法がないか調べてみた(サーバに余裕あれば、毎回backend(Sinatra)に問い合わせてもいいんだけどもね)。

nginxはデフォルトでは部分的なpurgeはできないので、 ngx_cache_purge module を使ってみた。FreeBSDのports (www/nginx-devel をこのサイトでは使っている)ではオプションで選択できる。Gentooだと USE=“nginx_modules_http_cache_purge” でいけそう(未確認)。

基本的には、 そのサイトのトップページに乗っている設定 を自分のURLの構成にあわせて変更してnginx.confとかに追加すれば動く。

ポイントは、

proxy_cache_key $uri$is_args$args;

の一つ目の引数と、

proxy_cache_purge tmpcache $1$is_args$args;proxy_cache_key

の二つ目の引数に同じ値が来るようにすること。一文字でも違ったら動かない(最後の/があるとかないとかではまる)。

実際のpurge(キャッシュのクリア)はhttpで行う。GETでいいみたい(POST/DELETEでもいい)。

たとえば、

curl http://example.com/purge/{key}

みたいにコマンドラインからも呼べるし、httpなんで呼ぶのは色々なスクリプトや言語からでも簡単。

で、何が現実的に使いづらいかというと、 key の部分は通常URL(query stringとかも込み)になるのだけど、たとえば、

http://example.com/hoge?page=2
http://example.com/hoge/
http://example.com/hoge?tag=gentoo

みたいなのが、それぞれ別々のキーでキャッシュされることになる(それ自体はそうなるべきであるケースもあるし別にいい)。しかし、ここで今/hogeに関連するコンテンツを更新したとして、/hogeに関するキャッシュをすべて消したいとしよう。でも、それを簡単にやる方法がない。ワイルドカードや正規表現が使えないので、いちいち全部自分で消さないといけない。引数まで考えると場合によっては非現実的。

同じように、 http://example.com/javascript/*.js みたいな条件でpurgeをしたい場合もあると思うけど、それもできない。

というわけで、いまいち使えないなーという結論なのでした。

おまけ: 会社では、リバースプロキシ+キャッシュとして、varnishをメインのサイトで、nginxを社内のちょっとしたサーバ用途(redmineとか)で使っている。両方触ってみて思うのは、nginxはすごく楽だけどもう一歩深い設定をしだすとできなかったりややこしいことが多い。今回の件もvarnishだと、正規表現でpurge対象を決められたりできる。 ま、適材適所ってことで。



Web開発の初歩 - ブラウザでのファイルキャッシュ

20th Jan, 2010 | nginx web

Webシステムではあちらこちらでキャッシュが使われている。クライアントサイドでのファイルのキャッシュ(ブラウザやproxy)や、サーバ側でのキャッシュ(memcacheとか)など、そこかしこでキャッシュが使われている。キャッシュする対象も、ファイルであったり、データベースの検索結果であったり、動的に生成したhtmlページであったり様々だ。

その中でも、今回はweb開発で基本となるユーザ側(ブラウザやproxyサーバ)での静的ファイル(Javascriptや画像など)のキャッシュについて書いてみる。ちなみに、以前nginxを使ったサーバサイドでの動的なhtmlコンテンツのキャッシュについても書いたのでそちらもどうぞ: nginxを使った簡単快速reverse proxy+cacheサーバ構築法

まず、説明するまでもなく、キャッシュとは、コストの高い処理の結果を保存しておき、再度同じリクエストがあった場合に同じ結果を返す仕組みのことだ。キャッシュというのものはWebシステムに限らずやっかいなもので、キャッシュが有効か無効かの判断(validation)が難しい。判断に失敗するとユーザは古いデータを使い続けてしまい、システム提供側の意図しないような動作になってしまう。システムのややこしい部分の大部分はキャッシュにあるんじゃないかというぐらいややこしいことが多い。

しかし、Webシステムにおけるブラウザ(or proxy)側でのキャッシュ戦略についてはいくつかの定番がある。

  1. ファイルが変更されたかどうかを最終ファイル更新日(Last-Modified)を元に毎回サーバに問い合せる (Apache等のWebサーバのデフォルトの機能はこれになっていることが多い)。そして変更があったときにのみ新しいファイルを返す。
  2. ファイルの有効期限(Expires)をある程度先(2週間後とか)に設定し、それまでの間は古いファイルが使われてしまっても仕方ないと諦める。
  3. ファイルの有効期限(Expires)を遠い未来(2037年とか)に設定する。そして、サーバ側でファイルの変更するときには、ファイル名も変更する。

1.は毎回サーバにアクセスしにいくので(ブラウザによっては必ず毎回ではないが)、そのための余分な時間がかかる。もちろん1個や2個ならたいしたことないが、1画面内に数十ファイルある場合それなりに違ってくる。また3と比較すると、壊れたブラウザやプロキシに対して弱い(後述する)。

2.はある程度古いコンテンツをユーザに流しても問題ないと言った緩い要求の場合には有効。ただ、1.と同じく、壊れたブラウザやプロキシには弱い。

3.は1, 2.と比べ、下記の理由で優れている。

  • 1.と違いファイルが変更したかどうかをサーバに問い合せる必要がないので、その通信の時間や帯域、サーバの負荷を低減できる。
  • 2.と違いファイルが更新された場合には、すぐにキャッシュが無効になる。
  • さらに、「ファイル名を変える」ということはブラウザやProxyから見ると「新規のファイル」になるのでvalidationの処理自体必要がなくなり、どんなことがあっても必ずサーバに問い合わすようになる。これは、キャッシュシステムが壊れていたり、バグがあったりしたときにもとても強い。ブラウザやproxyにキャッシュ関連のバグや、仕様という名のルール無視な製品によって、1や2がうまくいかずに、新しいファイルが配信されない、ということがそれなりにある。そういうブラウザやproxyを使っている人を無視できればいいのだけど難しいことも多い。また、ユーザ側で起こっていることをサーバ側から完全に知ることは基本的にはできないので、できるだけそういう問題がクリティカルにならないようにしておくことが大切。

ただし、3.はアプリケーション側でしなければいけないことが増えるという欠点がある(ファイル名が変わるということは、それを呼び出しているhtml等も変える必要がある等)。ちなみに、たいていの大手サイトでは静的ファイルは3.になっているし、YSlowPage Speedと言ったWebサイトの性能を計るようなソフトウェアでは、Expiresが設定されていないと評価が落ちる(警告が出る)ようにできている。YSlowやPage Speedと言ったツールも非常に面白いのでそのうち紹介したいと思っている。

実際の設定については、Webサーバソフトウェアやアプリケーションサーバによって変わってくるが、

  1. Last-modifiedヘッダーをつける(何もしなくてもこれがつくWebサーバやアプリケーションサーバがほとんど)。
  2. Expiresヘッダーのタイムスタンプをちょっと先にする。
  3. Expiresヘッダーを遠い未来(2037年)とかにする。また、ファイルを更新するときには名前を変える。ファイル名を変えるのが大変な場合には、query stringをつける、という手もある。例えば、http://www.example.com/picture.png?123456 みたいな感じにして123456のところをファイルの中身と連動して変える。ただし、query stringがついているキャッシュされなくなってしまうブラウザやproxyサーバがあるのは注意が必要。ただ「キャッシュされなくなる方」に誤動作する方が「古いファイルがキャッシュされ続けてしまうよりは」はマシなのでシステム的にこれが楽な場合はこれもありだろう。

おまけ:

この辺の設定もnginxだと非常に楽に書けて気に入っている。たとえば、これは実際のこのブログの設定だが、ブログ内に貼ってあるスクリーンショットなどの画像ファイルは中身によってファイル名を変えているので、3.のパターンが使え、Expiresを遠い未来にしている。

例:

location http://blog.madoro.org/mn/images/ {
  root /usr/local/projects/everblog/public/;
  if ($request_uri ~* "\/images\/[0-9a-f]+.(png|jpg$)") {
    expires max;
    break;
  }
}

nginxはこんな感じで、プログラム風に設定を書けるのがとても楽。一度使ってしまうとApacheのような設定が複雑なのには戻れない。

上記のように設定しFirebugのようなツールでヘッダーを確認するとExpiresが設定されているのを確認できる。

追記: twitterで聞かれたんで。これはキャッシュさせることができる(技術的なことよりも、仕様/要求的に)コンテンツが前提の話なので、キャッシュさせたくない、有効期限をちゃんと持たせたいファイルに対しては別の話になる。



nginxを使った簡単快速reverse proxy+cacheサーバ構築法

16th Jan, 2010 | nginx

ここのブログは、nginx(proxyサーバ)が外からのアクセスを受け、それを thin + sinatra (アプリケーションサーバ) と mongoDB (データベースサーバ)で処理する、というWebシステム定番の三層構造で構成している。

ただ、見てわかるようにほぼ静的なコンテンツのサイトなので、アクセス毎にアプリケーションサーバを走らせる意味がない。また、このVPSの一番安いコースにおいているので、あまり贅沢に資源を使いたくない。と言ったことから生成したhtmlをキャッシュして2度目のアクセスからはアプリケーションサーバやデータベースにアクセスしないようにしている。

Webシステムによっては、アプリケーションサーバで静的なhtmlファイルを作成し負荷の軽減をしたりするが、キャッシュファイルを自前で扱うのはvalidation等色々だるいので、このブログシステムではnginxに任せている。

今回はその設定の紹介

基本的にnginxは設定が簡単なので超オススメ。キャッシュの設定はたったこれだけ。全体の設定ファイルはこっちに置いとく。

http {
  proxy_cache_path /usr/local/nginx/cache levels=1:2 keys_zone=cache-space:4m max_size=50m inactive=120m;
  proxy_temp_path /usr/local/nginx/tmp; 

  server {
     location / {
      proxy_cache cache-space;
      proxy_cache_valid  200 302  60m;
      proxy_cache_valid  404 20m;
    }
  }
}

簡単に説明すると、まず、最初のproxy_cache_pathとproxy_temp_pathでキャッシュが格納されるディレクトリとサイズ等を指定する。詳しくはこの辺を見た方が早いが、この設定では、キャッシュ用に、メモリを4M使う、ディスクの最大サイズは50M、120分間アクセスがないキャッシュファイルは削除、の意味。

localtion / 内で、実際にどういうものをキャッシュするか決める。proxy_cache_valid には、httpのレスポンスコードと、そのレスポンスコードを返したときのコンテンツをどのくらいの時間キャッシュするかを書く。

この場合、正常処理である200または302を返した場合、そのコンテンツをnginxが60分間保持し、その後60分間、同じURLでアクセスがあった場合、アプリケーションサーバ(ここの場合はSinatra)に問い合わせずキャッシュをそのまま返す。

追記1:

     Twitterにて: 「なぜ404は20分にしているのか」というのがあったが、正直なところあまり深い意味はないが、設定したときは以下のことを考えていた。

  1. 404を返すだけなら、アプリケーションサーバの負荷はたいしたことなさそう。
  2. 404はエラーなので(相手側のエラーの場合と、実際にファイルがない場合)、何か直す必要がある場合にはすぐ反映されるように。

追記2: 

     Twitterにて: 「データ更新をしても最大60分間は実際の更新が行われないということか」というのがあった、そのままだとそうなる。でも自分は更新時の処理でCacheをクリアしている。ただし、NginxにはCacheを消すという正式な方法がまだ用意されていないのでアンオフィシャルな方法である「単純にcacheディレクトリの下のファイルをすべて消す」というので運用している(cacheディレクトリの下のディレクトリは消してはダメ)。今のところそれで問題は起きてない。追記: 部分的にcacheをpurgeする方法を書いた。

abを使って簡単なベンチマークを取ってみた

nginxのキャッシュなしの場合:

% ab -n 100 -c 20 http://blog.madoro.org/mn/14
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking blog.madoro.org (be patient).....done


Server Software:        nginx/0.8.31
Server Hostname:        blog.madoro.org
Server Port:            80

Document Path:          /mn/14
Document Length:        5844 bytes

Concurrency Level:      20
Time taken for tests:   9.049 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      605444 bytes
HTML transferred:       588577 bytes
Requests per second:    11.05 [#/sec] (mean)
Time per request:       1809.781 [ms] (mean)
Time per request:       90.489 [ms] (mean, across all concurrent requests)
Transfer rate:          65.34 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      161  174   9.2    173     202
Processing:   450 1522 301.2   1456    2268
Waiting:      224 1090 286.8   1059    1893
Total:        613 1696 306.1   1620    2454

Percentage of the requests served within a certain time (ms)
  50%   1620
  66%   1789
  75%   1808
  80%   1822
  90%   1853
  95%   2439
  98%   2449
  99%   2454
 100%   2454 (longest request)

nginxのキャッシュありの場合

% ab -n 100 -c 20 http://blog.madoro.org/mn/14
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking blog.madoro.org (be patient).....done


Server Software:        nginx/0.8.31
Server Hostname:        blog.madoro.org
Server Port:            80

Document Path:          /mn/14
Document Length:        5844 bytes

Concurrency Level:      20
Time taken for tests:   3.236 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      656124 bytes
HTML transferred:       636251 bytes
Requests per second:    30.90 [#/sec] (mean)
Time per request:       647.157 [ms] (mean)
Time per request:       32.358 [ms] (mean, across all concurrent requests)
Transfer rate:          198.02 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      162  177  10.1    174     201
Processing:   329  389  74.3    351     791
Waiting:      164  176  16.7    167     234
Total:        494  566  75.5    527     957

Percentage of the requests served within a certain time (ms)
  50%    527
  66%    599
  75%    617
  80%    628
  90%    652
  95%    667
  98%    817
  99%    957
 100%    957 (longest request)

格段にレスポンスがよくなっているのがわかる。

このような並列のアクセスだけなく、単体のアクセスのレスポンスも早くなるので、負荷が高いサイトだけでなく(ここのような)零細なサイトでも恩恵を得ることができる。最近のコンテンツは動的な部分にJavascriptを使うことが多くなっているので、このようなキャッシュがしやすくてよい。