はじめに
Phone Div Backend チームの西園です。
私たちのチームでは、システムの性能を向上させるために k6 と Datadog を利用した負荷テストを実施しました。本記事では、その際に利用したツールや実施方法について共有します。
想定読者
- 負荷テストをやったことがない方
- k6 を利用したことがない方
- 負荷テストをやってみたいが方法がわからない方
- k6 の結果を Datadog と連携したい方
負荷テストとは
負荷テストは、システムに負荷を与えて挙動を観測するテストです。これにより以下のような情報を得られます:
- システムのボトルネックの特定。
- システムが耐えられる最大負荷の確認。
- システムのパフォーマンス向上に必要な改善点の発見。
負荷テストの種類
負荷テストには目的に応じていくつかの種類があります:
- Smoke Test: 最小負荷で基本動作を確認するテスト。
- Average Load Test: 運用時の平均負荷を再現するテスト。
- Stress Test: システムの限界を探るための負荷をかけるテスト。
- Soak Test: 長時間負荷をかけてシステムの安定性を確認するテスト。
- Spike Test: 短時間で急激に負荷を増加させた際の挙動を確認するテスト。
- Breakpoint Test: 負荷を徐々に増やし、システムが壊れるポイントを特定するテスト。
注意点
負荷テストを行う際は、できるだけ本番環境と近いテスト環境を用意することが重要です。特に以下の点に注意してください:
- インフラリソース: テスト環境と本番環境でのCPU、メモリ、ネットワーク条件を可能な限り一致させる。
データ量: データベース内のデータ量を本番に近い状態にすることで、より正確な結果を得られる。
k6 は Grafana Labs が提供するオープンソースの負荷テストツールです。以下の特徴があります:
k6とは
- 幅広いプロトコル対応: HTTP、WebSocket、gRPC などに対応。
- 柔軟なスクリプト作成: JavaScript を用いて、シナリオに応じたスクリプトを作成可能。
- 多様な負荷パターン: Executor を利用して、一定負荷や段階的負荷増加などのシナリオを設定可能。
k6スクリプトの実装方法
では実際にどのようにスクリプトを作成するかを見てみましょう。
以下は Read 系のエンドポイントに負荷を掛けるスクリプトの例です。
import http from "k6/http"; import { check } from "k6"; export const options = { scenarios: { test1: { executor: "ramping-arrival-rate", exec: "testRequests", startRate: 1, timeUnit: "1s", preAllocatedVUs: 25, maxVUs: 50, stages: [ { target: 25, duration: "10s" }, { target: 25, duration: "50s" }, { target: 0, duration: "10s" }, ], }, }, }; export const setup = () => { // リクエストを送る前の事前処理を記載 } // もしsetup関数で取得したトークンなどを利用する場合は引数を設定する export const testRequests = () => { const requestParams = { headers: { // 必要なヘッダー情報を追加 }, }; const url = "" const body = { // bodyが必要な場合は記載 }; const res = http.post(`${url}`, JSON.stringify(body), requestParams); // リクエストが想定通りかを検証 check(res, { "status is 200": (r) => r.status === 200, }); };
上記の scenario は ramping-arraival-rate を設定しています。これは負荷を段階的に上げたり下げたりすることができます。
各パラメーターの説明は以下です。
- executor: どのような負荷を与えていくか(負荷を徐々に上げていくなど)
- exec: 実行する関数名(testRequestsという関数を実行する)
- startRate: testRequests関数を実行する単位
- timeUnit: どのくらいの間隔でtestRequests関数を実行するか(startRate / timeUnit でRPSを表現)
- preAllocatedVUs: あらかじめ用意するバーチャルユーザー数
- maxVUs: 負荷テストで利用する最大のバーチャルユーザー数
- stages: どのような間隔で負荷を増減させるか
つまり以下の内容の負荷をかけることになります。
1RPS から負荷を与えていき、10s かけて 25RPS まで負荷を上げる。
そして 50s 間 25RPS を維持し、最終的に 10s かけて 0RPS まで負荷を落とす。
他にも選択できる Executor はありますので詳細を知りたい方は以下の公式ドキュメントを参考にしてください。
参考: https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/
k6スクリプトの実行とDatadogとの連携
Datadog にダッシュボードを作成
Datadog を使用して負荷テスト結果を可視化するには、以下の手順を実施します:
- k6 のインテグレーションを有効化
- サイドメニューIntegrationから k6 を検索して有効化してください。
- 必要なメトリクスを表示するダッシュボードを作成
以下は、実際に利用したダッシュボードの例です:
各メトリクスは一例ですが、以下のような設定をします。
ここで後ほど Docker コンテナで設定する DD_HOSTNAME と同じ値を host:<設定値> に設定しておきます。
後ほど説明しますがこの設定をすることで特定の負荷テストにターゲットを絞ってメトリクスを表示できます。
負荷を与える側のマシンを用意
まず前提として負荷を与える側にも負荷がかかるのである程度のスペックを用意したマシンが必要になります。
弊社では主に AWS を利用しているので、負荷テスト時はインスタンスタイプが m4.4xlarge の EC2 を用意して負荷テストを行いました。
インスタンスタイプは負荷に耐えうる、かつ少し余裕を持ったスペックを選定するのをお勧めします。
コンテナの用意
負荷をかける上で今回チームでは Docker を利用して EC2 内にコンテナを立てて負荷テストを実行しました。
まず以下のような docker compose 用のファイルを用意します。
services: k6: container_name: k6 image: grafana/k6:latest networks: - k6 ports: - '6565:6565' environment: - K6_STATSD_ENABLE_TAGS=true - K6_STATSD_ADDR=datadog:8125 volumes: - # スクリプトのパスをマウントする depends_on: - datadog datadog: container_name: datadog-agent image: datadog/agent:latest networks: - k6 ports: - '8125:8125/udp' environment: - DD_SITE=datadoghq.com - DD_API_KEY=<YOUR_DD_API_KEY> - DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1 - DD_HOSTNAME=<YOUR_HOSTNAME> volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /proc/:/host/proc/:ro - /sys/fs/cgroup/:/host/sys/fs/cgroup:ro
Datadog エージェント用のコンテナの環境変数の DD_API_KEY には Datadog で利用している API key を利用してください。
また DD_HOSTNAME には特に指定はないですが、チーム名などわかりやすい名前を設定することをお勧めします。この値は Datadog のダッシュボードでメトリクスを指定する際に特定のテスト結果だけをメトリクス上に反映するために利用します。
参考: https://docs.datadoghq.com/ja/integrations/k6/
コンテナの準備ができたら以下のコマンドを実行して負荷をかけます。
docker compose run k6 run --out statsd <コンテナのスクリプトパス>
ただし、現在 k6 のバージョン v0.55.0 で statsd のオプションは廃止されてしまったので
xk6-output-statsd extension を利用して実行する必要があります。
詳しくは公式ドキュメントを参照ください。
参考: https://grafana.com/docs/k6/latest/results-output/real-time/datadog/
負荷テストの実施
k6 の実行が完了すると以下のような結果の指標が表示されます。
以下の指標は今回実施した際の指標です。
k6 result
/\ Grafana /‾‾/ /\ / \ |\ __ / / / \/ \ | |/ / / ‾‾\ / \ | ( | (‾) | / __________ \ |_|\_\ \_____/ INFO[0133] Failed on <executed api path>: expected 200 but got 500 source=console INFO[0133] Failed on <executed api path>: expected 200 but got 500 source=console INFO[0133] Failed on <executed api path>: expected 200 but got 500 source=console INFO[0133] Failed on <executed api path>: expected 200 but got 500 source=console INFO[0133] Failed on <executed api path>: expected 200 but got 500 source=console INFO[0133] Failed on <executed api path>: expected 200 but got 500 source=console INFO[0134] Failed on <executed api path>: expected 200 but got 500 source=console INFO[0734] Failed on <executed api path>: expected 200 but got 500 source=console ✗ status is 200 ↳ 99% — ✓ 366602 / ✗ 8 █ setup ✓ authenticated in successfully checks.........................: 99.99% 366605 out of 366613 data_received..................: 3.1 GB 1.5 MB/s data_sent......................: 54 MB 26 kB/s dropped_iterations.............: 16827 8.005593/s http_req_blocked...............: avg=3.57µs min=1.76µs med=1.93µs max=14.21ms p(90)=2.12µs p(95)=2.34µs http_req_connecting............: avg=417ns min=0s med=0s max=8.2ms p(90)=0s p(95)=0s http_req_duration..............: avg=288.92ms min=62.39ms med=239.73ms max=2.57s p(90)=491.68ms p(95)=621.08ms { expected_response:true }...: avg=288.89ms min=62.39ms med=239.73ms max=2.57s p(90)=491.64ms p(95)=621.01ms http_req_failed................: 0.00% 8 out of 366613 http_req_receiving.............: avg=199.78µs min=18.52µs med=169.85µs max=208.51ms p(90)=304.83µs p(95)=513.89µs http_req_sending...............: avg=77.22µs min=28.42µs med=72.18µs max=1.16ms p(90)=91.57µs p(95)=100.69µs http_req_tls_handshaking.......: avg=935ns min=0s med=0s max=9.5ms p(90)=0s p(95)=0s http_req_waiting...............: avg=288.65ms min=62.24ms med=239.44ms max=2.56s p(90)=491.35ms p(95)=620.75ms http_reqs......................: 366613 174.419363/s iteration_duration.............: avg=1.44s min=643.55ms med=1.34s max=5.7s p(90)=1.96s p(95)=2.42s iterations.....................: 73322 34.883587/s vus............................: 0 min=0 max=60 vus_max........................: 60 min=50 max=60 running (35m01.9s), 00/60 VUs, 73322 complete and 0 interrupted iterations
※ executed api path の部分にはリクエストを送信したパスが表示されます。
上記の結果を見ると平均で約 174 RPS の負荷をかけた結果 99% のリクエストは成功しているが、8回リクエストが失敗していることがわかります。
Datadog
Datadog の指標も一部見てみましょう。
HTTP リクエストの動作を見ると、理想的な台形型の負荷曲線にはなっておらず、負荷上昇フェーズでリクエストが失敗する、または適切に送信されないケースが見られました。(通常、問題がなければ負荷曲線は綺麗な台形になります)
さらに詳細な指標を確認した結果、データベースとのコネクションタイムアウトが発生していることが判明しました。
チームで議論した結果、以下の仮説を立てました: 「確保しているコネクションプールの上限を超えるリクエストが発生し、API と RDS Proxy 間の TLS 3way ハンドシェイクに時間がかかりタイムアウトした可能性がある。」
この問題は本番環境でも時折発生していたため、仮説に基づきプールサイズの調整を行い監視を続けた結果、これまでに出ていた 500 エラーを減らすことができました。
この負荷テストを通してエラーの原因に対して仮説を立て、システムの改善に至ることができたのでやって良かったと思っています。
その他利用できるツール
普段、Kubernetes や RDS のメトリクスを Datadog で取得しています。
なので負荷テスト時にそれぞれの指標が一緒に見れるように Datadog を利用して k6 の指標を確認しました。しかし他のツールを利用して結果を観測することもできます。
たとえば Grafana dashboards を利用すると以下の画像のようなリッチな感じで結果を見ることもできます。
引用:https://grafana.com/docs/k6/latest/results-output/grafana-dashboards/
まとめと今後の展望
k6 と Datadog を利用することで、簡単かつ効果的に負荷テストを実施できます。今後は、Write系エンドポイントへの負荷テストや、RDS Proxy間のパフォーマンス改善に取り組んでいく予定です。
もし負荷テストを検討している方がいれば、ぜひ試してみてください!