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倍) |
そもそも、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倍) |
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倍) |
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倍) |
ただしこの方式をとるためには、アプリから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のキャッシュだと思いますが、これには先だった設計が必要です。例えば、
といったような工夫をする必要があります。全体への適用がしんどければ、アクセスの多そうな部分だけに絞るのも良いと思います。
適切なキャッシュを行えば、パフォーマンスは劇的に改善します。言語やフレームワークの速度、アプリ側のボトルネックなどは、些細な問題なのかもしれません。