こんにちは、メディアプラットフォーム事業部の多保です。
当社は動画配信のプラットフォームを提供しており、
動画配信の開発プロセスにおいて避けて通れないのが負荷試験になります。
どのくらいの負荷に耐えうるアーキテクチャになっているか? 実装になっているか?
負荷試験によって事前に把握し、改善することが重要です。
今回は当社で活用している負荷試験のツールの紹介と環境構築、その使い方について簡単に紹介しつつ
最終目標として「秒間1万リクエストの負荷をかけることができる負荷試験環境の構築」を行います。
本記事の内容はMacでの操作を想定しています。
- 負荷試験ツールLocustについて
- Locustをインストールしよう
- ローカルでLocustを起動してみよう
- 簡単なAPIを作成しよう
- ローカルから負荷をかけてみよう
- 負荷試験の結果を見てみよう
- テストシナリオを構築してみよう
- AWSでLocustを起動してみよう
- AWSから負荷をかけてみよう
- 秒間1万リクエスト以上の負荷をかけてみよう
- まとめ
負荷試験ツールLocustについて
私のチームでは負荷試験ツールとしてLocustを採用しています。
「Locust」という名前は「群れで行動するバッタの種」にちなんで名付けられたそうです。
ちょっと気持ち悪いですね。
特徴としては下記のことがあげられます。
- Pythonでテストシナリオを作成する
- テストの進行状況をリアルタイムで表示するWeb UI
- 分散型でスケーラブル - 何十万もの同時ユーザーをサポート
Locustをインストールしよう
まずはローカルの環境を構築してみましょう。
Python のインストール (3.7 以降)
本記事では深く触れません。
下記などを参考にPython3.7以降のインストールをお願いします。
Properly Installing Python — The Hitchhiker's Guide to PythonLocustをインストール
$ pip3 install locust
Locustのバージョン確認
$ locust -V locust 2.14.2 from /usr/local/lib/python3.10/site-packages/locust (python 3.10.6)
上記のような表示になればokです。
ローカルでLocustを起動してみよう
下記のようにlocustというフォルダ配下にlocustfile.pyというファイルを作成しましょう。
./locust └── locustfile.py
locustfile.pyには下記を記述しておいてください。
locustfile.py
from locust import HttpUser, task class HelloWorldUser(HttpUser): @task def hello_world(self): self.client.get("")
./locustの配下でlocust
を実行してみましょう。
下記のような表示になっていれば起動できています。
$ locust [2023-01-24 15:04:58,214] .local/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces) [2023-01-24 15:04:58,228] .local/INFO/locust.main: Starting Locust 2.14.2
http://localhost:8089にアクセスしてLocustのWeb UIを見てみましょう。
入力欄は上から下記のようになってます。
「Start swarming」を押下するとホストに対して負荷をかけ始めます。
Number of users (peak concurrency): 最大ユーザー数 Spawn rate (users started/second) : 1秒ごとに増えるユーザー数 Host (e.g. http://www.example.com): 負荷をかける対象のホスト名
ここまででローカルにてLocustを起動できましたが、 負荷をかける対象がないのでAWS Lambdaを使用して簡単なAPIを作成していきましょう。
すでに負荷をかける対象がある方は「簡単なAPIを作成しよう」を読み飛ばしてください。
簡単なAPIを作成しよう
AWS Lambdaを使用して簡単なAPIを作成していきます。
AWSコンソールからLambda > 関数 > 関数の作成と遷移してLambdaを作成します。
関数名をdummyAPIとし、その他の入力は下記画像に合わせてください。
詳細設定から「関数URLを有効化」をチェックし、「認証タイプ」をNONEに設定して関数をpublicにします。
「オリジン間リソース共有 (CORS) を設定」はチェックを入れておいてください。
関数が作成し終わると関数URLのリンクが表示されているのでアクセスしてみます。
下記の画像のようにブラウザに"Hello from Lambda!"が表示されていればokです。
下記の画像のように"Hello from Lambda!"の部分を
"関数名"に書き換えて、「Deploy」を押下しましょう。
下記の画像のようにブラウザに"関数名"が表示されていればokです。
同じ手順で関数名「dummyAPI2」も作成しておいてください。
「dummyAPI」と「dummyAPI2」の二つの関数ができていればokです。
ローカルから負荷をかけてみよう
もう一度./locustの配下でlocust
を実行してみましょう。
下記のような表示になっていれば起動できています。
$ locust [2023-01-24 15:04:58,214] .local/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces) [2023-01-24 15:04:58,228] .local/INFO/locust.main: Starting Locust 2.14.2
下記の画像のように入力欄を埋めましょう。
「最大ユーザー100人になるまで毎秒5人づつユーザーが増加する」設定です。
HostはdummyAPIの関数URLを入れてください。
「Start swaming」を押下して負荷試験を開始しましょう。
負荷試験の結果を見てみよう
Locustで確認できる負荷試験の結果画面を下記3つに絞って見てみましょう。
- Statistics
- Charts
- Download Data
「Statistics」タブを見ると負荷試験を表で確認できます。
さすがLambdaくん。この程度の負荷では一回も失敗しませんね。
「Charts」タブを見ると負荷試験を3つのグラフで確認できます。
それぞれ軽く見てみましょう。
1つ目「Total Requests per Second」
当たり前ですが総リクエスト数/秒(RPS)が徐々に上がっていくことがグラフから読み取れます。失敗したエラーはないですね。
2つ目「Response Times (ms)」
負荷が上がるにつれて徐々にレスポンスが遅くなるのがグラフから読み取れます。
3つ目「Number of Users」
「最大ユーザー100人になるまで毎秒5人づつユーザーが増加する」設定がうまく機能していることがグラフから読み取れます。
「Download Data」タブを見ると負荷試験の結果をcsvなどのファイル形式でダウンロードできます。
負荷試験のエビデンスにできますし、開発メンバーとパフォーマンス改善を話し合うときの資料としても利用できそうです。
テストシナリオを構築してみよう
locustfile.pyにテストシナリオを構築してみましょう。
公開されているWeb APIで「認証のAPI」を叩いて取得したtokenを使って「別のAPI」を叩く ようなAPIの作りを見ますよね。
より簡単なテストシナリオに落とし込んで
最初の一回は「dummyAPI」を叩いて、それ以降は「dummyAPI2」を叩く
というテストシナリオを想定してlocustfile.pyを改修しましょう。
負荷試験対象は「dummyAPI2」です。
locustfile.pyは下記のように改修します。
locustfile.py
from locust import HttpUser, task class HelloWorldUser(HttpUser): # 最初の1回呼び出される。 def on_start(self): # dummyAPIの関数URL response = self.client.get("https://ih2xtobxskw4lvdhd7sdgscfsi0gtqoi.lambda-url.ap-northeast-1.on.aws/") print("Response text:", response.text) # 2回目以降、hello_worldが呼び出される。 @task def hello_world(self): # dummyAPI2の関数URL (試験開始時に入力するHostの値を使用) response = self.client.get("") print("Response text:", response.text)
コードの解説をしますと
on_start
はユーザーが生まれて1回だけ実行する関数です。
@task
のついた関数はon_start以降callされる関数になります。
今回のコードだとユーザーはインターバルを挟まず力の限りリクエストを何度も何度も投げ続けます。
最大ユーザー数1で実行してみましょう。
Hostには「dummyAPI2」の関数URLを入力してください。
実行ログをターミナルで確認するとdummyAPIが1度呼ばれ、それ以降dummyAPI2が呼ばれていることがわかります。
$ locust [2023-01-24 18:28:37,938] .local/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces) [2023-01-24 18:28:37,953] .local/INFO/locust.main: Starting Locust 2.14.2 [2023-01-24 18:28:50,330] .local/INFO/locust.runners: Ramping to 1 users at a rate of 1.00 per second [2023-01-24 18:28:50,332] .local/INFO/locust.runners: All users spawned: {"HelloWorldUser": 1} (1 total users) Response text: "dummyAPI" Response text: "dummyAPI2" Response text: "dummyAPI2" Response text: "dummyAPI2" Response text: "dummyAPI2" Response text: "dummyAPI2" Response text: "dummyAPI2" Response text: "dummyAPI2" Response text: "dummyAPI2" ...
簡単ですがテストシナリオの組み方に少し触れました。
参考程度にホストが同じで最初の一回は「認証のAPI」を叩いてそれ以降は「別のAPI」を叩く例も載せておきます。
locustfile.py
from locust import HttpUser, task class HelloWorldUser(HttpUser): # 最初の1回呼び出される。 def on_start(self): # 認証のパス /auth response = self.client.get("/auth") print("Response text:", response.text) # 2回目以降、hello_worldが呼び出される。 @task def hello_world(self): # 別のパス /another response = self.client.get("/another") print("Response text:", response.text)
その他にも多くの便利な定義されているアノテーションや、関数がありますので公式で確認ください。
Writing a locustfile — Locust 2.14.2 documentation
AWSでLocustを起動してみよう
Terraformを使ってAWS上にLocust環境を構築し、大規模な負荷試験環境を構築します。
Terraformとはクラウドサーバーなどシステム開発に必要なインフラをコードで記述することにより自動で構築するツールです。
IaC (Infrastructure as Code) ツールの一種ですね。
本記事では深く触れません。
下記が公式になります。
Terraform by HashiCorp
Locustの公式でAWS上での環境構築方法が示されているので手順に従います。
Running Locust distributed with Terraform/AWS — Locust 2.14.2 documentation
Terraformをインストールする
# hashicorp/tapをinstall $ brew tap hashicorp/tap # 次に、Terraformをインストール $ brew install hashicorp/tap/terraform # Terraformの最新バージョンに更新するには、まずHomebrewを更新します。 $ brew update # 次に、upgradeコマンドを実行して、最新のTerraformバージョンをダウンロードして使用します。 $ brew upgrade hashicorp/tap/terraform # インストールを確認する $ terraform version Terraform v1.3.5 on darwin_amd64 Your version of Terraform is out of date! The latest version is 1.3.7. You can update by downloading from https://www.terraform.io/downloads.html
上記のようにバージョン確認できればokです。
Terraformのファイルを作成する
下記のコードをforkするか、1つ1つコピペして構成をパクります。
https://github.com/locustio/locust/tree/master/examples/terraform/aws
./aws ├── data_subnet.tf ├── main.tf ├── output.tf ├── provisioner.tf ├── variables.tf └── plan └── basic.py
いくつか書き換えるファイルがあるのでみていきましょう。
basic.pyを下記に書き換えましょう。
basic.py
from locust import HttpUser, task class HelloWorldUser(HttpUser): # 最初の1回呼び出される。 def on_start(self): # dummyAPIの関数URL response = self.client.get("https://ih2xtobxskw4lvdhd7sdgscfsi0gtqoi.lambda-url.ap-northeast-1.on.aws/") print("Response text:", response.text) # 2回目以降、hello_worldが呼び出される。 @task def hello_world(self): # dummyAPI2の関数URL (試験開始時に入力するHostの値を使用) response = self.client.get("") print("Response text:", response.text)
provisioner.tfのリージョンを適切なものに書き換えましょう。
provisioner.tf
provider "aws" { region = "ap-northeast-1" }
variables.tfで変更すべき点は2点です。
- サブネット名を指定
- ノードの数の指定
subnet-prd-a
はお使いのAWSのsubnetに書き換えてください。
node_size
は一旦2でやってみます。
variables.tf
variable "node_size" { description = "Size of total nodes" default = 2 } variable "loadtest_dir_source" { default = "plan/" } variable "locust_plan_filename" { default = "basic.py" } variable "subnet_name" { default = "subnet-prd-a" description = "Subnet name" }
ファイルの書き換えは完了です。
下記を叩いて実際にAWSに負荷試験の環境を構築しましょう。
Apply complete!
と表示されたらdashboard_urlにあるurlにアクセスしてみてください。
# お使いのAWS環境のアクセスキーとシークレットキーを入力してください。 $ export AWS_ACCESS_KEY_ID=AIAXXXXXXXXXXXXXXXXX $ export AWS_SECRET_ACCESS_KEY=T9HyXXXXXXXXXXXXXXXXXXXXXXXXXXXX $ terraform init $ terraform apply --auto-approve … Apply complete! Resources: 11 added, 0 changed, 4 destroyed. Outputs: dashboard_url = "http://xx.xx.xxx.xx" leader_private_ip = "10.0.149.171" leader_public_ip = "xx.xx.xxx.xx" nodes_private_ip = [ "10.0.151.149", "10.0.150.115", ] nodes_public_ip = [ "yy.yy.yyyy.yy", "zz.zzz.zz.zzz", ]
LocustのUIが表示されればAWS上に負荷試験の環境が構築できています。
AWSから負荷をかけてみよう
ローカルから負荷をかけた時と同じ
「最大ユーザー100人になるまで毎秒5人づつユーザーが増加する」
の設定で実行してみましょう。
HostはdummyAPI2のURLにしてください。
問題なく動作してますね。
今回もエラーはなかったですね。
Chartsのほうも確認してみてください。
最後にAWSの負荷試験環境をクリーンアップします。
$ terraform destroy --auto-approve
秒間1万リクエスト以上の負荷をかけてみよう
variables.tfのnode_size
は20でやってみます。
variables.tf
variable "node_size" { description = "Size of total nodes" default = 20 }
variables.tfの変更を終えたら、もう一度AWS上に負荷試験の環境を作成しましょう。
$ terraform apply --auto-approve
負荷試験の環境を作り終えたら、dashboard_urlが新たに吐き出されているのでアクセスしましょう
dashboard_url = "http://xx.xx.xx.xxx" leader_private_ip = "10.0.149.231" leader_public_ip = "xx.xx.xx.xxx" nodes_private_ip = [ "10.0.150.184", "10.0.149.8", …
LocustのUIにアクセスすると右上のWORKERSが20になっていて、負荷をかけるパワーが上がってます。
「最大ユーザー1万になるまで毎秒10人づつユーザーが増加する」
設定で実行してみましょう。
HostはdummyAPI2のURLを入力してください。
「Start swaming」を押下して実行しましょう。
秒間1万リクエスト以上になったので右上のSTOPボタンを押下して負荷試験を止めて、結果を見ていきましょう。 43%ほどエラーしてますね。
「Failures」タブを見るとエラーの理由がわかります。
見てみると「429 Client Error: Too Many Requests」と出てますね。
Lambdaの方のメトリクスを見てみると、スロットリングが確認できました。
「Charts」も見てみましょう。
RPSは20000くらいありますね。
リクエストは半分くらい失敗してることがわかります。
レスポンスは良かったみたいですね。 いっぱいエラーしてますけどね。
「Workers」タブもみてみましょう。各Workerの下記情報がわかります。
- 状態
- ユーザー数
- CPU使用率
- メモリ使用率
Workerの状態はすべてrunningになっていますが、CPU使用率はほぼ100%になっています。
今回は負荷をかける対象がLambdaであり、ほぼ無限のキャパシティを持っているため、攻撃サーバ側がボトルネックになっていると考えられます。
とはいえ、今回はあくまで秒間1万リクエスト以上の負荷をかけることが目的なので、
これについては問題ないことにします。
「Lambdaに秒間1万リクエスト以上の負荷をかけた場合、スロットリングが起きる」 という結果になりました。
本来なら負荷試験の結果からアーキテクチャ見直しをしたり、コード改修したりするのですが、 今回は負荷試験の環境構築が目標なのでここまでで終わりです。
まとめ
今回はPython製の負荷試験ツール「Locust」を使って
「ローカルでの負荷試験の実行方法」
「AWS上での負荷試験の実行方法」
「AWS上で秒間1万リクエスト以上の負荷試験をする」
を見てきました。
私は改めてLocustのWeb UIがわかりやすくて良いなと思いました。
次週は別の負荷試験ツールを紹介&比較があるので、そちらをお楽しみに!