この記事について
負荷試験にてLocustを利用する必要がありました。そこで柔軟にスケールできるようにEKS上で動かすことになりました。
EKS上でLocustを動かして、簡単な負荷試験を実施する手順などについてこの記事にまとめます。
なお、この記事ではLocustのシナリオについては深く触れません。
あくまで動作確認レベルのシナリオになります。
簡単に登場人物の紹介
Locsut
Python製の負荷試験ツールです。
負荷試験のシナリオをPythonを用いて柔軟に作り込むことができます。
locustにはmaster/workerモード機能があり、これを利用することで複数インスタンス(コンテナ)を利用してスケールさせながらlocustによる負荷試験を行うことが可能です。
今回はこのmaster/workerモードを利用して、locustを動かします。
EKS
KubernetesのAWSマネージドサービスです。
面倒なノードインスタンスをAWSが管理してくれるため、簡単にk8sを利用することができます。
Kubernetes
最近流行りのコンテナオーケストレーション ソフトウェア/プラットフォームです。
コンテナ管理を自動的に行います。
環境のセットアップ
- Cloud9 : 踏み台サーバおよびIDEとして利用
- EKSクラスター:Kubernetesの基盤
- ECR : Kubernetes Deploymentのコンテナイメージを格納
- locust master/locust worker : LocustのMasterとWorkerとして動作するKubernetes Deployment
- Service : Locustへのアクセス用ポートを公開するためのKubernetes Service
VPC周りとCloud9の構築については割愛します。
ファイル構成
cloud9上のファイル構成は以下の通りです。
/home/ec2-user/environment/locust/:Locust関連資材用トップディレクトリ
├── Dockerfile:DeploymentのDocker Image用のDocker file
├── locust-cluster.yaml:EKSクラスター定義ファイル
├── locust-manifest.yaml:Locust実行環境を定義したマニフェストファイル
└── locust-tasks:コンテナにコピーされるディレクトリ
├── locustfile.py:Locustのシナリオファイル
└── run.sh:コンテナが実行するスクリプト
各ファイルの詳細については後述します。
準備
Cloud9にログインして、Cloud9に以下のアプリケーション/ツールをインストールします。
EKSクラスターの作成
cloud9上でEKSクラスターの構成を定義したyamlファイルを作成します。
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: locust-cluster
region: ap-northeast-1
vpc:
subnets:
private:
ap-northeast-1a: { id: subnet-0xxxxxxxxxxxxxxxx }
ap-northeast-1c: { id: subnet-0xxxxxxxxxxxxxxxx }
public:
ap-northeast-1a: { id: subnet-0xxxxxxxxxxxxxxxx }
ap-northeast-1c: { id: subnet-0xxxxxxxxxxxxxxxx }
nodeGroups:
- name: locust-nodegroup
instanceType: t2.small
desiredCapacity: 1
minSize: 1
maxSize: 30
volumeSize: 100
※ 定義しているPrivate/PublicサブネットはVPCにすでに存在するものです。
(もしサブネットを指定しないと、EKSクラスターの作成と一緒にVPC/サブネットも作成されてしまいます)
以下コマンドを実行して、EKSクラスターの作成を行います。
eksctl create cluster -f locust-cluster.yaml
EKSクラスターの作成には15-20分ほど時間がかかります。
コンテナイメージの作成
準備
locust-tasksというディレクトリを作成します。
そして、そのディレクトリ配下に以下ファイルを作成します。
#!/bin/bash
LOCUST_COMMAND="/usr/local/bin/locust"
LOCUST_OPTS="-f /locust-tasks/locustfile.py"
if [[ "$LOCUST_MODE" = "master" ]]; then
LOCUST_OPTS="$LOCUST_OPTS --master"
elif [[ "$LOCUST_MODE" = "worker" ]]; then
LOCUST_OPTS="$LOCUST_OPTS --worker --master-host=$LOCUST_MASTER"
fi
echo "$LOCUST_COMMAND $LOCUST_OPTS"
$LOCUST_COMMAND $LOCUST_OPTS
run.shはコンテナがlocustを効率よく実行するためのシェルスクリプトです。
from locust import TaskSet, HttpUser, task, constant
import csv
import itertools
class MyTasks(TaskSet):
@task(1)
def scenarioa(self):
data = {}
data["a"] = 1
data["b"] = 2
data["op"] = "add"
self.client.post(url="/test",
headers={"Content-Type": "application/json"},
json=data)
class MyUser(HttpUser):
wait_time = constant(1)
tasks = [MyTasks]
locustfile.pyはシナリオファイルです。
詳細については後述します。
イメージの作成
Dockerファイルを作成します。
FROM python:3.6
# Install locustio
RUN python -m pip install --upgrade pip && \
python -m pip install locust
ADD locust-tasks /locust-tasks
EXPOSE 5557 5558 8089
RUN chmod 755 /locust-tasks/run.sh
ENTRYPOINT ["/locust-tasks/run.sh"]
その後イメージをビルドします。
docker build -t locust:1.0 .
ECRにイメージをPUSH
こちらを参考にコンテナイメージをECRにPUSHします。
ECRにログインします。
aws ecr get-login-password | docker login --username AWS --password-stdin https://${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
※ACCOUNT_IDおよびAWS_REGIONには、実際にはアカウトIDとリージョン文字列を入力しています。
イメージにタグを付与します。
docker tag locust:1.0 ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
ECRにPUSH
docker push ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
Deployment/Serviceの作成
以下の通り、マニフェストファイルを作成します。
---
apiVersion: v1
kind: Namespace
metadata:
name: locust
---
apiVersion: v1
kind: Service
metadata:
labels:
app: locust-master
name: locust-master
namespace: locust
spec:
ports:
- name: master-web
nodePort: 30060
port: 8089
protocol: TCP
targetPort: master-web
- name: master-p1
nodePort: 30061
port: 5557
protocol: TCP
targetPort: master-p1
- name: master-p2
nodePort: 30062
port: 5558
protocol: TCP
targetPort: master-p2
selector:
app: locust-master
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust-master
namespace: locust
labels:
app: locust-master
spec:
selector:
matchLabels:
app: locust-master
replicas: 1
template:
metadata:
labels:
app: locust-master
spec:
containers:
- name: locust-master
image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
imagePullPolicy: IfNotPresent
env:
- name: LOCUST_MODE
value: master
ports:
- containerPort: 8089
name: master-web
protocol: TCP
- containerPort: 5557
name: master-p1
protocol: TCP
- containerPort: 5558
name: master-p2
protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust-worker
namespace: locust
labels:
app: locust-worker
spec:
selector:
matchLabels:
app: locust-worker
replicas: 3
template:
metadata:
labels:
app: locust-worker
spec:
containers:
- name: locust-worker
image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
imagePullPolicy: IfNotPresent
env:
- name: LOCUST_MODE
value: worker
- name: LOCUST_MASTER
value: locust-master
上記のマニフェストについて簡単に解説します。
1つ目のセクション
NameSpaceを作成しています。
apiVersion: v1
kind: Namespace
metadata:
name: locust
正直デフォルトのNameSpaceでも事足りますが、、どうせならNameSpaceも作成しようということでNameSpaceを定義しています。
2つ目のセクション
LoadBalancer Serviceを作成しています。
apiVersion: v1
kind: Service
metadata:
labels:
app: locust-master
name: locust-master
namespace: locust
spec:
ports:
- name: master-web
nodePort: 30060
port: 8089
protocol: TCP
targetPort: master-web
- name: master-p1
nodePort: 30061
port: 5557
protocol: TCP
targetPort: master-p1
- name: master-p2
nodePort: 30062
port: 5558
protocol: TCP
targetPort: master-p2
selector:
app: locust-master
type: LoadBalancer
master/workerモードでLocustを動かすとき、masterの以下3ポートを利用します。
- 8089 : Locust Web UIへのアクセス用ポート
- 5557 : master-worker間通信用のポート(その1)
- 5558 : master-worker間通信用のポート(その2)
外部からWeb UIにアクセスは、LoadBalancerのVIPのポート8089を経由して行われます。
k8sクラスター内部におけるworkerからmasterへのアクセスはCluster IPのポート5557/5558を経由して行われます。
3つ目のセクション
Locust MasterのDeploymentを作成しています。
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust-master
namespace: locust
labels:
app: locust-master
spec:
selector:
matchLabels:
app: locust-master
replicas: 1
template:
metadata:
labels:
app: locust-master
spec:
containers:
- name: locust-master
image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
imagePullPolicy: IfNotPresent
env:
- name: LOCUST_MODE
value: master
ports:
- containerPort: 8089
name: master-web
protocol: TCP
- containerPort: 5557
name: master-p1
protocol: TCP
- containerPort: 5558
name: master-p2
protocol: TCP
※ACCOUNT_IDおよびAWS_REGIONには、実際にはアカウトIDとリージョン文字列を入力しています。
masterとして起動するように環境変数を指定します。
また前述したとおり、masterにおいて3つのポートを利用するため、それらを定義します。
4つ目のセクション
Locust WorkerのDeploymentを作成しています。
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust-worker
namespace: locust
labels:
app: locust-worker
spec:
selector:
matchLabels:
app: locust-worker
replicas: 3
template:
metadata:
labels:
app: locust-worker
spec:
containers:
- name: locust-worker
image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/locust:1.0
imagePullPolicy: IfNotPresent
env:
- name: LOCUST_MODE
value: worker
- name: LOCUST_MASTER
value: locust-master
※ACCOUNT_IDおよびAWS_REGIONには、実際にはアカウトIDとリージョン文字列を入力しています。
workerとして起動するように環境変数を指定します。
またLOCUST_MASTERにはService(locust-master)を指定することで、Cluster IPのポート5557/5558を経由してmasterと接続することができます。
DeploymentとServiceの起動
以下コマンドで起動します。
kubectl apply -f locust-manifest.yaml
DeploymentとServiceのステータスを確認します。
kubectl get deployment -n locust
NAME READY UP-TO-DATE AVAILABLE AGE
locust-master 1/1 1 1 2m13s
locust-worker 3/3 3 3 2m13s
READYの分母と分子が全て一致していればOKです。
kubectl get service -n locust
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
locust-master LoadBalancer 10.100.217.66 axxxx3fdxxxx14f4c83xxxxab735xxxx-xxxxxxxxxx.ap-northeast-1.elb.amazonaws.com 8089:30060/TCP,5557:30061/TCP,5558:30062/TCP 16s
EXTERNAL-IPが表示されていればOKです。
Locustへ接続
ServiceのEXTERNAL-IPのポート8089にWebブラウザよりアクセスします。
実際に動かす
実際にテスト用のAPI Gatewayを作成して、それに対して負荷試験を実施します。
テスト用API Gatewayの作成
以下の簡単な四則計算を行うLambda関数をAPI GatewayにPostメソッドとして登録します。
console.log('Loading the calculator function');
exports.handler = function(event, context, callback) {
console.log('Received event:', JSON.stringify(event, null, 2));
if (event.a === undefined || event.b === undefined || event.op === undefined) {
callback("400 Invalid Input");
}
var res = {};
res.a = Number(event.a);
res.b = Number(event.b);
res.op = event.op;
if (isNaN(event.a) || isNaN(event.b)) {
callback("400 Invalid Operand");
}
switch(event.op)
{
case "+":
case "add":
res.c = res.a + res.b;
break;
case "-":
case "sub":
res.c = res.a - res.b;
break;
case "*":
case "mul":
res.c = res.a * res.b;
break;
case "/":
case "div":
res.c = res.b===0 ? NaN : Number(event.a) / Number(event.b);
break;
default:
callback("400 Invalid Operator");
break;
}
callback(null, res);
};
curlコマンドで正常に動作することを確認します。
curl -X POST -H "Content-Type: application/json" -d '{"a":1, "b":2, "op":"add"}' https://<作成したAPI Gatewayのエンドポイント>
{"a":1,"b":2,"op":"add","c":3}
負荷試験開始
前述したシナリオファイルは以下の通りです。
from locust import TaskSet, HttpUser, task, constant
import csv
import itertools
class MyTasks(TaskSet):
@task(1)
def scenarioa(self):
data = {}
data["a"] = 1
data["b"] = 2
data["op"] = "add"
self.client.post(url="/test",
headers={"Content-Type": "application/json"},
json=data)
class MyUser(HttpUser):
wait_time = constant(1)
tasks = [MyTasks]
上記のシナリオファイルを読み込ませることで**(LocustのWeb UIのHostに指定したURL)/test**に対してリクエストを送信することができます。
LocustのWeb UIにAPI Gatewayのエンドポイント名を入力して、Starting Swarmingをクリックします。