天然パーマです。

Webアプリのパフォーマンスアップ作戦

予定している機能を実現するアプリが完成するだけでWebサービスが成り立つわけではありません。 運用の最中にパフォーマンスにまつわる問題が出てくる可能性があります。 それは突然大きなトラフィックがやってきたというような時だけではありません。 知識が無いうちですと、いざ運用に乗せてみるとずいぶんとサイトの読み込みが遅いといったケースが発生することもあります。

僕はいくつかのエロサイトを管理しているのですが、 その中に月間700万PVのアクセスをいただいている「サイトA」があります。 サイトAの場合、トラフィックもそこまで無かった当初からパフォーマンスに関する問題がいくつか発生し、 その都度調べては実践で試して対策をしてきました。また、できる限り少ないリソースでの運用を目指しています。 今回はWebアプリのパーフォマンスアップ作戦として、 サイトAでの運用経験からのいくつかの方針やTipsを紹介したいと思います。



それはどこのパフォーマンス問題?

まず、パフォーマンスといってもWebアプリの場合、どこがボトルネックになっているか? その「どこ」を把握することが大事です。単に「Webサイトが遅いよ」と言っても、 Webページ自体のレスポンスを返すのが遅いのか、ページ内で使われている画像の配信が遅いのか、描画が遅いのか... 様々なケースが考えられます。 そこで、大きく切り分けて以下の3つでパフォーマンスを考えるといいと思っています。

  • バックエンド、アプリ部分のリクエストを処理する際の性能
  • フロントエンド、アプリ部分以外のページ全体を構成するパーツ郡の配信性能
  • クライアント、ページ全体を描画する際にクライアントにどれだけ負荷をかけるかの性能

今回はあえて、フロントエンドとクライアントを分けて考えています。 クライアントは人間がページを見る場合ですと、 昨今、マシンの性能が上がっているので、ある程度負荷をかけてもいいところかもしれませんが、意識することは必要です。

バックエンドの性能計測

パフォーマンスに対する問題を解決するには、対策と結果をどちらも把握しなくてはいけません。 それには結果に対する計測が必要になってきます。ちなみに、最初に言っておきますと、 計測の原則として常に同じ環境で計測するというのが前提となってきます。

さて、バックエンドの性能を計測するには一般的にApache Benchmark「ab」というコマンドが使われています。 その名の通りApacheに付属しているツールです。

http://127.0.0.1:5000/」にリクエストしてパフォーマンスを計測するには以下のようなコマンドを実行します。

$ ab -n 100 -c 10 "http://127.0.0.1:5000/"

オプションの「-n」はリクエストの回数、「-c」は同時接続数を意味します。結果は以下のように出力されます。

Benchmarking 127.0.0.1 (be patient).....done

Server Software:        
Server Hostname:        127.0.0.1
Server Port:            5000

Document Path:          /
Document Length:        271 bytes

