同僚の自信作アプリをTsungで襲撃してみた

2日連続で枠を確保したのに早速失敗をしてしまって後悔しています。クオリティ低めでスミマセン。

さて、[アドベントカレンダー初日](https://qiita.com/t-sin/items/c8f2dc5d0002769a2792)で同僚が「Common Lispで業務改善アプリ作った」と楽しそうにしており、悔しさのあまり襲撃してみました。そう、Tsungで。

## 対象について

詳細は上記記事をご覧いただきたいのですが、概要を整理しておきます。

* GitHubからWebHookでイベントを受け付けてSlackにメッセージを投げるアプリである
* Common Lisp製のWebサービスである
* イベント駆動っぽい([libev](https://github.com/enki/libev)を使っている)
* アクセスするURLによってはHTMLページを返す(トップページとか)
* ユーザー登録 & 一覧画面をもっている
* データベースは PostgreSQL を使っている

## テスト環境

Webアプリケーションは以下のような環境で負荷テストをかけています。Zabbixはサーバーのリソース消費具合を収集して記録する目的で使っています。

![構成図](https://qiita-image-store.s3.amazonaws.com/0/20589/771de31d-6980-2405-34fe-07ed7db1bb48.png)

## まずはトップページ

トップページは動的なコンテンツを含まない作りになっています。
延べ10000人のユーザーが10回ずつアクセスします。さて無事にさばき切ることができるでしょうか。

### どうなった？

毎秒4400リクエスト程をさばき切りました。レスポンスタイムも最大3000秒弱と良い値を出しています。

![Throughput](https://qiita-image-store.s3.amazonaws.com/0/20589/aef1ba59-de75-55b9-bc0a-fc944dcd40c2.png)
![Response Time](https://qiita-image-store.s3.amazonaws.com/0/20589/049ee3c1-1a51-95d1-9581-955fa1eee2b0.png)

Zabbixの方でもほとんど負荷は増えていません。
![CPU Utilization](https://qiita-image-store.s3.amazonaws.com/0/20589/d5d279df-7fb7-f724-0ef1-31782d544955.png)
![Memory Usage](https://qiita-image-store.s3.amazonaws.com/0/20589/884042b5-1e1d-e438-b2f3-1bfa0ad90c3d.png)


## ユーザーリストを表示するページを襲ってみる

トップページはあっさりとさばききってしまったので、ならばとユーザーリストを表示するページを同じ条件で襲ってみます。

### やったか…！？

毎秒250〜300リクエストほどまで落ち込みました。レスポンスタイムも5秒〜15秒とだいぶ待たされるようになりました。

![Throughput](https://qiita-image-store.s3.amazonaws.com/0/20589/38245d07-0c8f-dcc8-ef82-13feb9e18160.png)
![Response Time](https://qiita-image-store.s3.amazonaws.com/0/20589/ac45a571-0920-a94d-8db2-d1c4c556abeb.png)

エラーも現れるようになりました。HTTPエラーは生じてないのですが、接続時にエラーになっています。
![Errors](https://qiita-image-store.s3.amazonaws.com/0/20589/fec65229-b966-328b-b840-6843c3df54b4.png)

処理待ちが増えて、待っている間にタイムアウトを起こすことが増えたのでしょうか。

ところがZabbixのCPU負荷のグラフを見てみると、たいして負荷がかかっているようには見えません。

![CPU Utilization](https://qiita-image-store.s3.amazonaws.com/0/20589/60670fb4-576d-4ef9-fcc0-3546cfbdc7e3.png)

データベースのCPUは暇そうですが、ネットワークはそれなりに転送量がありました。
![DB CPU](https://qiita-image-store.s3.amazonaws.com/0/20589/dff4d957-d2d0-e65b-a11c-e6cc90d85b00.png)
![DB Traffic](https://qiita-image-store.s3.amazonaws.com/0/20589/84938305-f5b0-6435-1524-4649d4a776fd.png)


### 考察してみる

イベント駆動のWebサーバーが動いているということと、CPUへの負荷のかかり方から推測すると、このアプリケーションはシングルスレッドで動いているのでしょう。
機械的に、シングルスレッドが1コアに割り当てられる、Disk I/Oが発生しないと仮定すると、システムへの負荷的にはすごく安心です。リソースを食いつぶしてしまうことはなさそうですね。
ネガティブに言い換えると、このアプリケーションはリソースを有効活用できていないです。
ただとてもシンプルで計算しやすいので、1サーバーに複数のプロセスを動かすことで問題を解消できそうです。

#### 改善案

少しオーバーヘッドが大きいですがDockerコンテナでアプリケーションを複数動かしたり、オーバーヘッドが気になるなら直接複数のプロセスを起動すれば良いでしょう。

すべてのリクエストが1秒程度で処理されなければならないとしたら、ロードバランサやリバースプロキシを使って、1アプリケーションあたり300リクエスト/秒に制限すれば重くなりすぎることはないと思います（リクエスト数が制限値を超えるとリクエストが拒否されてしまうので、現実的な数字はもっと上になるでしょう）。
データベースのネットワークが1Gbpsだと仮定したらトラフィックとしては余裕があります。
LBで2コア、OSで1コアつかうとして、残り13コアをWebアプリに割当できそうです。この条件だと毎秒3900リクエストをさばくことができる見込みですね。

現状のアプリケーションではデータベースへの接続は1本しかないですが、コネクションプーリングしてアプリケーションあたり5本とかに増やしてみるのも良いかもしれません。ただしデータベースの処理時間が多くはなさそうで、スキマ時間を活かすというのは効果が出るかどうかわかりません。


## まとめ

このアプリケーションの特徴が少し見えました。

* リクエストの増に対して上昇する負荷は上限がある
* キャパシティを超えるとHTTPのエラーではなく、接続ができなくなる
* ロードバランサと組み合わせて水平スケールすると性能を向上できる見込みがある
* リクエスト増でプロセスがクラッシュしづらい


## 最後に

実際は同僚にネタを提供してもらい、協力を得て実験しました。
ただ個人的にはプロセスがクラッシュするなど派手なことがおこらず、とてもくやしい思いをしました。