SREグループ・ヒロチカです。今回、Bidirectional(双方向) gRPC streamingのアプリケーションの構成変更を行った際に、新しい構成でも問題ない性能が出るかどうか負荷試験を実施しました。ツール選定から実際に結果を観測するまでの一連の流れをまとめましたので、ご紹介できればと思います。
SREグループ・ヒロチカです。GO株式会社では、サービスのクラウドインフラの設計から構築・運用までを担当しています。
GO株式会社の持つアプリケーションの中には、通信量の多さや認証のオーバヘッドを考慮して通信技術にgRPC streamingを採用しているものがあります。
今回、こちらの記事で紹介したBidirectional(双方向) gRPC streamingのアプリケーションの構成変更を行った際に、新しい構成でも問題ない性能が出るかどうか負荷試験を実施いたしました。その負荷試験の一連の流れをご紹介できればと思います。
こちらの記事で紹介したアプリケーションに対しての負荷試験となります。
既記事の再掲ですが、構成はクライアントが異なるのみで基本的に一緒です。
gRPC streamingの技術には下記の4種類の方式がありますが、今回はBidirectional(双方向)のものになります。
今回の負荷試験に利用したツールはGo言語で実装されているghzというツールになります。弊社内でも以前よりストリーミングではないgRPC アプリケーションの負荷試験に利用した実績があるツールで、弊社もGo言語を利用した開発を中心としているため環境を準備しやすいという点なども考慮し採用しました。調べてみると、gRPC streamingに対する負荷試験ツールはいくつか存在するものの、今回の通信方式であるBidirectional(双方向)のgRPC streamingをカバーしたツールはそこまで多くないようです。
参考: 負荷試験ツール ghz(github)
クライアント想定の環境にghzをインストール
インストール手順はREADMEに記載の通りです。brewやdockerでのインストールの準備もあるためどんな環境でも比較的お手軽にインストールできます。
今回はSREが普段の運用で利用している通信テスト用環境のインスタンスがGo言語インストール済みであったため、そちらを利用することとして直接 go install して利用しました。
$ go install github.com/bojand/ghz/cmd/ghz@latest
負荷をかけるパラメータの基本設定
ghzコマンドのインストールが終わったら実際にかける負荷の設定に入ります。
まず通常のgRPC通信と同じ、基本的な設定部分を確認しておきます。
負荷をかけるためのリクエスト内容は、ファイルで定義して渡すことも直接コマンドの引数で指定することもできます。
参考: ghzパラメータリファレンス
gRPCのリクエストのイメージは下記になります。
{
"proto": "path/to/hello.proto",
"call": "hoge.Foo/Hello",
"metadata": {
"Authorization": "Bearer $TOKEN"
},
"total": 100, # リクエストの合計数
"concurrency": 10, # 同時並列実行ワーカー数
"rps": 10, # req/sec
"data": {
"key": "value"
},
"max-duration": "30s", # 最大の実行期間
"host": "load-test.example.com:443"
}
$ ghz \
--proto "path/to/hello.proto" \
--call "hoge.Foo/Hello" \
-m "{\"Authorization\":\"BEARER $TOKEN\"}" \
-n 100 \
-c 10 \
-d "{\"key\": \"value\"}" \
-x "30s" \
load-test.example.com:443
上記イメージのリクエスト数に関連するパラメータとして、concurrencyは同時並列実行ワーカー数、totalは送りたいリクエストの合計数となります。上記の例で言えば、rpsで指定した10rpsを上限として10並列で100リクエストを送るのでレスポンスタイムの短いアプリケーションであれば10秒程度で完了する想定です。
しかし、totalとmax-durationではmax-durationが優先されるため、totalで設定したリクエスト数に関係なくmax-durationの秒数がきたらその時点で中断されるようになります。なので上記の設定で実際に動かすと、10rpsを上限として10並列で100リクエストしたら10秒程度で到達はしますが、10rpsでmax-durationの30秒間実行され続けます。加えて、duration(もしくはmax-duration)設定で時間指定して負荷試験のプロセスを終了するようにしていた場合、終了タイミングの通信中リクエストをエラーとみなして結果に反映させてしまうため、基本はtotalとrpsの組み合わせで実行時間を調整し目標数のリクエストが全て正常に終わるようにしておく方が望ましそうです。
負荷をかける目標値をrpsベースで決めている場合には、duration(もしくはmax-duration)設定をせず、rpsを設定した上で流したい時間になるようtotalの値を決めていきます。
以下の例は、10rpsで600リクエスト分投げ続けて問題ないかを確認したい時の設定例になります。 約60秒間リクエストになります。
{
"proto": "path/to/hello.proto",
"call": "hoge.Foo/Hello",
"metadata": {
"Authorization": "Bearer $TOKEN"
},
"total": 600, # リクエストの合計数
"concurrency": 10, # 同時並列実行数
"rps": 10, # req/sec
"data": {
"key": "value"
},
"host": "load-test.example.com:443"
}
負荷をかけるパラメータのgRPC streaming関連設定
ここまでは通常のgRPC通信含めた基本設定の確認でした。ここからgRPC streamingに関わる設定へと修正を加えていきます。
まず初めに押さえておきたいのは、ここまでconfigファイルのパラメータで設定していたリクエスト数の単位が、gRCP streaming内で送るリクエスト数ではなくgRPC streamingがサーバに対して張る1コネクション数である点です。つまり、totalやrpsで設定しているリクエスト数は、正確にはgRPC streamingがコネクションを張ってクローズするまでの数となります。張ったgRPC streamingの1コネクションの中で送るコール数は、別のパラメータstream-call-countで設定します。具体例としては下記で、1秒間に10のgRCP streamingの接続を行いそれぞれの1通信の中で50回dataの1要素をコールするという設定となっています。
{
"proto": "path/to/hello.proto",
"call": "hoge.Foo/Hello",
"metadata": {
"Authorization": "Bearer $TOKEN"
},
"total": 100, # gRPC streaming 接続数
"concurrency": 10, # 同時並列実行数
"rps": 10, # gRPC streaming 接続に対する req/sec
"stream-call-count": 50, # 1つのgRPC streamingの接続内で送るリクエストの数
"data": {
"key": "value"
},
"host": "load-test.example.com:443"
}
dataで設定しているリクエストbodyは固定パターンだけでなく、いくつかのパターンをリスト化して渡すことも可能です。Bidirectional(双方向) gRPC streamingでは、それらのパターンを先頭からストリーム内でコールします。今回、dataに設定したいパターンがかなりの行数になると見込まれるため、外部ファイル上でリスト化しdata-fileというパラメータを通して読み込む形にしています。尚、リストのパターン以上のコール数を設定した場合は、またリスト先頭からコール数に到達するまで繰り返します。
{
"proto": "path/to/hello.proto",
"call": "hoge.Foo/Hello",
"metadata": {
"Authorization": "Bearer $TOKEN"
},
"total": 100, # gRPC streaming 接続数
"concurrency": 10, # 同時並列実行数
"rps": 10, # gRPC streaming 接続に対する req/sec
"stream-call-count": 50, # 1つのgRPC streamingの接続内で送るリクエストの数
"data-file": "path/to/data-file.json" # ここでファイル指定
"host": "load-test.example.com:443"
}
[
{
"key1": "value1"
},
{
"key2": "value2"
},
{
"key3": "value3"
},
・・・略・・・
]
ちなみに、今回のケースでは正常なコールが通って返ってくるまでのステータスだけを見ており、返ってきたレスポンスの中身についてはサーバ側が正しく返している前提として精査はしていません。
また、今回のアプリケーションはコネクションを繋いだ状態の中でクライアント側が送る複数のリクエスト(コール)それぞれに対してサーバ側のレスポンスを期待するアプリケーションであるため、サーバ側からクライアント側あたるghzへ向けたリクエスト(コール)とレスポンスについては考慮していません。パラメータの内容もクライアントとしての挙動を制御するものしかなさそうです。Bidirectional(双方向) gRPC streamingにも対応しているツールなので、応用すればghz側がサーバからのリクエスト(コール)に対して何かレスポンスを返すような挙動についても確認できるかもしれませんが、今回は未検証になります。
実際にコマンドを実行し負荷をかけてみます。
$ ghz --config=./config.json
今回は下記サンプルでのリクエストです。
{
"proto": "path/to/hello.proto",
"call": "hoge.Foo/Hello",
"metadata": {
"Authorization": "Bearer $TOKEN"
},
"total": 50, # gRPC streaming 接続数
"concurrency": 5, # 同時並列実行数
"rps": 5, # gRPC streaming 接続に対する req/sec
"stream-call-count": 10, # 1つのgRPC streamingの接続内で送るリクエストの数
"data-file": "path/to/data-file.json" # ここでファイル指定
"host": "load-test.example.com:443"
}
rps、stream-call-count、concurrency、total他にはconnectionsなどのパラメータをみながら、目的とする負荷がかかるよう調整していきます。
前項の負荷試験実行に記載したサンプルのconfig.jsonをもとにghzコマンドを打った結果をみていきます。ghzの実行結果は通常、標準出力ですがformatで指定することでHTMLでの出力も可能です。標準出力では下記のように返ってきました。
Summary:
Count: 50
Total: 10.01 s
Slowest: 652.36 ms
Fastest: 5.07 ms
Average: 23.94 ms
Requests/sec: 5.00
Response time histogram:
5.067 [1] |∎
69.796 [47] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
134.525 [0] |
199.253 [0] |
263.982 [1] |∎
328.711 [0] |
393.440 [0] |
458.169 [0] |
522.898 [0] |
587.627 [0] |
652.356 [1] |∎
Latency distribution:
10 % in 5.33 ms
25 % in 5.63 ms
50 % in 5.98 ms
75 % in 6.47 ms
90 % in 6.85 ms
95 % in 8.63 ms
99 % in 652.36 ms
Status code distribution:
[OK] 50 responses
今回は5rpsでコネクションを繋ぎ1コネクション内で10個のパターンのデータをコールをする想定をしたリクエストで、総コネクション数にあたるSummaryのCountが5 x 10=50となっていることから想定通りのコネクションでの通信ができたといえます。Statusとしては全てOKが返ってきています。
該当期間のrps推移の概略(5rps想定)
他、試しに100rpsでstream-call-countを100程度にし、120秒間の負荷をかけてみた結果が下記です。
Summary:
Count: 12000
Total: 120.02 s
Slowest: 1.61 s
Fastest: 8.81 ms
Average: 33.17 ms
Requests/sec: 99.98
Response time histogram:
8.809 [1] |
169.194 [11850] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
329.578 [61] |
489.963 [3] |
650.348 [6] |
810.733 [14] |
971.117 [12] |
1131.502 [9] |
1291.887 [22] |
1452.272 [16] |
1612.656 [6] |
Latency distribution:
10 % in 14.25 ms
25 % in 15.81 ms
50 % in 18.24 ms
75 % in 23.73 ms
90 % in 47.47 ms
95 % in 70.67 ms
99 % in 206.57 ms
Status code distribution:
[OK] 12000 responses
該当期間のr/s,リソース推移のグラフ
上記は負荷をかけた結果をグラフ化したものになります。想定する負荷のケースにもよりますが、今回のアプリケーションは100rpsのコネクション接続を行っている中で1コネクションあたり100程度のコールを流した場合にCPU負荷が高くなるとわかり、この流量のトラフィックを受け切るためにはリソース調整やアプリケーションの改修を行う必要があると分かります。
今回の試験シナリオでは、Bidirectional(双方向) gRPC streamingの通信自体のコネクションの間隔についての制御は行いましたが、1コネクションあたりに流れるリクエスト(コール)の間隔ついては未制御です。必要に応じて、stream-call-duration、stream-intervalなどのパラメータで調整し、流量等の観測をしていただければと思います。
このようにして各箇所確認した負荷試験結果を基にここから更にもっと強く負荷をかけても問題がないかを確かめつつ様子を見ながら、アプリケーションのスペックやスケールの調整を行っていきます。
Bidirectional(双方向) gRPC streamingにおける負荷試験ツールghzを使った一連の流れをご紹介しました。
使用したghzはJMeterやApache Benchといった広く使われている負荷試験ツールと似たような形でパラメータが設定できるため、他の負荷試験ツールに慣れている人にとっても扱いやすいツールではないかと思います。また今回は触れませんでしたが、ghzのパラメータ設定には時間経過ごとに並列実行数を増減させたり、リクエストのパターンを変化させたりするような細かい設定をすることも可能です。
今回の記事がどなたかの役に立てば幸いです。
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @goinc_techtalk のフォローもよろしくお願いします!