afnf.net

ブログエンジン(3) キャッシュ機構とproxy_cache_revalidate

キャッシュは大事

SpringやJSPは速くありません。そしてSQL実行は容易にボトルネックになります。さらにServersMan@VPSとかもう絶望的ですw

きちんとしたキャッシュ設計ができていないと、悲惨なことになります。キャッシュを全く行わない場合、毎秒8.2リクエストしか出ませんでした。しかも、平均レスポンスタイムは3秒越えです。アカンやつです。

でも大丈夫。適切なチューニングを行うと、毎秒2758リクエストまで伸びるんです。336倍です。

方式 平均 [ms] 90% Line [ms] Max [ms] スループット [req/sec]
キャッシュ無し 3,612 5,813 15,923 8.2
サイドバーキャッシュ 884 1,166 1,766 44.3
+nginxキャッシュ 69 43 1,914 566
+nginx2段化 23 17 2,415 1588
+revalidate 13 17 44 2758

 ※Let's note R5 (Core Solo 1.06GHz、メモリ1GB)、並列40スレッドでの結果

このエントリでは、blog-java1のキャッシュ機構とチューニング手法を紹介したいと思います。

サイドバーキャッシュ

サイドバーの情報は、エントリが追加・更新されない限り変わらないので、リクエスト毎に集計するのは無駄です。blog-java1では、これをEntryCacheとしてメモリ上に保持します。

このキャッシュにより、1リクエストのSQLの実行回数は5回から1回に減ります。スループットもおおよそ5倍になっています。

方式 スループット [req/sec]
キャッシュ無し 8.2
サイドバーキャッシュ 44.3 (5.4倍)

nginxによるキャッシュ

そもそも、Jettyにリクエストが届いている時点で負けなんです。URL単位のコンテンツが固定的で、厳密な更新反映が不要なら、フロントエンド側でキャッシュしない理由はありません。

blog-java1では、nginxに以下のようなキャッシュ設定を行いました。

proxy_cache_path   /usr/local/nginx/cache levels=1:2 keys_zone=STATIC:2m max_size=100m;
proxy_cache        STATIC;
proxy_cache_valid  any 1s;

1秒というキャッシュ期限は短いように感じるかも知れませんが、Jettyへのリクエストを減らすという意味においてはこれで十分ですし、長くしてしまうとアプリ側からのキャッシュ削除が必要になるため面倒です。

このキャッシュにより、90% Lineは96%減、スループットは12.7倍になりました。

方式 90% Line [ms] スループット [req/sec]
サイドバーキャッシュ 1,166 44.3
+nginxキャッシュ 43 (96%減) 566 (12.7倍)

nginx2段化

nginxによるキャッシュを有効にすると、CPU時間のほとんどがnginxのusr時間で占められるようになります。 帯域幅がボトルネックにならないよう、gzip圧縮を有効にしているためです。特に工夫しないとリクエストのたびにgzipが行われてしまいますが、これは無駄です。gzip圧縮済のコンテンツをキャッシュするよう、nginxのリバースプロキシを2段構えにしました。

# for cache
server {
  listen         80;
  server_name    blog.afnf.net;
  gzip_types     text/css application/javascript;
  gzip_proxied   any;
  gunzip         on;

  location / {
    include           proxy.conf;
    proxy_set_header  Accept-Encoding gzip;
    proxy_pass        http://unix:/var/run/nginx.sock;
  }
}

# for gzip
server {
  listen             unix:/var/run/nginx.sock;
  gzip               on;
  gzip_types         text/css application/javascript;
  gzip_proxied       any;
  gzip_http_version  1.0;
  include            proxy.conf;

  location /blog/ {
    proxy_pass   http://127.0.0.1:8080/blog/;
    expires      1s;
  }
  #以下略
}

スループットがぐっと上がります。

方式 平均 [ms] 90% Line [ms] スループット [req/sec]
+nginxキャッシュ 69 43 566
+nginx2段化 23 17 1588 (2.8倍)

proxy_cache_revalidateの有効化

nginxキャッシュやnginx2段化を行った場合、最長レスポンスタイムはむしろ悪化しています(1,766→1,914→2,415)。これは、キャッシュ期限切れと同時に多数のリクエストがJettyに流れ込み、処理待ちが発生するためです。

そもそもコンテンツが変化していないのであれば、SQLの実行も、JSPのレンダリングも必要ないはずです。そこで、nginx 1.5.7から導入された proxy_cache_revalidate が役に立ちます。

proxy_cache_revalidate  on;

このオプションを有効にすると、nginxが保持しているLast-Modified時刻で、バックエンドに条件付きGETしてくれます。blog-java1のIfModifiedSinceFilterでこれを受け取り、304(Not Modified)を返すようにすることでさらなる高速化が可能になりました。

方式 Max [ms] スループット [req/sec]
+nginx2段化 2,415 1588
+revalidate 44 (98%減) 2758 (1.7倍)

20140216_pref

ただしこの方式をとるためには、アプリからLast-Modifiedヘッダを送出する必要があります。また、Last-Modifiedヘッダだけ送出すると、ブラウザキャッシュが優先されリクエストが発生しなく場合があるため、Expiresヘッダも合わせて設定する必要があるようです。

blog-java1では、アプリからLast-Modifiedヘッダを送出し、nginxでExpiresヘッダを送出するようにしました。ちなみにアプリからExpiresヘッダを送出すると、意図した動作になりませんでした(nginx 1.5.10)。原因不明。

静的ファイルのクライアント側キャッシュ

JMeterでのベンチマークには現れませんが、JavascriptやCSSファイルの配信は体感速度に大きく影響します。blog-java1ではこれらを/static/以下に配置し、nginx側で1年のExpiresヘッダを送出しています。これによってブラウザが静的ファイルをキャッシュするようになり、1年間はリクエストすら発生しません。

location /blog/static/ {
  proxy_pass   http://127.0.0.1:8080/blog/static/;
  expires      1y;
}

まとめ

いろいろな工夫を紹介しました。費用対効果が最も高いのはnginxのキャッシュだと思いますが、これには先だった設計が必要です。例えば、

  • URL単位のレスポンス固定化
    • モバイル対応はBootstrapなどのCSSフレームワークを使う
    • 入力チェックや国際化はクライアント側で行う
    • クライアント毎に変化するようなコンテンツはAjaxで遅延ロードする
  • 更新直後は結果が反映されない可能性を告知しておく
  • 厳密なコントロールが必要な機能は、キャッシュを無効にする

といったような工夫をする必要があります。全体への適用がしんどければ、アクセスの多そうな部分だけに絞るのも良いと思います。

適切なキャッシュを行えば、パフォーマンスは劇的に改善します。言語やフレームワークの速度、アプリ側のボトルネックなどは、些細な問題なのかもしれません。

続き

ブログエンジン(4) Maven+Jetty+Seleniumで総合テスト

comments (0)

blog-java2 engine (build:2017-09-13 21:46 JST)