GKE上に秒間1万リクエスト以上を生成できる負荷生成環境を作成する(Locust)

# はじめに
サービスのパフォーマンスチェックをするために、1万リクエスト/秒以上の負荷を瞬間的（スラムテスト）や持続的（ドリップテスト）に生成する必要が出てきました。TerraformでGoogle Kubernetes Engine(GKE)クラスタをワンコマンドで展開、負荷環境(Locust)のクラスターを作成するツールを作成しました。

[https://github.com/studio-design/distributed-load-testing-using-kubernetes-locust](https://github.com/studio-design/distributed-load-testing-using-kubernetes-locust)

#環境
- Google Kubernetes Engine
- Google Cloud SDK 361.0.0
- Terraform 1.0.5
- Locust Cluster (Helm)

# 環境構築
Terraformファイルの構成は以下です。この記事を参考にさせていただきました。
> 技術とかボドゲとかそんな話をしたい
> [正解が未だに見つからないTerraformのディレクトリ構成を考えてみた](https://ohshige.hatenablog.com/entry/2020/12/09/190000)

```
├── projects
│   └── studio-loadtesting
│       ├── 0-build-cluster
│       ├── 1-build-monitoring
│       │   └── values
│       └── 2-deploy-locust
│           └── values
└── usecases
    └── gke_cluster
```

- GKE環境の構築
- Locust Cluster(Helm)の展開
- 負荷スクリプトの更新(ConfigMap)

を`Makefile`を利用して操作できるようにします。

#負荷をかけるターゲットのサーバーを立てる
GCP公式のLocustサーバーテストにある、[サンプル アプリケーションのデプロイ](https://cloud.google.com/architecture/distributed-load-testing-using-gke#deploying_the_sample_application)でターゲットのサーバーを建てました。

## Makefileの作成
以下を参考にして、`Makefile.example`から`Makefile`を作成します。

| 値 | 説明 |
|:-- |:--|
| PROJECT_ID | GCPプロジェクトID
| CLUSTER_NAME | クラスタの基本名。クラスタの削除には時間がかかるため、このツールではベースとなるクラスタ名の最後にランダムなテキストを追加しています。
| REGION | GCPのRegion　|
| ZONE | GCPのゾーン名|
| MACHINE_TYPE | ローディングマシンのマシンタイプ。詳細は[マシンタイプ](https://cloud.google.com/compute/docs/general-purpose-machines)を参照してください | 
| CREDENTIALS|サービスアカウントのJSONファイルへの**フルパス**です。(相対パスは不可)|
| SERVICE_ACCOUNT_EMAIL | サービスアカウントのEメール。例：`[ユーザー名]@[プロジェクト名].iam.gserviceaccount.com` | サービスアカウントのEメール。
| TARGET_HOST|負荷を掛ける対象ホストのURLです。|

:::note alert
TARGET_HOSTはご自身で管理されているサーバーを指定してください。それ以外のサーバーは指定しないようにしてください。
:::

## 環境の展開

まず`deploy`フォルダに移動します。

```
make help
```
と打つと利用できるコマンドのヘルプが表示されます。

### terraformの初期化

```
make init_all
```
で`terraform init`を全サブプロジェクトで実行します。

### GKEクラスターの作成
```
make build
```
を実行してGKEクラスターをセットアップし、作成したGKEクラスターを指す`gcloud`コマンドを初期化します。構築までに5分程度かかります。
詳細は`0-build-cluster`ディレクトリにありますが、以下を行っています。

1. `gke_cluster`モジュールを利用してクラスタを展開。クラスタを作って潰してを繰り返している際に、GCP側で完全に環境を消しきれていない状況で、再構築時にエラーが発生することがあったので、ランダムな文字列をサフィックスとしてつけている。
1. GKEクラスタ生成後、生成クラスタ情報を`kubeconfigfile`に取得。GKE絡みのterraformモジュールを利用する際に必要になる。
1. `locust_connect.sh`という名で`gcloud`コマンドを、作成したGKEに向けるスクリプトを生成。Makefile内部で直で実行してしまうと、その実行中のみ有効なだけになってしまうので、一旦シェルファイルに吐き出して実行させている。


:::note info
deploy/projects/distributed-load-testing-using-kubernetes-locust/0-build-cluster 以下に、GKEの設定情報ファイルとしてkubeconfigが生成され、またgcloudコマンドのGKEクラスタへの初期化用に、gcloud_conf.shというファイルが生成されます。これは以後実行する他のmakeコマンドからも参照されます。
:::

### Locustスクリプトの編集
デフォルトでは、`locust`ディレクトリ以下の

- main.py
- libディレクトリ以下のpyという拡張子を持つ複数ファイル

を`ConfigMap`に展開し、Locustクラスターから参照できるようにします。`main.py`の中に負荷スクリプトを書いて展開してください。またサンプル用の負荷ファイルが`main.py`と同じディレクトリに配置してあるので、中身をコピーして`main.py`に貼り付ければ利用できます。

### Locustクラスターの作成
```
make a_locust
```

を実行して、パフォーマンステスト用の `locust` と必要なコンフィグマップ（ロードテストスクリプトを格納する）をセットアップします。
詳細は`2-deploy-locust`内部にありますが、

1. `kubernetes`, `kubectl`, `helm`モジュールで先ほど生成した`kubeconfig`を利用して、GKEに接続できるように準備
1. `helm_updater`で、必要な`helm`リポジトリを取得できるように設定
1. `kubernetes_config_map`リソースを使って、Locustスクリプトファイルとライブラリを`ConfigMap`に展開
1. `helm_release`リソースを利用して、Locustのhelmファイルを展開。Locustのhelmのパラメータ設定は`locust/values.yaml`から取得する。

:::note warn
helmチャートはhttps://github.com/helm/charts 以下が廃止になったので、https://charts.deliveryhero.io/　のものを利用しています。
:::

:::note info
デフォルトではスクリプトはlocustディレクトリ以下のmain.pyを参照し、ライブラリはlib以下に書かれているpyという拡張子がつくファイルをConfigMapに展開します。
:::

### Locustマスターサーバーへの接続(port forwarding)
```
make locust
```
これでローカルへのポートフォワーディングが行われます。これで、`localhost:8089`で`Locust Master`にアクセスできるようになります。

### 負荷スクリプトを書き換えて、再度Locustクラスタに展開
```
make refresh
```
Scriptの書き換えには`ConfigMap`を利用しています。そのため手順としては以下となります。

1. Scriptを書き換え
1. ConfigMapを更新
1. Podを更新

これら作業を`make refresh`コマンド一回で実施します。`terraform`による更新処理が終わり、Locustクラスタが再展開されたら、`make locust`で`Locust Master`に接続します。

## ローカルで負荷テスト環境
とはいえ、負荷スクリプトが完成するまで、試行錯誤を繰り返すかと思います。そのたびにGKE上で毎回`ConfigMap`を更新して、クラスターを再展開というのは時間がかかります。

意図する負荷パターンのスクリプトが完成するまでは、ローカルで色々試行錯誤できた方が効率がいいかと思います。そのため、`docker-compose`を利用して1ワーカースレッドで起動する小規模クラスタをローカルで動かせる`docker-compose.yaml`も添付しました。

ローカルでスクリプトを動作させるには

```bash
docker-compose up --build --scale worker=1
```
でクラスター起動させ、`localhost:8089`で`Locust Master`にアクセスできます。

# 参照設定
今回、GKE上の`Locust Cluster`で、`spike_load.py`を使用して**10000RPS**を生成した設定は以下になります。

`spike_load.py` は最初に全ユーザー展開し、すべてのユーザーが展開し終わるまでリクエストを投入するのを待ちます。（全ワーカーが同期しているわけでなく、各ワーカー毎でのユーザーになります。）

| パラメータ | 説明 |
|:-- | :-- |
| Locustワーカーのマシンタイプ (`Makefile`内の`MACHINE_TYPE`) | e2-standard-2 |
| ワーカーのReplicas(`values.yaml`の66行目) | 15 |
| ユーザー数(`spike_load.ph`の15行目、`user_amount`変数) | 10000 |

この設定で

- 最初の1秒間のRPSは600前後
- 15～20秒後には10000RPSに達し、それ以上になります。

ほぼ10000RPSで負荷をしばらく保持する場合は、`constant_pacing`関数でアクセスのペースを上げるとよいでしょう。

`spike_load.py`では、以下の行で`dwell load time`を設定しています。このコードは、`user_amount`のユーザー数で120秒維持することを意味しています。維持時間は適宜変更してください。

```spike_load.py
 target_with_times = Step(user_amount, 120)
```

## ワーカーとユーザーのバランスを調整する方法
低コストで負荷を発生させるためには、できるだけワーカーの数を少なくしたいかと思います。ここでは自分が実際行った、ユーザー数とワーカー数を適切に調整するためのサンプルステップをご紹介します。

`spike_load.py`スクリプトを利用して、10000RPSを発生させるのに、試した手順は以下の通りです。

### 最初のトライ
`HPA`を有効にして、10台のワーカー、2000人のユーザーからスタートし、Locustクラスタがどのくらいの負荷を発生できるかを確認しました。この場合、Locustは3000RPSを生成し、そこで飽和しました。Cloud LoggingではCPUエラーが発生していないことから、CPUがまだ限界に達していないと考えられます。

### 2回目のトライ
3倍のユーザー数で10000RPSを生成すると仮定します。ユーザー数を6000に変更し、`make refresh`を実行、`ConfigMap`とLocustクラスタを再構築します。結果、ワーカーが自動的に15にスケールアップし、負荷が10000RPS以上になったことを確認しました。

### 3回目のトライ
ワーカーがスケールする時間を短縮するため、`values.yaml` で初期ワーカー数を `15` に調整し、`make refresh` で Locust ポッドを更新します。

# まとめ
10000 RPS以上の負荷を生成するクラスタの構成には成功しましたが、1〜数秒で10000RPSに到達するような、スパイクを再現するようなスクリプトは上手くつくれませんでした。どなたか見識がある方がいらっしゃったら教えてください。

比較的容易に負荷生成サーバーは作成できたので、GKE上に負荷サーバーの構築を検討されている方の参考になれば幸いです。