🐊

k6でGCEから8万RPSくらいの負荷をかけてみた

2021/10/01に公開

10万 RPS(Request Per Second)が要求されるサービスに携わっていて、たびたび負荷検証をしています。
今まで JMeter や locust を使ってきましたが、k6 が気になっていました。
そして、k6 を試せる機会があったので、その記録を記します。
k6 の概要についてはフューチャー技術ブログさんのこちらの記事で理解してもらえると良いかと思います。

大規模負荷検証のためのドキュメント

公式ドキュメントの Running large testsFine tuning OS に、k6 で大規模な負荷をかける時の注意点がまとめられています。
Benchmarking k6 on AWS には、現実的なシナリオを EC2 上から実行した場合の結果の値が記載されています。

AWS m5.large EC2 server

The m5.large instance has 8GB of RAM and 2 CPU cores.

➡️ VUS 6000

AWS m5.4xlarge

The m5.4xlarge instance has 64GB of RAM and 16 CPU cores.

➡️ VUS 20000

AWS m5.24xlarge

The m5.24xlarge has 384GB of RAM and 96 CPU cores.

➡️ VUS 30000

➡️ sleep を短くすると、188000 RPS

GCE だと、EC2 の変わりにこれらを使えばいいのかなと思っています。

AWS m5.large ≒ e2-standard-2
AWS m5.4xlarge ≒ e2-standard-16
AWS m5.24xlarge ≒ n2d-standard-96

GCEで試してみる

さて、本題です。普段 GCP を使うことが多いので、GCE に k6 をインストールして、大規模負荷をかけてみます。

シナリオによって消費するリソースの値は大きく変わると思いますが、参考にはなると思うので
(AppEngine 上の 10万 RPS を捌くことが確認できている API に対して)シンプルなシナリオを作り、負荷をかけてみました。
※ k6 の version は 0.34.1

scenario.js
import http from "k6/http";
import { sleep, check } from "k6";

// 適切な値に変更する!!!
// ----- ここから
const BASE_URL = "https://ENDPOINT";
const CONCURRENT_USERS = 3000;
// 最大 RPS は CONCURRENT_USERS / SLEEP_TIME_SECOND で計算する
const SLEEP_TIME_SECOND = 1;
const STAGES = [
  { duration: "10s", target: 1000 }, // warmup
  { duration: "10s", target: 2000 }, // warmup
  { duration: "3m", target: CONCURRENT_USERS },
  { duration: "20s", target: 0 },
];
// ----- ここまで

export let options = {
  stages: STAGES,
  thresholds: {
    http_req_duration: ["p(95)<1000"],
    checks: ["rate>0.95"],
  },
  summaryTrendStats: [
    "avg",
    "min",
    "med",
    "max",
    "p(90)",
    "p(95)",
    "p(99)",
    "count",
  ],
};

export default function () {
  let payload = JSON.stringify({
      // パラメータ
  });
  let params = {
    headers: {
      "Content-Type": "application/json",
    },
  };
  let res = http.post(`${BASE_URL}/PATH`, payload, params);
  check(res, {
    "status is OK": (r) => r.status === 200,
  });
  sleep(SLEEP_TIME_SECOND);
}

GCE に k6 セットアップ

GCE instance 作成

gcloud compute instances create k6-vm \
--preemptible \
--image=debian-10-buster-v20210817 \
--image-project=debian-cloud \
--machine-type=e2-standard-2 \
--project=YOUR_PROJECT \
--zone=YOUR_ZONE

シナリオスクリプトを転送(doc)

gcloud compute scp --recurse YOUR_DIR k6-vm:~ \
--project=YOUR_PROJECT \
--zone=YOUR_ZONE

(GCE インスタンスへ ssh(doc))

gcloud compute ssh --project=YOUR_PROJECT --zone=YOUR_ZONE k6-vm

k6 install(doc)

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6

OS fine-tuning

sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
sudo sysctl -w net.ipv4.tcp_timestamps=1
ulimit -n 250000

--compatibility-mode=basediscardResponseBodies--no-thresholds --no-summary などで更なるパフォーマンス向上が見込めるようですが、今回は使っていません。k6 Options

Monitoring

sudo apt-get install htop
sudo apt-get install iftop

htop
sudo iftop

e2-standard-2

毎秒6000人がリクエストする負荷をかけてみました。

const CONCURRENT_USERS = 6000;
const SLEEP_TIME_SECOND = 1;
const STAGES = [
  { duration: "10s", target: 1000 }, // warmup
  { duration: "10s", target: 2000 }, // warmup
  { duration: "10s", target: 3000 }, // warmup
  { duration: "10s", target: 4000 }, // warmup
  { duration: "10s", target: 5000 }, // warmup
  { duration: "3m", target: CONCURRENT_USERS },
  { duration: "20s", target: 0 },
];

