はじめまして。ピクシブで広告関連のプロダクトを開発しているeastです。今回は、社内で運用している広告配信サーバーの負荷テストを実施したので、その話をしたいと思います。
経緯
ピクシブの広告配信サーバーは、pixiv本体を中心に複数のサービスに対して広告配信を行なっています。現在私はこの広告配信サーバーの大規模改修を行なっているのですが、先日ついに広告配信サーバーの改修がほぼ完了したので、試しに負荷試験を行なってみたいと思い立ちました。
目標は毎秒1万リクエスト
ピクシブの広告配信サーバーへのリクエスト数はDailyで 4〜6億req もあり、これは毎秒平均に直すと約 5,000RPS(Request Per Second) になります。さらに、ピークタイムである休日の深夜帯には 12,000RPS にも達します。つまり新しい広告配信サーバーにも、毎秒12,000のリクエストを捌く性能が必要ということです。
負荷テストツールの選定
負荷テストを行うためのツールはいくつも存在しており、どれを使うか迷ったのですが、最終的にはLocustを使うことにしました。
Locustは、OSSのPython製負荷テストツールです。Locustを選んだ理由としては、
- GUIのダッシュボードが使えて途中経過もリアルタイムで見れる
- LocustはPythonでテストシナリオを書ける(個人的にPythonは書き慣れてるので)
- Pythonで独自の拡張もできる
等が挙げられます。単純に負荷を与えるだけなら、CPU効率の良いツールが他にもありました。しかし、他の有名なツールはXMLで独自の記法でテストシナリオを書く必要があったりと取っつきにくい物が多かったため、ツールとしての使いやすさからLocustを選びました。 ちなみに、採用を迷ったツールの1つがTsungです。 Tsungを選択しなかった理由は、
- XMLの独自形式でシナリオを書く必要があるので学習コストが高そうだった
- ダッシュボードがないのでテスト中の観測や調整がしづらい
- Erlang、Perlと言った、普段自分が使っていない言語で書かれているので、拡張しにくい
等で、Locustを選択した理由の裏返しとも言えます。単純なCPU効率ならTsungの方が優秀だったと思いますが……
総じてLocustは負荷テストツールとして癖が少なく敷居が低いので、小〜中規模の負荷テストなら特にオススメです。
また、Locustを動かすためのインフラとしては、Google Cloud Platformから提供されているGoogle Kubernetes Engine(GKE)を採用しました。GKEなら発生させたい負荷に応じて容易にクラスタをスケールすることが可能なためです。これにより、小規模・短期間の負荷テストから、大規模・長期間の負荷テストまで幅広く対応することができます。また、必要な時だけマシンリソースを増加させることができるので、負荷テストに必要な金銭的コストを削減することにも繋がります。
負荷テスト環境構築の流れ
基本的にはこのページを参考にしつつ独自に改良を加えました。
大まかな流れは以下のようになります。
- GKEのクラスタを作成
- Locustの設定ファイルの作成
- Docker Imageの作成
- Master Podのデプロイ
- Worker Podsのデプロイ
- Masterのserviceをデプロイ
- ダッシュボードから負荷試験開始
テンプレート用のリポジトリを作成してあるので、これを利用して進めていきます。
ちなみに、負荷試験中サーバーのモニタリングはPromethus + Grafanaで行いました。
GKEクラスタの作成
GKEクラスタ作成の方法や、gcloudコマンド、kubectlが使えることは前提とします。GCPは公式のドキュメントが充実してるので、初めての場合でもそれほど迷わず設定できると思います。
それでは、Locustを動かすためのGKEクラスタを作成します。負荷試験の規模によりますが、今回は毎秒1万リクエストを目指すので余裕を持ってn1-standard-2
を20台で構成しました。費用を抑えたいなら、プリエンプティブノード
を有効にするといいかもしれません(参考)。後は特に気をつけることはありませんが、ブートディスクは小さくてもいいかもしれませんね。
Locustの設定ファイルを作成
まず、前述したテンプレートリポジトリをcloneします。
リポジトリ内のlocust-tasks/tasks.py
にテストシナリオを書きます。詳しくは公式のドキュメントを見てという感じですが、単純に特定のエンドポイントに対してリクエストを飛ばすだけならとても簡単です。
例えば、https://hogehoge/index
というURLに対してGETする場合、locust-tasks/tasks.py
を以下のように書き換えます。
sessionの利用も普通にできるので、ログイン処理等も可能です。以下は公式ドキュメントから引用したサンプルコードです。
Docker Imageの作成
前項で編集したtask.py
を含むDocker imageをbuildします。リポジトリのルートにDockerfileがあるので、buildしてGoogle Container Registryにアップロードします。
以下のコマンドを実行すると、buildとGoogle Container Registryへのアップロードが続けて実行されます(PROJECT-IDの部分は適宜置き換えて下さい)。
Master-Podのデプロイ
リポジトリのdeployment-master.yaml
にDeploymentが定義してあるので、これを編集します。
まずcontainers.locust.image
に、先ほどbuildしたDocker imageを指定します。
また、containers.locust.env
のTARGET_HOSTに、負荷を与えたいURLのHOSTを指定します。https://hogehoge/index
なら、以下のように編集します。
編集が終わったら、apply
なりcreate
なりでデプロイします。
Worker-Podsのデプロイ
masterと同じ要領でdeployment-worker.yaml
を書き換えます。また、GKEのクラスタ数に応じて、レプリカ数を調節します。適切なレプリカ数に関してはまだ検証不足ですが、今回(vCPU 2 × 15台)はレプリカ数80に設定しました。
編集が終わったら、masterと同じようにデプロイします。
MasterのServiceをデプロイ
webブラウザからlocustのダッシュボードにアクセスするために、locust-masterのserviceをデプロイします。
type: LoadBalancer
のServiceがデプロイされるので、数分経ったらkubectlコマンドあるいはGCPのダッシュボードから外部IPを確認します。
EXTERNAL-IP
が、目的のIPアドレスになります。
ダッシュボードから負荷試験を開始
Port 8089でダッシュボードにアクセスします。以下画像のようなwindowが出るので、負荷を与えたい量を入力します。
上段のNumber of users to simulte
には、作成するクライアント数を指定します。1秒間にテストを実行する分量と考えて差し支えないと思います。毎秒1万リクエストの負荷試験を行いたい場合は、ここに10000
と入力します。
下段のHatch rate
には、クライアント数の増加ペースを指定します。例えばここに10
を指定すると、毎秒10のペースでクライアント数が増加します。Number of users to simulte
に10000を指定した場合を考えると、開始してから段々とRPSが上昇していき、開始から1000秒後に10,000RPSに達する感じですね。
以下は、10,000RPSに到達した際のスクショです。
考察
今回、10,000RPSを目指した際には vCPU 2 × 15台 のクラスタでGKEを構築しました。今回は単純にいくつかのエンドポイントに対してGETリクエストするだけの負荷試験だったため、この台数で10,000RPSに達することができました。しかし、本格的なシナリオで負荷テストを行う場合は、今回以上にマシンリソースが必要になると思われます。
せっかくなので、負荷テストに使ったクラスタの料金を簡単に計算してみました。
n1-standard-2
を15台使って、負荷試験を1時間行う場合を考えてみます。n1-standard-2
の東京リージョンでの料金は 0.122$/h です。が、今回の用途ならプリエンプティブインスタンスで事足りそうなので、それだと 0.0265$/h になります。これを前提にすると、0.0265 × 15 × 1 × 110(円/ドル)
として、 約44円 の計算です。実際にはこれに加えて通信量とかもかかってくるのでもう少し上がりますが、10,000RPSの負荷試験が1時間40円超でできるというのは悪くないのではないでしょうか。
まとめ
今回負荷試験を行うことで、以下のように様々な情報を得ることができました。
- 本番稼働に必要と思われるサーバーの台数
- 本番稼働時の推定費用
- システムの負荷が増大した際のスケーリングの動き
- システムのボトルネックとなっていたポイント
また、これまで動作検証では発見できていなかったバグや不具合を見つけることもできました(例:高負荷時にRedisのコネクションが溢れる等)。
今回に限らず、今後も重要な機能をリリースする際には負荷試験を行うことを検討してもいいかもと思いました。