この記事について
Locust+GKEの組み合わせでWebアプリの規模に柔軟に追従可能なロードテスト環境をさくっと構築する。なお、ベースとなるドキュメントはGCPのもの。
環境について
Locust
特徴は以下の通り。
- スケーラブル
- PODのスケーリングによる大量のクライアントの展開
- シナリオベース
- ログイン、複数回コンテンツにアクセス、ログアウトといったシナリオベースのテスト
- オープンソース
GKE(Googke Kubernetes Engine)
- リードタイムなしでLocustワーカー用ノードプールをスケール可能
- ノードの管理・PODのスケジュールはGKEにお任せ
環境構築
環境変数
環境にあわせたシェル変数をあらかじめエクスポートしておく。
export GKE_CLUSTER=locust-gke-cluster
export AR_REPO=dist-lt-repo
export REGION=asia-northeast1
export ZONE=asia-northeast1-b
export PROJECT=$(gcloud config get-value project)
export LOCUST_IMAGE_NAME=locust-tasks
export LOCUST_IMAGE_TAG=latest
export PROXY_VM=locust-nginx-proxy
GKEの構築
今回はシングルゾーンクラスタとしTerraformでミニマム構成のクラスタを作成した。GKEのテンプレートは以下のとおり。
resource "google_service_account" "gke_sa" {
account_id = "gke-sa"
display_name = "Service Account"
}
resource "google_project_iam_member" "ar_reader" {
project = data.google_client_config.this.project
role = "roles/artifactregistry.reader"
member = "serviceAccount:${google_service_account.gke_sa.email}"
}
resource "google_project_iam_member" "node_serviceaccount" {
project = data.google_client_config.this.project
role = "roles/container.nodeServiceAccount"
member = "serviceAccount:${google_service_account.gke_sa.email}"
}
resource "google_container_cluster" "locust-cluster" {
name = "locust-gke-cluster"
location = "asia-northeast1-b"
remove_default_node_pool = true
initial_node_count = 1
}
resource "google_container_node_pool" "primary_preemptible_nodes" {
name = "locust-node-pool"
location = "asia-northeast1-b"
cluster = google_container_cluster.locust-cluster.name
node_count = 1
node_config {
preemptible = true
machine_type = "e2-small"
disk_type ="pd-standard"
service_account = google_service_account.gke_sa.email
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
}
}
なお、検証用にサイズを小さくするポイントは以下のとおり。
リソース | パラメータ | 説明 |
---|---|---|
google_container_cluster | location | locationを選択する。GKEのコントロールプレーンの冗長度。リージョン、ゾーンクラスタどちらもコントロールプレーンのコストは同じで、リージョンクラスタの方が高可用だが、リージョンクラスタの場合はデフォルトのノードプールが3ゾーンにまたがって作成される。 |
google_container_node_pool | node_config{machine_type} | GKEクラスタが展開するGKEノードのサイズ |
google_container_node_pool | node_config{location} | ノードプールに対するノードの展開先。ゾーン数 x node_countの単位でノードが展開される。 |
GKEコントロールプレーンへの接続
GKEクレデンシャルの取得
kubectlの資格情報をgoogle apiから取得する。
ゾーンクラスタ
gcloud container clusters get-credentials ${GKE_CLUSTER} --zone ${ZONE} --project ${PROJECT}
クラスタの確認
ミニマムクラスタとして作成したノードが見える
$ kubectl get node
NAME STATUS ROLES AGE VERSION
gke-locust-gke-clust-locust-node-pool-24f00ed8-1n6c Ready <none> 27m v1.24.9-gke.3200
GKEにLocustをデプロイ
コンテナイメージのビルドとpush
GKEにLocustのManagerとWorkerイメージをデプロイするために、まずはArtifact Registryにイメージをpushする。
リポジトリのクローン
git clone https://github.com/GoogleCloudPlatform/distributed-load-testing-using-kubernetes
Locustタスクの修正
測定シナリオ・対象サイトの構造にあわせLocustタスクを修正する。今回は単純にTopページの同時リクエスト数を計測する。
cd ./distributed-load-testing-using-kubernetes/docker-image/locust-tasks
vi tasks.py
from locust import HttpUser, task, between
class GetTopPage(HttpUser):
wait_time = between(0.5, 2.5)
@task
def get_toppage(self):
self.client.get('/')
リポジトリ作成
cd distributed-load-testing-using-kubernetes
gcloud artifacts repositories create ${AR_REPO} \
--repository-format=docker \
--location=${REGION} \
--description="Distributed load testing with GKE and Locust"
イメージのビルド
gcloud builds submit \
--tag ${REGION}-docker.pkg.dev/${PROJECT}/${AR_REPO}/${LOCUST_IMAGE_NAME}:${LOCUST_IMAGE_TAG} \
docker-image
ビルドしたイメージ
$ gcloud artifacts docker images list ${REGION}-docker.pkg.dev/${PROJECT}/${AR_REPO} | grep ${LOCUST_IMAGE_NAME}
Listing items under project cloudnizeddotcom, location asia-northeast1, repository dist-lt-repo.
IMAGE: asia-northeast1-docker.pkg.dev/cloudnizeddotcom/dist-lt-repo/locust-tasks
PODのデプロイ
envsubst < kubernetes-config/locust-master-controller.yaml.tpl | kubectl apply -f -
envsubst < kubernetes-config/locust-worker-controller.yaml.tpl | kubectl apply -f -
Deploymentの確認
$ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
locust-master 1/1 1 1 8s
locust-worker 5/5 5 5 8s
Locustのモードについて
Locustでは同じDockerイメージを用いてLOCUST_MODE環境変数によりマスタとワーカーの役割を切り替えている。
locust-master-controller.yaml.tpl
spec:
containers:
- name: locust-master
...
env:
- name: LOCUST_MODE
value: master
locust-worker-controller.yaml.tpl
spec:
containers:
- name: locust-worker
...
env:
- name: LOCUST_MODE
value: worker
PODデプロイ失敗の原因
Failed to pull image
GKEノードプール作成時に指定するサービスアカウントに"roles/artifactregistry.reader”ロールに相当する権限がないとクラスタがイメージをPULLできずPODのデプロイに失敗する。
Warning Failed 12s (x4 over 101s) kubelet Failed to pull image "asia-northeast1-docker.pkg.dev/cloudnizeddotcom/dist-lt-repo/locust-tasks:latest": rpc error: code =Unknown desc = failed to pull and unpack image "asia-northeast1-docker.pkg.dev/cloudnizeddotcom/dist-lt-repo/locust-tasks:latest": failed to resolve reference "asia-northeast1-docker.pkg.dev/cloudnizeddotcom/dist-lt-repo/locust-tasks:latest": failed to authorize: failed to fetch oauth token: unexpected status: 403 Forbidden
Locustタスク内容の変更
Locustのテストシナリオを記すtasks.pyはDockerイメージに内包される。 テストシナリオを変更するにははDockerイメージをリビルドし、POD内のシナリオを更新する必要がある。
kubectl scale deployment --replicas=0 locust-master
kubectl scale deployment --replicas=0 locust-worker
kubectl scale deployment --replicas=1 locust-master
kubectl scale deployment --replicas=5 locust-worker
サービスのデプロイ
workerからmasterへのレポーティング、テスト実施者からLocust WebにアクセスするためのKubernetesサービスをデプロイする。 Google GITリポジトリの構成ではlocust-master-web用のサービスにGCPのパブリックIPがアサインされ、 インターネットからLocust Web管理コンソールにアクセスできるようになっている。
envsubst < kubernetes-config/locust-master-service.yaml.tpl | kubectl apply -f -
$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.95.240.1 <none> 443/TCP 88m
locust-master ClusterIP 10.95.248.28 <none> 5557/TCP,5558/TCP 30s
locust-master-web LoadBalancer 10.95.250.173 ***.***.***.*** 8089:30558/TCP 30s
Locust Webコンソールへの接続
Google Gitリポジトリの構成ではLocust Webコンソールへの接続に"GKEノードのパブリックIP経由の接続"と"Cloud Shellを利用したWeb Preview"の2通りが利用できる。この記事ではWeb PreviewとGCP IAPトンネルの機能でTLSで保護された後者のアクセス経路を利用する。
接続手段 | HTTPS |
---|---|
Web Preview(Cloud Shell) | あり |
GKEノードのパブリックIP | なし(別途Ingressの構成が必要) |
nginxプロキシの構成
限定公開クラスタ上に展開するLocustにCloud ShellのWeb Preview機能を使って接続するには、プロキシとなるnginx VMをGKEノードプールと同じVPCにデプロイする。
export INTERNAL_LB_IP=$(kubectl get svc locust-master-web \
-o jsonpath="{.status.loadBalancer.ingress[0].ip}") && \
echo $INTERNAL_LB_IP
gcloud compute instances create-with-container ${PROXY_VM} \
--zone ${ZONE} --preemptible --tags="locust-proxy" \
--container-image gcr.io/cloud-marketplace/google/nginx1:latest \
--container-mount-host-path=host-path=/tmp/server.conf,mount-path=/etc/nginx/conf.d/default.conf \
--metadata=startup-script="#! /bin/bash
cat <<EOF > /tmp/server.conf
server {
listen 8089;
location / {
proxy_pass http://${INTERNAL_LB_IP}:8089;
}
}
EOF"
次にCloud Shellからlocust VMに対し、SSHポートフォワーディングを行う。
gcloud compute ssh --zone ${ZONE} ${PROXY_VM} --tunnel-through-iap -- -N -L 8089:localhost:8089
その後Cloud ShellのWeb Preview機能を用いてHTTPSで保護された経路を用いてLocsutのWebコンソールに接続する。
パブリックIP経由の接続
Google Gitリポジトリの定義ファイルををそのまま使う場合はlocust-master-webがHTTPSで保護されないことを理解しておく必要がある。 別途Ingress(GKE LB)とGCPマネージド証明書でHTTPS化は可能。
IPアドレスの制限
非限定公開クラスタ(ノードがパブリックIPを持つ)に展開したlocust-master-webのアクセス元IPアドレスを制限して インターネットに非公開とするにはlocust-master-webサービスのspecにloadBalancerSourceRangesフィードを追加すれば良い。
kind: Service
apiVersion: v1
metadata:
name: locust-master-web
annotations:
networking.gke.io/load-balancer-type: "Internal"
...
spec:
...
loadBalancerSourceRanges: ["VPC CIDR","接続元端末IP"]
selector:
app: locust-master
type: LoadBalancer
テスト
tasks.pyで定義されたシナリオとテスト実行時に定義する同時リクエスト数に応じ、ワーカーから対象サイトにリクエストが送信される。各ノードのstatsはマスタノードで集計されWebコンソールから見られる。同時リクエスト数とLocustシナリオを調整することで規模やサイトの内容に応じたクライアントリクエストをシミュレートできる。
Locustワーカーのスケール
大規模環境ではワーカーがボトルネックとなり、LocustがWebサーバーのパフォーマンスを飽和させられないケースが想定される。たとえば以下のような状況が考えられる。
- ワーカーPODがデプロイされるコンテナホストの飽和(CPU/メモリ)
- コンテナホストOSのTCPコネクション数飽和
- Locustコンテナのなんらかの上限に到達した
PODのスケールアウト
スケールアウト前
ワーカーPODのレプリカを増やす
$ kubectl get deployment locust-worker
NAME READY UP-TO-DATE AVAILABLE AGE
locust-worker 5/5 5 5 56m
$ kubectl scale deployment --replicas=10 locust-worker
deployment.apps/locust-worker scaled
$ kubectl get deployment locust-worker
NAME READY UP-TO-DATE AVAILABLE AGE
locust-worker 10/10 10 10 56m
スケールアウト後
なお、ノードプールのリソースに空きがない場合はGKEノードプール内のノードを増やす必要がある。
GKEノードのスケールアウト
ノードプールのノード数を指定する。locationに指定された各ゾーンにnode_countの数だけノードがデプロイされる。(ゾーンが3つの場合は3x2=6ノード)
resource "google_container_node_pool" "primary_preemptible_nodes" {
name = "locust-node-pool"
location = "asia-northeast1-b"
cluster = google_container_cluster.locust-cluster.name
***node_count = 2***
...
}
GKEノードにわたる均等なPODの割り付け
PODの作成先ノードの決定はKubernetesスケジューラーの仕事で、POD作成時にのみ行われる。したがって先にノードを増やし、つぎにワーカーDeploymentのレプリカを増やすことでノード間で均等にLocustワーカーPODが分散される。
動いているPODを積極的にGKEコントローププレーンがTemrminateし別のノード上で再作成するといったような動きはしない。
$ kubectl get pods -o wide -o custom-columns=NAME:.metadata.name,Status:.status.phase,Node:.spec.nodeName --sort-by="{.spec.nodeName}"
NAME Status Node
locust-worker-54c97459db-47dxt Running gke-locust-gke-clust-locust-node-pool-079ef65f-r9m3
locust-worker-54c97459db-ltssk Running gke-locust-gke-clust-locust-node-pool-079ef65f-r9m3
locust-worker-54c97459db-mldg4 Running gke-locust-gke-clust-locust-node-pool-079ef65f-r9m3
locust-worker-54c97459db-vkqdg Running gke-locust-gke-clust-locust-node-pool-079ef65f-r9m3
locust-worker-54c97459db-wr2l6 Running gke-locust-gke-clust-locust-node-pool-079ef65f-r9m3
locust-master-68dfb6d697-lvzfr Running gke-locust-gke-clust-locust-node-pool-079ef65f-xlhn
locust-worker-54c97459db-2w7m2 Running gke-locust-gke-clust-locust-node-pool-079ef65f-xlhn
locust-worker-54c97459db-fnd6j Running gke-locust-gke-clust-locust-node-pool-079ef65f-xlhn
locust-worker-54c97459db-kt5tj Running gke-locust-gke-clust-locust-node-pool-079ef65f-xlhn
locust-worker-54c97459db-l2l4p Running gke-locust-gke-clust-locust-node-pool-079ef65f-xlhn
locust-worker-54c97459db-pwgjt Running gke-locust-gke-clust-locust-node-pool-079ef65f-xlhn
GKEノードを増やすよりも前にワーカーPODを増やすと新しいノードにPODが分散されず、Locustの負荷が思うように増えなかったり、Webアプリケーション側には余裕があるのにレスポンスタイムが実際の値と乖離する原因となる。
そんな時は一度PODをDestroyし再デプロイするとよい。
kubectl scale deployment --replicas=0 locust-worker
kubectl scale deployment --replicas=10 locust-worker
長くなってきたのでPODスケジューラについてはまた別の機会に。