結果、

vus_max 6000 となっています。

しかし、AppEngine のモニタリングを確認すると、実際にかかっていたのは 4000 RPS くらいのようです。

また、k6 を実行した GCE インスタンスの CPU 使用率は平均 80%くらいで、htop で見ていると 100%に達することもあったので、CPUリソースを使い切っていた感じがします。

e2-standard-16

毎秒2万人がリクエストする負荷をかけてみました。

const CONCURRENT_USERS = 20000;
const SLEEP_TIME_SECOND = 1;
const STAGES = [
  { duration: "10s", target: 1000 }, // warmup
  { duration: "10s", target: 2000 }, // warmup
  { duration: "10s", target: 4000 }, // warmup
  { duration: "10s", target: 6000 }, // warmup
  { duration: "10s", target: 8000 }, // warmup
  { duration: "10s", target: 10000 }, // warmup
  { duration: "10s", target: 12000 }, // warmup
  { duration: "10s", target: 14000 }, // warmup
  { duration: "10s", target: 16000 }, // warmup
  { duration: "10s", target: 18000 }, // warmup
  { duration: "3m", target: CONCURRENT_USERS },
  { duration: "20s", target: 0 },
];

結果、

vus_max 20000 となっています。

しかし、AppEngine のモニタリングを確認すると、実際にかかっていたのは 16000 RPS くらいのようです。

htop/iftop で見ている感じ、CPUもメモリもネットワークもまだ余裕がありそうでした。

n2d-standard-96 その1

毎秒5万人がリクエストする負荷をかけてみました。

const CONCURRENT_USERS = 50000;
const SLEEP_TIME_SECOND = 1;
const STAGES = [
  { duration: "10s", target: 1000 }, // warmup
  { duration: "10s", target: 2000 }, // warmup
  { duration: "15s", target: 4000 }, // warmup
  { duration: "15s", target: 6000 }, // warmup
  { duration: "15s", target: 8000 }, // warmup
  { duration: "15s", target: 10000 }, // warmup
  { duration: "15s", target: 12000 }, // warmup
  { duration: "15s", target: 14000 }, // warmup
  { duration: "15s", target: 16000 }, // warmup
  { duration: "15s", target: 18000 }, // warmup
  { duration: "15s", target: 20000 }, // warmup
  { duration: "15s", target: 22000 }, // warmup
  { duration: "15s", target: 24000 }, // warmup
  { duration: "15s", target: 26000 }, // warmup
  { duration: "15s", target: 28000 }, // warmup
  { duration: "15s", target: 30000 }, // warmup
  { duration: "15s", target: 32000 }, // warmup
  { duration: "15s", target: 34000 }, // warmup
  { duration: "15s", target: 36000 }, // warmup
  { duration: "15s", target: 38000 }, // warmup
  { duration: "15s", target: 40000 }, // warmup
  { duration: "15s", target: 42000 }, // warmup
  { duration: "15s", target: 44000 }, // warmup
  { duration: "15s", target: 46000 }, // warmup
  { duration: "15s", target: 48000 }, // warmup
  { duration: "3m", target: CONCURRENT_USERS },
  { duration: "20s", target: 0 },
];

結果、

vus_max 50000 となっていますが、AppEngine のモニタリングを確認すると、実際にかかっていたのは 23000 RPS くらいで少なすぎます。
htop/iftop で見ている感じ、CPUもメモリもネットワークもまだ余裕がありそうでした。

AWS EC2 の例では、VUS 30000 で sleep を短くして RPS を増やしていたので、VUS 50000のところが問題あるのかと仮説を立て、その2を試しました。

n2d-standard-96 その2

VUS は 25000人にして、sleep を短くすることで 5万 RPS に調整してみました。

const CONCURRENT_USERS = 25000;
const SLEEP_TIME_SECOND = 0.5;
const STAGES = [
  { duration: "10s", target: 1000 }, // warmup
  { duration: "10s", target: 2000 }, // warmup
  { duration: "10s", target: 3000 }, // warmup
  { duration: "10s", target: 4000 }, // warmup
  { duration: "10s", target: 5000 }, // warmup
  { duration: "10s", target: 6000 }, // warmup
  { duration: "10s", target: 7000 }, // warmup
  { duration: "10s", target: 8000 }, // warmup
  { duration: "10s", target: 9000 }, // warmup
  { duration: "10s", target: 10000 }, // warmup
  { duration: "10s", target: 11000 }, // warmup
  { duration: "10s", target: 12000 }, // warmup
  { duration: "10s", target: 13000 }, // warmup
  { duration: "10s", target: 14000 }, // warmup
  { duration: "10s", target: 15000 }, // warmup
  { duration: "10s", target: 16000 }, // warmup
  { duration: "10s", target: 17000 }, // warmup
  { duration: "10s", target: 18000 }, // warmup
  { duration: "10s", target: 19000 }, // warmup
  { duration: "10s", target: 20000 }, // warmup
  { duration: "10s", target: 21000 }, // warmup
  { duration: "10s", target: 22000 }, // warmup
  { duration: "10s", target: 23000 }, // warmup
  { duration: "10s", target: 24000 }, // warmup
  { duration: "3m", target: CONCURRENT_USERS },
  { duration: "20s", target: 0 },
];