Concurrency Level:      10
Time taken for tests:   0.227 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      44000 bytes
HTML transferred:       27100 bytes
Requests per second:    440.95 [#/sec] (mean)
Time per request:       22.678 [ms] (mean)
Time per request:       2.268 [ms] (mean, across all concurrent requests)
Transfer rate:          189.47 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   4.4      0      20
Processing:     4   20  12.8     20     104
Waiting:        0   19  12.7     19     103
Total:          4   22  13.1     22     104

Percentage of the requests served within a certain time (ms)
  50%     22
  66%     23
  75%     24
  80%     26
  90%     36
  95%     46
  98%     56
  99%    104
 100%    104 (longest request)

ここで注目したいのは「Request per second」という欄ですね。

Requests per second:    440.95 [#/sec] (mean)

これは秒間に「440.95リクエスト」を処理しているということを意味しています。 「Hello World」程度のWebアプリなので速い数字が出てます。 この数字は複雑な処理をしたり、データベースアクセスやWeb APIへの接続をするアプリになってくると、 極端に小さくなっていきます。つまりアプリの処理速度が低下することを意味します。 サービスの規模や処理内容によるのでどういう数値が適切かはケースバイケースですが、 バックエンドのパフォーマンス計測に使う値の一つです。

Webサーバの構成

「サイトA」はPerl製のPSGI互換Webアプリとしてつくられています。 よって様々なWebアプリケーションサーバで起動することができるのですが、サイトAの場合は「Starman」を使用しています。 このStarmanで立ち上げているアプリだけでサイト内のコンテンツを全て配信して完結させることは可能ですが、 フロントエンドサーバとして「nginx」を構えさせています。 nginxがCSSや画像ファイルなどの静的コンテンツの配信とリバースプロキシによるバックエンド、 つまりStarmanとのつなぎをしています。

サーバ構成

Starmanなどで起動するアプリサーバのプロセスがどうしてもメモリを占有してしまうために、 画像などのアクセスの多いコンテンツはフロントエンドに任せています。 サイトAの場合だとStarmanの子プロセスが一つ50MBほどになってしまいますから、 静的ファイルを全てアプリケーションサーバで配信すると大変なのです。 このような構成にすることで、最小限の動的コンテンツ部分をアプリケーションサーバで担うことになって、 プロセス数の把握がしやすくなり、プリフォークさせるプロセス数の調整がしやすくなります。

アプリケーションでのキャッシュ

アプリケーションでのキャッシュをすることはバックエンドの性能を向上させる常套手段でもあります。 キャッシュプログラムでよく使うのは「set」と「get」で、それぞれ簡単な概念です。 とあるキーを指定して、値をキャッシュするにはこのようなプログラムになるでしょう。

$cache->set('key', $value);

値を取り出す時は、

my $cached_value = $cache->get('key');

とします。memcachedなどを利用した場合、「set」する際にキャッシュの有効期限を指定することができます。 例えば、24時間だけ値をキャッシュしたい時には

$cache->set('key', $value, 60 * 60 * 24);

と、秒単位で時間を記述します。この有効期限を利用して、 サイト内で頻繁にアクセスのあるデータをある一定時間キャッシュさせるということをサイトAでは行っています。

my $data = $cache->get('key');
return $data if $data;
$data = $api->get_data();
$cache->set('key', $data, 60 * 60 * 24);
return $data;

このコードは、もし、キャッシュにヒットしたらその値を即座に返し、そうじゃなかったらデータを取得し、 キャッシュに設定するということを意味しています。 このコードはよく使うパターンです。

また、データのキャッシュよりも効果的なのは、制限が多くなりますが、 HTMLのページをまるごともしくは一部をキャッシュする方法です。 Web Application Framework内でビューを通してレンダリングした結果をキャッシュに乗せてそれを利用すればよいでしょう。

クライアント側でのパフォーマンス計測と対策

静的コンテンツの配信、そしてそれの描画に関するクライアント側での最終的なパフォーマンス計測には、 ブラウザのアドオンであるYahoo! YSlowやGoogle Page Speedなどが利用されています。 対象となるサイトをブラウザ開きつつアドオンを起動すると様々なチューニングポイントと共に、 「A」とか「C」とかアルファベットでスコアを付けてくれます。 ちなみに今、サイトAのランクを計ったら「B」のスコアをいただきました。

YSlow

これを「A」にしなくてはいけないというわけでは決してないです。 それぞれのチューニングポイントは、サイトによって達成しなくていい項目も含まれているからです。

僕は最低限、自分がフロントエンドサーバで配信するコンテンツに関しての「header」調整はします。 特に「expires header」を適切に設定することでクライアント側に画像やCSSなどのファイルのキャッシュをすることができます。 サイトAでは極力長く1年間のexpiresを設定しています。nginxのコンフィグファイルは以下のように記述しています。

location ~ ^/(favicon.ico|images|js|css)/  {
    expires 1y;
}

注意したいのはこちらが意図的に「/css/style.css」を書き換えたとすると、 そのファイルのクライアント側のキャッシュを解除しなくてはいけません。 対象となるCSSを読み込むHTMLでファイル名にパラメータを付加することで強制的に書き換えた新しいものを読み込ませています。

<link rel="stylesheet" href="/css/style.css?v=0002" />

クライアントサイドに関して様々な環境が存在するので、ある程度慎重にパフォーマンスを改善していくことがいいと思います。 ちなみにフロントとクライアントサイドのチューニングについては以下の書籍が詳しいです。

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール
Steve Souders スティーブ サウダーズ
オライリージャパン
売り上げランキング: 91462


まとめ

他にもサーバ構成をより見直すことができますし、非同期で処理するべき点はそうすべきかもしれませんし、 データベースアクセスなどは簡単に改善ポイントであります。 サイトAを例に取りましたが、やり方は色々あるので一つの参考にしてください。 また、Web上にも似たような事例、もっと大規模なチューニング事例が多く掲載されているのでそちらも見てみるとよいと思います。

お知らせ

メルマガ「ゆーすけべーラジオ」でもWebアプリのつくり方を連載しています。よろしければお試し購読を!