Slide 1

Slide 1 text

k6による負荷試験 入門から実践まで 2023.06.07 TechFeed Experts Night#20 @fujiwara 藤原俊一郎

Slide 2

Slide 2 text

@fujiwara 面白法人カヤック SREチーム ISUCON 1,2,5,11 優勝4回 ISUCON 3,8,12,13 運営(出題)4回 github.com/kayac/ecspresso Amazon ECS デプロイツール github.com/fujiwara/lambroll AWS Lambda デプロイツール

Slide 3

Slide 3 text

今回話す「負荷試験」の範囲 測定対象のWebサービス(アプリケーション)に対して 機械的にリクエスト(HTTPなど)を送信して 「性能(パフォーマンス)」を計測する ところで 「性能」 とは…?

Slide 4

Slide 4 text

負荷試験をする「前」に考えること ゴールを決める レイテンシ(レスポンスタイム)が目標? どのレスポンスを何秒で返せればOKなのか 平均値? 中央値? P99? max? そのレイテンシが保てる最大の並列アクセス数は? スループット(単位時間内に処理できるリクエスト数)が目標? レイテンシ要件はない? レイテンシ1secで1,000並列 レイテンシ10secで10,000並列(どちらも1,000req/sec) あるサーバーリソースでのxxの最大化が目標?

Slide 5

Slide 5 text

負荷試験をする「前」に考えること その試験で測りたいものを言語化する レイテンシ スループット サーバーリソースの利用率・利用量(コストも含む) 運用中に実際に起きるどのような状況で、これらの数値目標を達成するのか 【具体的な例】 状況 : Push通知から○○分以内に○○人のユーザーがアプリを起動する シナリオ : アプリが起動してホーム画面に遷移するときに発生するAPIアクセス 目標 : 平均レイテンシ250msec、P99 2sec、サーバーエラーなし 詳しくは ISUCON本 1章へ

Slide 6

Slide 6 text

負荷試験をする「前」にやること 負荷をかける対象のサーバーリソースは(できるだけ)固定する →「AutoScaling」的なものを止めておく 負荷試験には 「変数が多い」 変数の例: シナリオ(測定対象URL)、リクエストの送信レート、並列度… 固定できないものは、せめてそのサイズ(キャパシティ)を同時に記録しておく

Slide 7

Slide 7 text

負荷試験をする「前」にやること サーバー側のメトリクスを本番同様に取得して、ダッシュボードを作る 最低でも以下を一目で見られる状態に 各種CPU使用率 HTTP スループット(req/sec)、レイテンシ、statusコード別 データベースへのクエリ数(など) アプリケーションサーバー固有の情報 同時リクエスト処理数(worker数) job queueがあるならそのメトリクス ネットワークトラフィック

Slide 8

Slide 8 text

ツールの選び方 最初におすすめ - k6 https://k6.io JavaScriptでシナリオが書ける (Node.JSではない) 実行エンジンは Go (軽量、省メモリ)

Slide 9

Slide 9 text

他のツール/自作を検討したほうがいい場合も リクエストやレスポンスbodyが特殊 (k6でもgRPC, GraphQLはサポート) gRPC https://k6.io/docs/using-k6/protocols/grpc/ GraphQL https://k6.io/blog/load-testing-graphql-with-k6/ JavaScriptよりも他の言語が得意な人が多い場合 複数シナリオ間で連係や同期を取る必要がある場合 ゲームのレイドバトルとか 自作用Go言語フレームワーク isucandar https://github.com/isucon/isucandar ISUCON本 付録Bに解説あり

Slide 10

Slide 10 text

シナリオの作り方 まず単一URL連打から import http from 'k6/http' import { check } from 'k6' export default function () { let res = http.get('https://example.com/') check(res, { 'success': (r) => r.status === 200 }) } いきなり複雑なものは書かない

Slide 11

Slide 11 text

k6で実行 最初は「並列度(vus=virtual users)」= 1 から $ k6 run --vus 1 --duration 300s simple.js checks.............: 99.98% ✓ 52644 ✗ 6 http_req_duration..: avg=285.56ms min=32.93ms med=144.61ms max=30.05s p(90)=521.34ms p(95)=788.36ms http_reqs.. ......: 52650 172.404676/s (いっぱい出力があるけど)とりあえずこの3個を見る checks: コード内の check() の成功・失敗数 レイテンシ: http_req_duration スループット: http_reqs

Slide 12

Slide 12 text

結果を評価する 1. k6の出力とサーバー側のメトリクスに齟齬がないか →メトリクスの取得がおかしい可能性が高いので見直す 2. checksは想定通りか →エラーが目標/想定より多い場合は性能を評価する意味がない 原因を調査する 3. レイテンシは目標に達しているか →これ以上並列度を増やすのはたいてい無意味 ボトルネックの調査をする 4. スループットは目標に達しているか or →並列度を2倍にして変化を見る 無闇に変数を変えて試さない

Slide 13

Slide 13 text

結果を評価しながら並列度/サーバーリソースを増やしていく 並列度=1で エラー率/レイテンシが目標に達している スループットが目標に達していない →並列度を増やすことでスループットが増加する「可能性がある」 並列度を増やしていくとある時点で… スループットが上がらない エラー率/レイテンシが目標に達しなくなる →「サーバーリソースを増強する」ことでレイテンシが改善/スループットが増加する「可能 性がある」 無闇にサーバーリソースを変えて試さない(時間の無駄)