結果、

実際かかっていたのは 20000〜30000 RPS くらいでその1と変わらずといった感じでした。

n2d-standard-96 その3

VUS は 25000人にして、さらに sleep を短くすることでどうなるか試しました(理論上は 12.5万 RPS)。

const CONCURRENT_USERS = 25000;
const SLEEP_TIME_SECOND = 0.2;
// 略

実際かかっていたのは 4万 RPS を超えることもあり(理論上の数値は大きく下回りますが)変わりました。
※sleep をさらに短くすることで、RPS をもう少し増やすことができるかもしれないです。
※htop/iftop で見ている感じ、CPUもメモリもネットワークもまだ余裕がありそうでした。

n2d-standard-96 その4

その3 と同じシナリオで、負荷をかけるサーバー(n2d-standard-96)を 2台にして実行してみました。

AppEngine に実際かかっていたのは 8万 RPS を超えることもあり、4万 RPS × 2台 の計算どおりの結果が得られました。

k6 を試した感想

JavaScript は普段使っている人が多いのではと思っています。これでシナリオを書けるのはハードルが低く、ありがたいです。
また、GCE インスタンス 1台でもかなりの負荷をかけることができました。
僕が携わっているサービスの 10万 RPSの負荷検証ケースでは、GCE インスタンス複数台から同時に実行することで満たせるので、k6 使っていこうと思っています。
※VUS が 30000 を超える大きな値にすると問題があるのかは不明なので、この辺りの情報持っている方は教えていただきたいです。

その他の情報

distributed execution

まず、Running large tests のトップにこのような記載があります。

The common misconception of many load testers is that distributed execution (ability to launch a load test on multiple machines) is required to generate large load. This is not the case with k6.

大規模な負荷をかけるのに、分散実行が必要だというのは誤りだそうです。

Benchmarking k6 on AWSではこのような記載があります。

The limit to this scalability is in the number of open connections. A single Linux machine can open up to 65 535 sockets per IP. This means that maximum of 65k requests can be executed simultaneously on a single machine. The RPS limit depends on the response time of the SUT. If responses are delivered in 100ms, the RPS limit is 650 000.

65k ソケット開けるので、もし 100msec 以内にレスポンス返せるなら、65万 RPSの負荷が可能だと。なるほど。

とはいえ、
distributed execution は考えられています。
こちらの issue で議論されているようです。

k6 Cloud

今回 k6 cloud は使用しませんでした。

k6 cloud では、シナリオを実行することもできますし、結果をストリーミングし k6 cloud 上で分析するという使い方もできます。

How many VUs can be run from the same Dedicated IP?

Tier 1 is used when there are 1-999 VUs in a load zone

Tier 2 is used when there are 1000-4001 VUs in a load zone

Tier 3 is used when there are more than 4001 VUs in a load zone

Tier 1 server handles up to 300VUs

Tier 2 server handles up to 1200VUs

Tier 3 server handles up to 5000VUs

最終的に 10万RPS の検証が求められたので、k6 cloud でシナリオを実行すると厳しそうと考えました。

また、結果を k6 cloud 上にあげるのは、トライアルだと 50回までとのことでした。(k6 run –out cloud 1回で 1つカウント)なので、試すのやめました。

終わりに

今回、k6 の公式ドキュメントをざっと全体的に読んだのですが、k6 の使い方だけでなく、負荷検証の基本知識もたくさん書かれていて、非常に勉強になりました。

例えば、Test Typesでは、Smoke TestLoad TestStress Test and Spike testingSoak Test の分類が書かれています。

The important thing to understand is that each test can be performed with the same test script. You can write one script and perform all the above tests with it. The only thing that changes is the test configuration, the logic stays the same.

そして、テストスクリプトは同じだよと。なるほど。

今後も(僕が)使うことがありそうな k6 のシナリオの例サンプルはいくつかこちらにあげています。
もし誰かの役に立つことがあれば。

Discussion