実際のアクセスに近づけたAPIの負荷テストを行う

この記事は[EDOCODE Advent Calendar 2024](https://adventar.org/calendars/10618)、12月02日(月)の記事です。

1つ前の記事は、Kyoさんによる[Skill Matrix - EDOCODE's Evaluation System](https://edocode.notion.site/skill-matrix-edocode-evaluation-system)でした。
また、グループ親会社である[WanoのAdvent Calendar](https://qiita.com/advent-calendar/2024/wano-group)もありますので、そちらもどうぞ！
そちらの方にも、[GoでTSVからエラーレスポンス用のコードを自動生成する](https://qiita.com/ktat/items/1cb5e68585392f4145e2)という記事を書いています。

## リアルなAPIの負荷テストについて考える

負荷テストといえば、はるか昔からある`ab`が有名ですね。もちろん、`ab`でテストを行うこともできます。ヘッダーに認証情報を入れて、APIにアクセスすればよいですね。

ただ、これは、実際のアクセスに近いでしょうか？

実際のAPIに対しては、単一のシステムがアクセスしていない場合も多いでしょうし、ユーザーデータを取得するために、ユーザーIDのパラメータを追加する必要があるかもしれません。
1分間に数千ユーザーが1万アクセスするのと、単一ユーザーが１万アクセスする場合とは負荷が異なるかもしれません。

そうすると、動的にリクエストを変更する必要がありそうです。

また、大抵のAPIは複数同時に叩かれることが多いです。同時に叩かれるAPIに同じタイミングで負荷をかける必要があります。

### つまり、実際に近いアクセスとは？

1. 動的にリクエストを変更したい
1. 複数のAPIに同時に負荷をかけたい

ということになります。もちろん、IPとか環境要因も異なるはずですが、そこはそんなに重要ではないと思いますので無視します。

## 1. 動的にリクエストを変更したい

使うツールを変えましょう。[wrk](https://github.com/wg/wrk) を使います。

### wrk

wrk は負荷テストツールです([wrk2](https://github.com/giltene/wrk2)もあるのですが、なんか出力がいまいちだったので、wrkを使いました)。
wrkには、[Lua](https://www.lua.org/)のscriptを渡すことができます。これで、負荷テストのリクエストを変更することができます。

以下のスクリプトは、`load-test-headers.txt`を読み込んで、`headers` に入れ、リクエストごとに`headers`からランダムに一つ取り出し、`X-User-ID`ヘッダーに取り出した値を設定するというものになります。ちなみに、Luaは書いたことなかったので、ChatGTPに頼みました。

```lua
local headers = {}

function init()
  math.randomseed(os.time())
  local file = io.open("load-test-headers.txt", "r")
  for line in file:lines() do
      headers[#headers + 1] = line
  end
  file:close()
end

request = function()
  random_value = headers[math.random(#headers)]
  wrk.headers["X-User-ID"] = random_value
  return wrk.format(wrk.method, wrk.path, wrk.headers, nil)
end
```
`load-test-headers.txt`は、適当に生成すればよいですが、僕は、以下のようにしてつくりました。

```shell
perl -e 'my @letters = ("a" .."z", 0..9, "A".."Z"); for (1..10000) { my \$s = ""; \$s .= \$letters[int(rand @letters)] for 1..32; print \$s,"\n"}' > load-test-headers.txt
```
## 2. 複数のAPIに同時に負荷をかけたい

これは簡単ですね。Shell scriptでwrkを並列実行すればよいです。

### Shell scriptでwrkを実行する

以下のようになります。ただ、注意点として、大量の数でテストする場合は、open files のlimitを変更する必要があると思います。helpに書いてますが、`ulimit`を使う必要があるかと思います。

```bash
#!/bin/bash

if [ "$3" = "" ]; then
  cat <<_USAGE
NAME
  load-test.sh ... load test for real situation using wrk

USAGE
   ./load-test.sh \$count \$threads \$duration [\$timeout]

SYNOPSIS
   ./load-test.sh 100 8 1m 2s

NOTE
   ulimit may be required when you test with large number of connections.

     ulimit -n (num of \$count*\$threads*10)

_USAGE
  exit 1
fi

cd "$(dirname $0)"

count=$1
threads=$2
duration=$3
timeout=$4

if [ "$timeout" = "" ]; then
  timeout=2s
fi

t=$(date +"%Y.%m.%d-%H.%M.%S")
log_file="wrk-$t-$count-$threads-$duration-$timeout.log"

if [ ! -e "load-test-headers.txt" ]; then
     perl -e 'my @letters = ("a" .."z", 0..9, "A".."Z"); for (1..10000) { my $s = ""; $s .= $letters[int(rand @letters)] for 1..32; print $s,"\n"}' > load-test-headers.txt
fi

echo "count: $count, threads: $threads, duration: $duration"

do_wrk() {
  path=$1
  # 適当に変えてください
  url="https://example.com$path"
  echo $url

  wrk --timeout 3s -t$threads -c$count -d$duration -s load-test-script.lua -H "User-Agent: wrk-load-test" $url >> $log_file
}

# 適当に変えてください
do_wrk /api/1 &
do_wrk /api/2 &
do_wrk /api/3 &
do_wrk /api/4 &

count=1
threads=1
# ヘルスチェックのパスがあれば、負荷テスト中にヘルスチェックも叩いたほうが良いでしょう
do_wrk /health_check

echo "log_file: $log_file"
```

## 余談: ヘルスチェックについて考える

先程の Shell scriptの最後に、ヘルスチェックもテストしていますが、ヘルスチェックは意図的にある程度重たくしたほうが良いと思います。少なくとも、"OK"みたいな文字を返すだけなのは、あまり意味がないと思います。

というのは、負荷テスト中に他のAPIが重いのに、ヘルスチェックが軽いと、Load Balancerのチェックとしてはあまり意味がないからです。DBにある程度のSQLで検索してみたりとか、専用のテーブルにisnertしてみるとかをやると良いかもしれません。

## 終わり

これで、ちょっとリアルに近い負荷テストができるようになりました。

厳密には、以下の部分は、4つとも同じヘッダで叩きたい気はしますが、まぁ、そこまでしなくても良いでしょう。

```
do_wrk /api/1 &
do_wrk /api/2 &
do_wrk /api/3 &
do_wrk /api/4 &
```

以上、お役に立てば幸いです。

明日、12月03日(火)は、@fujitayyの[自分だけのパーサーをパーサーコンビネーターで作ってみよう](https://qiita.com/fujitayy/items/be7cfaa6c669b17247e6)になります。

私たちWanoグループでは人材募集をしています。興味のある方は、下記のリンクからぜひ募集中の求人をご確認ください！
[JOBS | Wano Group](https://group.wano.co.jp/jobs/)