Slide 14

Slide 14 text

シナリオを増やしていく 1. 参照系のリクエスト/画面遷移を足す 2. ログイン/更新系のリクエストを足す (コードの具体例はISUCON本 4章に) 本番のアクセスパターンと負荷を網羅しようと思うのはやめよう(大変) 1. 一連のシナリオごとに実行する 例: トップ→ログイン→マイページ→お知らせを見る これだけでも特定の箇所で大きな問題がある場合には改善点が分かる 2. 複数のシナリオを混ぜて実行する 複数シナリオを実際に想定される程度の割合で混ぜる

Slide 15

Slide 15 text

負荷試験中に気を付けること 負荷は5分以上掛ける メトリクスの解像度が1分の場合、実行開始と終了時を除いた3分間のメトリクスし か信用できない エラー率/レイテンシが目標に達しない場合、並列度を増やさない たいてい状況は悪化するので時間の無駄 常に「目標」を意識して負荷を与える、結果を評価する ベンチマーカーがボトルネックになっていないか気を付ける シナリオが重い、ツールが速くない場合、クライアントが先に根を上げる

Slide 16

Slide 16 text

クライアント側で起きる問題の頻出例 ネットワーク帯域が上限を打っている 1Gbpsの回線で1MBのレスポンスを取得→(たったの)125req/sec(=125MB/sec) HTTP Keep-Aliveが無効になっている 3 way handshake のオーバーヘッドが大きい 無駄にファイルディスクリプタ(ソケット)を使う Goで自作した場合にやりがち: http.Response.Body を全て読み切らない →次のリクエスト時は新規のTCP接続になる ファイルディスクリプタを使い果たしている ulimit -n (max open files)がデフォルトの1024 ローカルポート(エフェメラルポート)が枯渇している 大量/高速にリクエストを送受信すると使い果たす ISUCON本 9章をどうぞ

Slide 17

Slide 17 text

k6 応用編 k6は基本的に、シングルプロセス(1台)で動作するツール クラスタリングの仕組みは公式には提供されていない Unless you need more than 100,000-300,000 requests per second (6-12M requests per minute), a single instance of k6 is likely sufficient for your needs. https://k6.io/docs/testing-guides/running-large-tests/ 公式 「10〜30万req/secぐらいは1インスタンスでいけるで」(チューニング頑張れば) 分散実行をk8sでやる方法も紹介はされているが… https://k6.io/blog/running-distributed-tests-on-k8s/

Slide 18

Slide 18 text

AWS Step Functions Distributed Mapで k6を分散実行してみた k6を気軽に分散実行したいので試してみた Step Functions(SFn): AWS上でworkflowを定義/実行で きるサービス Distributed Map: 大規模(1000並列以上)に並列実行して 結果を集約できる k6をAWS Lambdaで実行できるようにする →SFn Distributed Mapで並列実行!! 1台で大量の負荷を発生させるチューニングが不要に

Slide 19

Slide 19 text

k6から必要な結果だけJSON出力 シナリオの.jsに handleSummary() を定義すると https://k6.io/docs/results-output/end-of-test/custom-summary/ export function handleSummary(data) { return { '/tmp/summary.json': JSON.stringify({ 'checks': data.metrics.checks.values, 'http_reqs': data.metrics.http_reqs.values, }) } } 結果がファイルに出力される {"checks":{"rate":1,"passes":82,"fails":0}, "http_reqs":{"count":82,"rate":27.087064577167027}}

Slide 20

Slide 20 text

k6をAWS Lambdaで実行 k6が配布しているLinuxバイナリはstatic link Zipに含めてしまえばAWS Lambdaのカスタムランタイム(provided.al2)で実行できる bootstrap > https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes- walkthrough.html function handler () { cd $LAMBDA_TASK_ROOT # k6 でシナリオを実行する # $1: 関数の引数としてvus を取る ./k6 run --quiet --vus "$1" --duration 30s simple.js # handleSummary で出力した結果をLambda のレスポンスにする cat /tmp/summary.json }

Slide 21

Slide 21 text

SFn Distributed MapでLambdaを並列実行 SFnへの入力例: 3並列でそれぞれvus=1で実行する ["1", "1", "1"] // この値はvus として使われる 配列の値が各Lambda関数への引数になる ./k6 run --vus "$1" --duration 30s simple.js

Slide 22

Slide 22 text

分散実行の結果はS3に保存される "Output" : Lambdaが返した結果のJSON文字列 [ { "Input": "\"1\"", "Output": "{\"checks\":{\"rate\":1,\"passes\":82,\"fails\":0}, \"http_reqs\":{\"count\":82,\"rate\":27.087064577167027}}", "StartDate": "2023-06-06T16:07:24.203Z", "StopDate": "2023-06-06T16:07:41.272Z" // ... }, // ... // 並列分実行した結果が並ぶ ] あとはこれを集計すればよさそう!?

Slide 23

Slide 23 text

まとめ 負荷試験はやる「前」の準備が大事 ゴールと目標の設定 いたずらに変数を増やさないために準備 負荷試験をやっているときは「評価」が大事 結果を評価してから次の試行を いたずらに試行錯誤で時間を無駄にしない 負荷を与える側にもハマりどころはいろいろある クライアント側のボトルネックに気がつかないと時間を無駄に k6をStep Functionsで分散実行してみた 気軽に分散実行できてよいのでは?