この記事はEDOCODE Advent Calendar 2024、12月02日(月)の記事です。
1つ前の記事は、KyoさんによるSkill Matrix - EDOCODE's Evaluation Systemでした。
また、グループ親会社であるWanoのAdvent Calendarもありますので、そちらもどうぞ!
そちらの方にも、GoでTSVからエラーレスポンス用のコードを自動生成するという記事を書いています。
リアルなAPIの負荷テストについて考える
負荷テストといえば、はるか昔からあるab
が有名ですね。もちろん、ab
でテストを行うこともできます。ヘッダーに認証情報を入れて、APIにアクセスすればよいですね。
ただ、これは、実際のアクセスに近いでしょうか?
実際のAPIに対しては、単一のシステムがアクセスしていない場合も多いでしょうし、ユーザーデータを取得するために、ユーザーIDのパラメータを追加する必要があるかもしれません。
1分間に数千ユーザーが1万アクセスするのと、単一ユーザーが1万アクセスする場合とは負荷が異なるかもしれません。
そうすると、動的にリクエストを変更する必要がありそうです。
また、大抵のAPIは複数同時に叩かれることが多いです。同時に叩かれるAPIに同じタイミングで負荷をかける必要があります。
つまり、実際に近いアクセスとは?
- 動的にリクエストを変更したい
- 複数のAPIに同時に負荷をかけたい
ということになります。もちろん、IPとか環境要因も異なるはずですが、そこはそんなに重要ではないと思いますので無視します。
1. 動的にリクエストを変更したい
使うツールを変えましょう。wrk を使います。
wrk
wrk は負荷テストツールです(wrk2もあるのですが、なんか出力がいまいちだったので、wrkを使いました)。
wrkには、Luaのscriptを渡すことができます。これで、負荷テストのリクエストを変更することができます。
以下のスクリプトは、load-test-headers.txt
を読み込んで、headers
に入れ、リクエストごとにheaders
からランダムに一つ取り出し、X-User-ID
ヘッダーに取り出した値を設定するというものになります。ちなみに、Luaは書いたことなかったので、ChatGTPに頼みました。
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
は、適当に生成すればよいですが、僕は、以下のようにしてつくりました。
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
を使う必要があるかと思います。
#!/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の自分だけのパーサーをパーサーコンビネーターで作ってみようになります。
私たちWanoグループでは人材募集をしています。興味のある方は、下記のリンクからぜひ募集中の求人をご確認ください!
JOBS | Wano Group