LocustとGKEによる30分Webパフォーマンス測定環境構築レシピ

GCP

この記事について

Locust+GKEの組み合わせでWebアプリの規模に柔軟に追従可能なロードテスト環境をさくっと構築する。なお、ベースとなるドキュメントはGCPのもの。

Distributed load testing using Google Kubernetes Engine  |  Cloud Architecture Center  |  Google Cloud
Explains how to use Google Kubernetes Engine (GKE) to deploy a distributed load testing framework that uses multiple containers to create traffic for a simple R...

環境について

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コンソールに接続する。

Preview web apps  |  Cloud Shell  |  Google Cloud

パブリック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スケジューラについてはまた別の機会に。

タイトルとURLをコピーしました