本セッションの登壇者
セッション動画
「k6による負荷試験 入門から実践まで」という話をする藤原と申します。自己紹介ですが、Twitterでは@fujiwara、面白法人カヤックという会社でSREをやっています。去年、「達人が教えるWebパフォーマンスチューニング」(通称「ISUCON本」)を今回イベントをやっている方たちと一緒に出しました。ISUCONというWebパフォーマンスチューニングコンテストにはこれまでに4回優勝していて、今年の運営にもアドバイザーとして関わっています。あとは趣味としてOSSの開発をしていて、Amazon ESCデプロイツールのescpressoやLambdaデプロイツールを作っています。
負荷試験を始めるに前にやるべきこと
負荷試験といっても「パフォーマンスチューニングするために負荷をかける」「システムに負荷をかけてその性能を計測する」などいろいろあります。今回は、Webアプリケーション全体に対して外側から機械的にリクエストを送信してパフォーマンスを計測する話をします。
まず、「性能とは何か」を負荷試験をやる前にちゃんと考えておいた方が良いという話をします。
はじめに「今回はどういう条件が達成できたらいいか」というゴールを決めます。運用中のサービスに対して負荷試験をかけてパフォーマンスを改善する場合は、今の実際の性能をベースにするのが良いですし、リリース前だったら目標値をベースにするべきです。
性能を表す代表的な数字のひとつは**レイテンシ(レスポンスタイム)**です。目標設定は、どのレスポンスを何秒で返せればOKなのかを考えます。また、平均なのか、中央値なのか、たとえば99パーセンタイルなのか、max値なのか、最悪の方を担保したいのか、平均を速くしたいのか…なども考えておいたほうが良いでしょう。また、Webサービスというのは直列に1人がリクエストするだけではなく、いろいろな人がたくさん同時に並列リクエストしてきます。その場合に、並列のアクセスを受けた状態でどれぐらいレイテンシを保たなければいけないのかも考える必要があります。
レイテンシよりもスループットを目標にしたい場合もあるかもしれません。スループットというのはある一定時間の中にどれぐらいの処理をさばけるかです。ただ、スループットで同じ1000リクエスト/secが目標でも、1秒のレイテンシで1000並列に耐えられる状態と、10秒のレイテンシで1万並列に耐えられる状態のどちらなら良いのか、1秒で返さなきゃいけないのか、10秒でゆっくり返して本当に良いのか、なども考えておく必要があります。サーバコストを目標にする場合もあります。
負荷試験をする前に、スライドにあるような具体例を考えてシナリオをちゃんと言語化しておきましょう。このあたりの準備段階の話は、ISUCON本で言うと第1章の中にあります。
さらに、始める前にまだ準備が必要です。最近はクラウドのシステムが非常に多いと思いますが、クラウドサービスというのはエラスティックにキャパシティが変わっていきます。これに負荷をかける場合、オートスケーリング的な項目はある一定の値でキャパシティを設定して止めておきたいです。負荷試験というのは非常に変数の多い試験なので、どの条件を変えても結果の数字が変わってきます。その状態でスライドの図にあるようにリソースのキャパシティがどんどん変わってしまうと、何を測っているのかがよくわからなくなってしまいます。なので、可能な限り止めましょう。ただ、どうしても固定できない、勝手に調整されるのでいじれないというサービスもあるので、そういう場合はその時点でどれぐらいのキャパシティになっているのかを同時に記録しておくと良いと思います。
前段階はまだあります。先ほどモニタリングの話もありましたけれども、サーバ側のメトリクスをちゃんと取得して観測できるようにしておくことが大事です。負荷だけかけてサーバやシステムはどうなってるかわかりませんという状態だと、たとえば性能が伸びなくなったときにどこに問題があるのかがわからなくなってしまいます。CPUの使用率、リクエスト数、データベースの処理worker数など、本番で見るようなダッシュボードを整備して観測できる状態にしてから始めたほうが良いでしょう。
お勧めツールはJavaScriptでシナリオが書けるk6
ここからやっと実際の話になるんですが、まずツールの選び方です。今回はタイトルに「k6」とあるように、Grafanaが出しているk6というツールがあるのでこれをまずお勧めしたいと思います。これはNode.jsではなく実行エンジンはGoで書かれていて、非常に軽量かつ省メモリで動くのですが、シナリオを書くのはGoよりもわりと気軽に書けるJavaScriptというのがポイントです。
ただ、このツール以外を検討したほうが良い場合もあって、リクエストやレスポンスが非常に特殊な形式を扱っていてJavaScriptで普通にパースできない場合は、自作や他のツールのほうが良さそうです。k6でもgRPCやGraphQLをサポートしているのでだいたいのことはできると思います。あと、負荷試験をする人にJavaScriptよりももっと得意な言語がある場合は、その言語でシナリオを書けるほうが効率が良いかもしれません。
それから、複数のシナリオで複雑なことをしたい場合、具体的に言うとたとえば「オンラインゲームでボスレイドバトル」みたいな場合、10人20人のプレーヤーが同時に取る行動に対して同期を取って何かをしたいとか、シナリオ間でデータをやり取りしたいようなことがある場合は、k6ではちょっと難しいところがあるので自作することになるかと思います。そういうときに便利なツールとして、Goで書かれたISUCON用のベンチマークを作るフレームワーク「isucandar」というものがあって、これはISUCONのGitHubに公開されています。isucandarを使って負荷をかけるクライアントを作る方法もISUCON本の付録Bにあるので参考にしてください。
シナリオ作成例その1‐ 単一URLの連打
シナリオの作り方ですが、とにかくまずは単一URLの連打から始めるのがお勧めです。k6はこのようにわりと簡単に書けます。http.get
で実際にリクエストが飛んでレスポンスが返ってきます。check
でレスポンスに対してOKになる条件を定義します。これだけで単一URLを連打できます。
これを実行してみましょう。k6はCLIツールなのでrun
で実行します。どれぐらいの並列度で実行するかというvirtual users、vusという値がありますが、これも最初はとにかく1から始めます。何秒かけるかを設定して、さっきのシナリオのファイルを指定するとリクエストが飛んでいくことになります。出力は、k6は本当はもっと大量の出力を出すのですが、とりあえずこの3個を見ておけば良いというのを並べておきました。checksというのは先ほどのコードで、最後にステータスコードが200であることをチェックしていますが、これがどれだけ成功したか、失敗したかが出てきます。http_req_durationというのがレイテンシ、最後がスループットでhttp_reqsというのがリクエスト/secの値です。この3つを見た上で結果を評価します。
このスライドにある評価の順番が前後するとあまり実のある結果にならないので、気を付けてください。結果を評価する前に、まずサーバのメトリクスとk6の出力した結果に齟齬がないかを確認する必要があります。ここでメトリックスの取得がおかしかったら、モニタリングからやり直しです。
あと、エラーが出てないかチェックします。正常に処理できていないと性能を評価する以前の問題(簡単に言うと500エラーをものすごく高速に返せても意味がないみたいな話ですね)なので、エラーの原因は別途調べましょう。
ということで、ちゃんと結果を評価しながら並列度/サーバリソースを変えていくということですね。先ほども言ったように、エラーが出なくてレイテンシが保てている状態だと、スループットは並列数を増やすと増加する可能性があります。システムに余裕があれば並列処理したものも同時にさばけるからです。ただ、並列度をどんどん増やしていくと、どこかでシステムの性能は飽和して、これ以上スループットが上がらなくなったり、エラーがいっぱい出始めたりします。そうなったらどこかにサーバのボトルネックが生まれているはずなので、ボトルネックは何かをちゃんと調べた上で、さらに性能を求めたかったらサーバリソースを増強する、などいろいろな解決策を考えます。
シナリオ作成例その2 ー 複数のシナリオで本番のワークロードに近づける
今までずっと単一URL連打の話をしてきましたが、ある程度慣れてきたらシナリオを増やして、だんだん本番に近い状況を作れるようになります。シナリオの書き方についてはISUCON本の4章で解説しているので、具体例を見ながら参考にしてください。ただ、本番のアクセスパターンと負荷を完全に再現しようというのは無理なので、ほどほどで止めておくのがいいと思います。
1つのシナリオをものすごく長くしないことも大事で、たとえば「トップページを見てログインしてマイページからお知らせを見る」ぐらいのシナリオをまず1つ作る。トップページに行って検索をして、検索から詳細を見る別のシナリオを作る。そういうふうにシナリオ1個ずつはそんなに大きくしないようにして、複数のシナリオを混ぜて実行することで、本番に近いワークロードを再現できるようになります。これはk6の方に仕組みがあって、別々のファイルに書いたシナリオを、単独で実行したり組み合わせて使ったりできるようになっています。
実行時の留意点
では、実行時に気を付けることについてです。負荷をかける時間は、僕としては最低5分以上かけることをお勧めしています。解像度の問題があって、モニタリングが1分より短いとメトリクスが取れなかったりするんですね。実行開始から1分と、終了前の1分は負荷のかかっていない半端な時間が混じっているので、正確ではない値になっています。5分実行すれば、間の3分間は負荷がかかり続けた時間のメトリクスが取れるので、なるべく5分ぐらいかけていくのが良いでしょう。
また、エラーがいっぱい出ていたり、レイテンシがやたら遅いときにそれ以上負荷をかけても時間の無駄になるので、常に目標を意識して負荷を与えることに留意してください。性能を改善するのにサーバリソースや並列度を上げていくと、そのうちクライアントのほうがボトルネックになる可能性があります。k6はGoなのでわりと速いですが、ツールがあまり速くない言語でできていたり、シナリオですごく複雑なことをしていると、クライアントのほうでCPUを使ってしまい、なかなかサーバの性能を引き出せないといったことが起こります。
ここで、クライアントにいろいろ問題が起きる場合の頻出例を並べておきます。
ネットワーク帯域が上限を打つのはよくある話で、1Gbpsの回線しかないのに1MBのレスポンスを転送すると125リクエスト/secぐらいしか出ません。また、Goで自作する場合によくやりがちなミスとして、http.Response.Bodyを全部読み切らずにヘッダだけ見てOKとしてボディをクローズして次の処理を始めてしまうと、Keep-Aliveした状態で次のリクエストを送れなくなります。そうなると知らないうちに都度接続になってしまって、実際のブラウザやクライアントの挙動と変わってくるし、性能も出ないことになります。
また、並列度をどんどん上げて高速にしていくと起きがちなのが、「ファイルディスクリプタが1024しか開けない状態になっていて使い果たしてしまう」「通信するためのローカルネットワークポートの送信元ポートを使い果たしてこれ以上リクエストが送れなくなる」といったことが起きてきます。そうなるとOSレベルのチューニングが必要になることもあるので、これについてはISUCON本の9章で解説しているのでそちらを参照してください。
k6応用編 ‐ AWS Step FunctionsのDistributed Mapを使って分散実行
では応用編です。k6は1台で動作するツールで、クラスタリングはとくに公式には提供されていないのですが、公式の引用で「大きいインスタンスでちゃんとチューニングすると10 ‐ 30万qpsぐらいは出せる」と言っています。それ以上を求めたいとか、大きなインスタンスが用意できない場合にKubernetesで分散実行する方法が紹介されてはいますが、そのためにKubernetesクラスタを用意するのは大変かもしれません。
ここからいきなりAWSの話になりますが、「AWS Step Functions Distributed Map」を使ってk6を分散実行してみた話をします。AWS Step FunctionsというのはAWS上でワークフローを定義してそれを実行できるサービスです。その中で最近、Distributed Mapという大規模に並列実行できる機能ができました。1台で頑張ってチューニングして大量の負荷を発生させなくても、複数のk6のインスタンスを同時にLambdaの上で動かして、終わったら全部きれいにできます。
具体的なやり方としては、k6はデフォルトではテキストで出力をするのでちょっと扱いづらいので、サマリをJSONで吐き出すように細工しておきます。このhandleSummary
という関数を書いておくと、結果がファイルに出力されるようになるので、ここでchecksとhttp_reqs、リクエストのスループットを指定します。tmp/summary.jsonというところに結果が落ちます。
k6はGoで実装されてるシングルバイナリな実行ファイルなので、これをZipに入れて配置すればLambdaのカスタムランタイムで何も考えずに実行できます。Lambdaからシェルスクリプトを実行できるようにして、k6 run
というコマンドでシナリオを指定して負荷をかけ、先ほどのsummary.jsonをLambdaのレスポンスとして返していきます。こういうLambdaを用意します。
これをDistributed Mapに組み込みます。入力をどれぐらいの並列度で動かすかというのはStep Functionsの開始時の入力で設定できます。このJSON配列の1個1個の引数がLambdaの1個1個の引数になるので、["1", "1", "1"]
のように3要素の配列を指定すると3並列で動きます。
最後に実行が終わるとStep Functionsが全部自動的に結果を取りまとめてくれて、S3にボンとJSONで置いてくれるので、これを見れば集計もできそうな感じですね。
まとめ ‐ 負荷試験はやる前の準備が大切!
まとめですが、負荷試験はとにかくやる前の準備が非常に大事です。ゴールと目標を設定して、変化させる変数を意識してやるということですね。あっちをいじってこっちをいじって…とすると何を変えたか、性能が変わったのか変わらないのかもわからなくなるので、何を変化させるかを意識的に計画しましょう。また、この結果にどういう意味があるのかということをちゃんと評価すること。負荷を与える側にもやっぱりハマりどころがけっこうあるので、ここら辺はチューニングをするなりいろいろ知識が求められるところです。それをちょっとでも楽にするために分散実行してみたというアイデアをご紹介しました。
今日のお話は以上になります。ありがとうございました。