この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
サーバーレス開発部@大阪の岩田です。 AWS IoTに対して負荷テストを行う機会があったのですが、負荷テストツールのLocustとECSを組み合わせることで、スケール可能な負荷テスト環境が構築できたので手順をご紹介します。
Locustとは
まず、Locustについて簡単にご紹介します。 LocustはPythonで記述された分散型の負荷テストツールです。
- Pythonのコードでテストシナリオが記述できる
- master - slave構成を取ることができ、slaveを増やすことで簡単にスケールアウトすることが可能
- WebのUIが提供されている
といった特徴があります。
WebのUIはこんなイメージです。
Locustを選定した理由
負荷テストのツールを選定する際に、Locustの対抗馬としてJMeterとGatlingが候補に上がっていたのですが、両ツールともMQTT OverTLSの負荷テストを行うのが困難で、Pythonの既存ライブラリを活用してテストシナリオを自由に記述できるLocustを採用しました。
テストシナリオ
下記のようなシナリオを想定していました。
- 数万台のデバイスが同時にAWS IoTに対してPublishする
- デバイスはMQTT OverTLSでAWS IoTに接続する
- Publishするトピックはデバイスごとに固有 例) foo/device0001/bar
- 数万台のブラウザが同時にAWS IoTに接続し、各デバイスがPublishしているトピックをSubscribeする
- ブラウザはMQTT Over WebsocketでAWS IoTに接続する
- ブラウザとデバイスは1:1で紐づく(pub - sub の組み合わせが数万セット出来上がる)
構築する環境
下記のような環境を構築します。
Publish用のクラスタ、Subscribe用のクラスタそれぞれにVPCを作成し、パブリックサブネット内にmasterを、プライベートサブネット内にslaveを配置します。 masterは手っ取り早くFargateで起動、slaveはFargateの上限に引っかからない様にEC2で起動します。
ソースコード
テストに使用したソースコードを見て行きます。 ※実際に使用したものから一部改変しています。
ソースコードはこちらのサイトを参考に実装しました。
記述のお作法については下記のサイトが参考になりました。
まず、ディレクトリ構成は下記の通りです。
.
├── Dockerfile
├── Pipfile
├── Pipfile.lock
├── VeriSign-Class\ 3-Public-Primary-Certification-Authority-G5.pem
├── cfn-template.yml
├── common.py
├── pub.py
├── sub.py
├── thing1-certificate.pem
└── thing1-private.pem
thing1-private.pem
、thing1-certificate.pem
はそれぞれテストに使用するモノのクライアント証明書と秘密鍵です。
事前にAWS IoT上にモノを1つ登録し、クライアント証明書を発行しておきます。
今回は全クライアントが1つのモノを使い回す構成としました。
また、VeriSign-Class 3-Public-Primary-Certification-Authority-G5.pem
がAWS IoTのCA証明書になります。
※今後は、Amazon Trust ServicesのCA証明書を使用していく方が良いでしょう
AWS IoT Core がお客様に提供する Symantec の認証局無効化の対応方法
Pipfile
ライブラリの導入などはpipenvを利用しました。 最終的なPipfileの中身は下記の通りです。
Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
locustio = "*"
paho-mqtt = "*"
awsiotpythonsdk = "*"
"boto3" = "*"
[dev-packages]
[requires]
python_version = "3.6"
共通処理
Publish側とSubscribeの共通ロジックを切り出しています。 プログラムの規模が大きくならないのが分かっているので、特にクラス化などもせずに、なんでもかんでも全て1ファイルに詰め込みました。 また、使い捨てのコードなので、エラーハンドリングはガッツリ省略しています。
common.py
import boto3
import os
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
AWS_DEFAULT_REGION = os.getenv('AWS_DEFAULT_REGION', 'us-east-1')
IOT_ENDPOINT = os.getenv('IOT_ENDPOINT')
TABLE_NAME = os.getenv('SEQ_TABLE')
QoS = 1
CA_FILE_PATH = './VeriSign-Class 3-Public-Primary-Certification-Authority-G5.pem'
DYNAMO = boto3.resource('dynamodb', region_name=AWS_DEFAULT_REGION)
def get_topic(seq:int)->str:
imei = '{0:07d}'.format(seq)
return f'hoge/{imei}/fuga'
def get_slave_no(mqtt_type:str)->int:
table = DYNAMO.Table(TABLE_NAME)
res = table.update_item(
Key={
'type': mqtt_type,
},
UpdateExpression="set seq = seq + :val",
ExpressionAttributeValues={
':val': 1
},
ReturnValues="UPDATED_NEW"
)
return res['Attributes']['seq']
ポイントとしてslaveがN台構成になった時に、シナリオを実行中のLocustコンテナ側から自分が何台目のslaveなのかを把握できるようにget_slave_no
という関数を作成しています。
この関数ではDynamoDBを利用して連番を採番しています。
下記のようなテーブルがあり、slaveが起動する度にSEQをカウントアップしていくようなイメージです。
type | seq |
---|---|
pub | 1 |
sub | 1 |
DynamoDBのキャパシティユニットをケチりたかったので、同一slave内での同時実行数に応じたSEQカウントアップはPythonのコードで行いました。 このコードだと、テストシナリオの同時ユーザー数がslave数で割り切れない場合は複数のユーザーが同一トピックにPub・Subすることになりますが、あまりパワーやコストをかけたくなかったので、割り切って諦めました。
Publish
次にPublish側のコードです。 こちらもエラーハンドリングはガッツリ省略しています。
pub.py
# -*- coding: utf-8 -*-
import gevent
import json
import time
from locust import TaskSet, Locust, task, runners
from locust.events import request_success, request_failure
import ssl
import paho.mqtt.client as mqtt
import time
import random
import threading
from common import *
def get_client():
client = mqtt.Client(protocol=mqtt.MQTTv311)
client.tls_set(CA_FILE_PATH,
certfile='thing1-certificate.pem',
keyfile='thing1-private.pem',
tls_version=ssl.PROTOCOL_TLSv1_2)
client.tls_insecure_set(True)
return client
class MQTTPubTaskSet(TaskSet):
slave_no = 0
seq = 0
client = None
topic = ''
def setup(self):
MQTTPubTaskSet.slave_no = get_slave_no('pub')
lock = threading.Lock()
lock.acquire()
num_clients = runners.locust_runner.num_clients
# 自分のスレーブID -1 台のスレーブがすでに起動しており、スレーブ1台あたりのクライアント数は均等に分散されているとする
MQTTPubTaskSet.seq = num_clients * (MQTTPubTaskSet.slave_no - 1) + 1
lock.release()
def on_start(self):
self.client = get_client()
lock = threading.Lock()
lock.acquire()
self.topic = get_topic(int(MQTTPubTaskSet.seq))
MQTTPubTaskSet.seq += 1
lock.release()
self.client.connect(IOT_ENDPOINT, 8883, keepalive=60)
self.client.loop_start()
@task
def pub(self):
time.sleep(1)
# 生データに加えて負荷テストでの集計用にtimestampを付与
payload = json.dumps({
"timestamp": time.time(),
"topic": self.topic,
"payload": {
"hoge":"hogehoge"
}
})
start_time = time.time()
err, mid = self.client.publish(self.topic, payload, qos=QoS)
if err:
request_failure.fire(
request_type='publish',
name=self.topic,
response_time=(time.time() - start_time) * 1000,
exception=err,
)
return
request_success.fire(
request_type='publish',
name=self.topic,
response_time=(time.time() - start_time) * 1000,
response_length=len(payload),
)
class Devices(Locust):
task_set = MQTTPubTaskSet
min_wait = 1
max_wait = 1
MQTT OverTLSで接続するためにpaho mqttを使用しています。
なお、接続のために、事前にAWS Iot側でモノと証明書、ポリシーの登録を行う必要があります。
Subscribe
次にSubscribe側のコードです。 こちらもエラーハンドリングはガッツリ省略しています。
sub.py
# -*- coding: utf-8 -*-
import json
import time
import uuid
import gevent
from locust import TaskSet, Locust, runners, task
from locust.events import request_success
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
import threading
from common import *
def on_receive(client, userdata, message):
payload = json.loads(message.payload)
elapsed = time.time() - payload['timestamp']
request_success.fire(
request_type='sub',
name=message.topic,
response_time=elapsed * 1000,
response_length=len(message.payload),
)
def get_client():
client = AWSIoTMQTTClient(clientID=uuid.uuid4().hex,
useWebsocket=True)
client.configureIAMCredentials(AWSAccessKeyID=AWS_ACCESS_KEY_ID,
AWSSecretAccessKey=AWS_SECRET_ACCESS_KEY)
client.configureCredentials(CAFilePath=CA_FILE_PATH)
client.configureEndpoint(hostName=IOT_ENDPOINT,
portNumber=443)
client.configureOfflinePublishQueueing(-1)
client.configureDrainingFrequency(2)
client.configureConnectDisconnectTimeout(120)
client.configureMQTTOperationTimeout(60)
return client
class AWSIoTTaskSet(TaskSet):
slave_no = 0
seq = 0
client = None
clients = {}
topic = ''
def setup(self):
AWSIoTTaskSet.slave_no = get_slave_no('sub')
lock = threading.Lock()
lock.acquire()
num_clients = runners.locust_runner.num_clients
# 自分のスレーブID -1 台のスレーブがすでに起動しており、スレーブ1台あたりのクライアント数は均等に分散されているとする
AWSIoTTaskSet.seq = num_clients * (AWSIoTTaskSet.slave_no - 1) + 1
lock.release()
def on_start(self):
self.client = get_client()
self.client.connect()
lock = threading.Lock()
lock.acquire()
user_count = runners.locust_runner.user_count
seq = AWSIoTTaskSet.seq
AWSIoTTaskSet.seq += 1
lock.release()
topic = get_topic(int(seq))
self.client.subscribe(topic, QoS, on_receive)
while True:
time.sleep(300)
@task
def dummy(self):
# on_startの中で無限ループさせるため実際にこの処理は呼ばれないが、
# taskが無いとlocust的にエラーになるのでダミーのタスクを作成しておく
print('----------dummy task called--------------')
class AWSIoTUser(Locust):
task_set = AWSIoTTaskSet
min_wait = 1
max_wait = 1
Subscribe側はテスト終了までずっとSubscribeし続けておいて欲しいので、on_startの中でAWS Iotに接続した後は無限ループさせています。 また、タスクが無いと怒られるようなのでdummyというメソッドを作成しています。
こちらはMQTT Over Websocketで接続するためAWSIoTMQTTClientを利用しています。
Dockerfile
Dockerfileです。
FROM python:3.6
RUN pip install pipenv
RUN mkdir /app
WORKDIR /app
ADD Pipfile /app/
ADD Pipfile.lock /app/
RUN LIBRARY_PATH=/lib:/usr/lib pipenv install --system --ignore-pipfile
ADD . /app/
構築手順
実際に負荷テストの環境を構築していきます。
VPC等のリソース作成
下記のCloudFormationのテンプレートで構築しました。 EC2のインスタンスサイズは決め打ちでm5.largeにしているので、必要に応じて適宜修正します。
cfn-template.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Setup Stress Test Environment
Parameters:
UserGIP:
Description: The IP address range that can be used to Locust WebUI
Type: String
MinLength: '9'
MaxLength: '18'
Default: 0.0.0.0/0
AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.
Resources:
PubVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: 'true'
EnableDnsHostnames: 'true'
InstanceTenancy: default
Tags:
-
Key: Name
Value: locust Pub VPC
PubVPCPublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref PubVPC
Tags:
-
Key: Name
Value: PubVPCPublicRouteTable
PubVPCPrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref PubVPC
Tags:
-
Key: Name
Value: PubVPCPrivateRouteTable
PubVPCPublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PubVPC
CidrBlock: 10.0.0.0/24
AvailabilityZone: us-east-1a
MapPublicIpOnLaunch: true
Tags:
-
Key: Name
Value: PubVPCPublicSubnetA
PubVPCPublicSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PubVPCPublicSubnetA
RouteTableId: !Ref PubVPCPublicRouteTable
PubVPCPrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref PubVPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: us-east-1a
Tags:
-
Key: Name
Value: PubVPCPrivateSubnetA
PubVPCPrivateSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PubVPCPrivateSubnetA
RouteTableId: !Ref PubVPCPrivateRouteTable
PubVPCInternetGateway:
Type: AWS::EC2::InternetGateway
PubVPCAttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref PubVPC
InternetGatewayId: !Ref PubVPCInternetGateway
PubVPCRoute:
Type: AWS::EC2::Route
DependsOn: PubVPCInternetGateway
Properties:
RouteTableId: !Ref PubVPCPublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref PubVPCInternetGateway
PubVPCPrivateSubDefaultRoute:
Type: AWS::EC2::Route
DependsOn: PubVPCNatGateway
Properties:
RouteTableId: !Ref PubVPCPrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref PubVPCNatGateway
PubVPCNatGatewayEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
PubVPCNatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt PubVPCNatGatewayEIP.AllocationId
SubnetId: !Ref PubVPCPublicSubnetA
Tags:
-
Key: Name
Value: PubVPCNatGateway
PubVPCLocustSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow Access To Locust
VpcId: !Ref PubVPC
PubVPCLocustSecurityGroupIngressWebUIInner:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref PubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 8089
ToPort: 8089
SourceSecurityGroupId: !Ref PubVPCLocustSecurityGroup
PubVPCLocustSecurityGroupIngressMasterSlaveInner:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref PubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 5557
ToPort: 5558
SourceSecurityGroupId: !Ref PubVPCLocustSecurityGroup
PubVPCLocustSecurityGroupIngressWebUI:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref PubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 8089
ToPort: 8089
CidrIp: !Sub ${UserGIP}
PubVPCLocustSecurityGroupIngressMasterSlave:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref PubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 5557
ToPort: 5557
CidrIp: !Sub ${UserGIP}
PubVPCLocustSecurityGroupIngressSSH:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref PubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Sub ${UserGIP}
SubVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: 'true'
EnableDnsHostnames: 'true'
InstanceTenancy: default
Tags:
-
Key: Name
Value: locust Sub VPC
SubVPCPublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref SubVPC
Tags:
-
Key: Name
Value: SubVPCPublicRouteTable
SubVPCPrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref SubVPC
Tags:
-
Key: Name
Value: SubVPCPrivateRouteTable
SubVPCPublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref SubVPC
CidrBlock: 10.0.0.0/24
AvailabilityZone: us-east-1a
MapPublicIpOnLaunch: true
Tags:
-
Key: Name
Value: SubVPCPublicSubnetA
SubVPCPublicSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubVPCPublicSubnetA
RouteTableId: !Ref SubVPCPublicRouteTable
SubVPCPrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref SubVPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: us-east-1a
Tags:
-
Key: Name
Value: SubVPCPrivateSubnetA
SubVPCPrivateSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubVPCPrivateSubnetA
RouteTableId: !Ref SubVPCPrivateRouteTable
SubVPCInternetGateway:
Type: AWS::EC2::InternetGateway
SubVPCAttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref SubVPC
InternetGatewayId: !Ref SubVPCInternetGateway
SubVPCRoute:
Type: AWS::EC2::Route
DependsOn: SubVPCInternetGateway
Properties:
RouteTableId: !Ref SubVPCPublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref SubVPCInternetGateway
SubVPCPrivateSubDefaultRoute:
Type: AWS::EC2::Route
DependsOn: SubVPCNatGateway
Properties:
RouteTableId: !Ref SubVPCPrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref SubVPCNatGateway
SubVPCNatGatewayEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
SubVPCNatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt SubVPCNatGatewayEIP.AllocationId
SubnetId: !Ref SubVPCPublicSubnetA
Tags:
-
Key: Name
Value: SubVPCNatGateway
SubVPCLocustSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow Access To Locust
VpcId: !Ref SubVPC
SubVPCLocustSecurityGroupIngressWebUIInner:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 8089
ToPort: 8089
SourceSecurityGroupId: !Ref SubVPCLocustSecurityGroup
SubVPCLocustSecurityGroupIngressMasterSlaveInner:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 5557
ToPort: 5558
SourceSecurityGroupId: !Ref SubVPCLocustSecurityGroup
SubVPCLocustSecurityGroupIngressWebUI:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 8089
ToPort: 8089
CidrIp: !Sub ${UserGIP}
SubVPCLocustSecurityGroupIngressMasterSlave:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SubVPCLocustSecurityGroup
IpProtocol: tcp
FromPort: 5557
ToPort: 5558
CidrIp: !Sub ${UserGIP}
ECSTaskRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ECSTaskRolePolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: AllowDynamoDB
PolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Action: dynamodb:*
Resource: "*"
Roles:
- !Ref ECSTaskRole
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
ECRRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: hogehoge/locust
LocustPubCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: LocustPubCluster
LocustSubCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: LocustSubCluster
EcsInstancePubAsg:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
VPCZoneIdentifier:
- Fn::Join:
- ','
- - !Ref PubVPCPrivateSubnetA
LaunchConfigurationName: !Ref PubInstanceLc
MinSize: 0
MaxSize: 100
DesiredCapacity: 0
EcsInstanceSubAsg:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
VPCZoneIdentifier:
- Fn::Join:
- ','
- - !Ref SubVPCPrivateSubnetA
LaunchConfigurationName: !Ref SubInstanceLc
MinSize: 0
MaxSize: 100
DesiredCapacity: 0
PubInstanceLc:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
ImageId: ami-0254e5972ebcd132c
InstanceType: m5.large
IamInstanceProfile: !Ref EC2InstanceProfile
SecurityGroups:
- !Ref PubVPCLocustSecurityGroup
BlockDeviceMappings:
- DeviceName: /dev/xvdcz
Ebs:
VolumeType: gp2
VolumeSize: 32
UserData:
Fn::Base64:
Fn::Sub: |
#!/bin/bash
echo ECS_CLUSTER=LocustPubCluster >> /etc/ecs/ecs.config
echo ECS_AVAILABLE_LOGGING_DRIVERS='["json-file","awslogs"]' >> /etc/ecs/ecs.config
echo ECS_ENABLE_TASK_IAM_ROLE=true >> /etc/ecs/ecs.config
echo ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true >> /etc/ecs/ecs.config
yum update -y
SubInstanceLc:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
ImageId: ami-0254e5972ebcd132c
InstanceType: m5.large
IamInstanceProfile: !Ref EC2InstanceProfile
SecurityGroups:
- !Ref SubVPCLocustSecurityGroup
BlockDeviceMappings:
- DeviceName: /dev/xvdcz
Ebs:
VolumeType: gp2
VolumeSize: 32
UserData:
Fn::Base64:
Fn::Sub: |
#!/bin/bash
echo ECS_CLUSTER=LocustSubCluster >> /etc/ecs/ecs.config
echo ECS_AVAILABLE_LOGGING_DRIVERS='["json-file","awslogs"]' >> /etc/ecs/ecs.config
echo ECS_ENABLE_TASK_IAM_ROLE=true >> /etc/ecs/ecs.config
echo ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true >> /etc/ecs/ecs.config
yum update -y
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles: [!Ref 'EC2Role']
EC2Role:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ec2.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ecs-service
PolicyDocument:
Statement:
- Effect: Allow
Action: ['ecs:CreateCluster', 'ecs:DeregisterContainerInstance', 'ecs:DiscoverPollEndpoint',
'ecs:Poll', 'ecs:RegisterContainerInstance', 'ecs:StartTelemetrySession',
'ecs:Submit*', 'logs:CreateLogStream', 'logs:PutLogEvents']
Resource: '*'
LocustPubMasterTaskDef:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 512
Family: locust-pub-master
Memory: 1GB
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
TaskRoleArn : !GetAtt ECSTaskRole.Arn
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
-
Command:
- "locust"
- "--master"
- "-f"
- "pub.py"
Image: !Sub
- '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}'
- {RepoName: !Ref ECRRepository}
Name: locust-pub-master
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: !Sub '${AWS::Region}'
awslogs-group: !Ref PubMasterLog
awslogs-stream-prefix: !Ref PubMasterLog
LocustPubSlaveTaskDef:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 512
Family: locust-pub-slave
Memory: 1GB
NetworkMode: bridge
RequiresCompatibilities:
- EC2
TaskRoleArn : !GetAtt ECSTaskRole.Arn
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
-
Command:
- "locust"
- "--slave"
- "-f"
- "pub.py"
- "--master-host"
- "pub-master.local"
Environment:
-
Name: IOT_ENDPOINT
Value: xxxxxx.iot.us-east-1.amazonaws.com
-
Name: SEQ_TABLE
Value: seq_table
Image: !Sub
- '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}'
- {RepoName: !Ref ECRRepository}
Name: locust-pub-slave
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: !Sub '${AWS::Region}'
awslogs-group: !Ref PubSlaveLog
awslogs-stream-prefix: !Ref PubSlaveLog
LocustSubMasterTaskDef:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 512
Family: locust-sub-master
Memory: 1GB
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
TaskRoleArn : !GetAtt ECSTaskRole.Arn
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
-
Command:
- "locust"
- "--master"
- "-f"
- "sub.py"
Image: !Sub
- '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}'
- {RepoName: !Ref ECRRepository}
Name: locust-sub-master
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: !Sub '${AWS::Region}'
awslogs-group: !Ref SubMasterLog
awslogs-stream-prefix: !Ref SubMasterLog
LocustSubSlaveTaskDef:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 512
Family: locust-sub-slave
Memory: 1GB
NetworkMode: bridge
RequiresCompatibilities:
- EC2
TaskRoleArn : !GetAtt ECSTaskRole.Arn
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
-
Command:
- "locust"
- "--slave"
- "-f"
- "sub.py"
- "--master-host"
- "sub-master.local"
Environment:
-
Name: IOT_ENDPOINT
Value: xxxxxx.iot.us-east-1.amazonaws.com
-
Name: SEQ_TABLE
Value: seq_table
-
Name: AWS_ACCESS_KEY_ID
Value: xxxxxxxxxx
-
Name: AWS_SECRET_ACCESS_KEY
Value: xxxxxxxxxx
Image: !Sub
- '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}'
- {RepoName: !Ref ECRRepository}
Name: locust-sub-slave
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: !Sub '${AWS::Region}'
awslogs-group: !Ref SubSlaveLog
awslogs-stream-prefix: !Ref SubSlaveLog
LocustPubMasterService:
Type: AWS::ECS::Service
Properties:
Cluster: !GetAtt LocustPubCluster.Arn
DesiredCount: 0
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !Ref PubVPCLocustSecurityGroup
Subnets:
- !Ref PubVPCPublicSubnetA
ServiceName: locust-pub-master
TaskDefinition: !Ref LocustPubMasterTaskDef
LocustPubSlaveService:
Type: AWS::ECS::Service
Properties:
Cluster: !GetAtt LocustPubCluster.Arn
DesiredCount: 0
LaunchType: EC2
ServiceName: locust-pub-slave
TaskDefinition: !Ref LocustPubSlaveTaskDef
LocustSubMasterService:
Type: AWS::ECS::Service
Properties:
Cluster: !GetAtt LocustSubCluster.Arn
DesiredCount: 0
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !Ref SubVPCLocustSecurityGroup
Subnets:
- !Ref SubVPCPublicSubnetA
ServiceName: locust-sub-master
TaskDefinition: !Ref LocustSubMasterTaskDef
LocustSubSlaveService:
Type: AWS::ECS::Service
Properties:
Cluster: !GetAtt LocustSubCluster.Arn
DesiredCount: 0
LaunchType: EC2
ServiceName: locust-sub-slave
TaskDefinition: !Ref LocustSubSlaveTaskDef
SeqTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: seq_table
AttributeDefinitions:
-
AttributeName: type
AttributeType: S
KeySchema:
-
AttributeName: type
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
PubMasterLog:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/pub-master
PubSlaveLog:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/pub-slave
SubMasterLog:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/sub-master
SubSlaveLog:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/sub-slave
Outputs:
LocustPubCluster:
Description: LocustPubCluster
Value: !GetAtt LocustPubCluster.Arn
LocustSubCluster:
Description: LocustSubCluster
Value: !GetAtt LocustSubCluster.Arn
PubVPCSecurityGroup:
Description: PubVPCSecurityGroup
Value: !Ref PubVPCLocustSecurityGroup
PubVPCPublicSubnet:
Description: PubVPCPublicSubnet
Value: !Ref PubVPCPublicSubnetA
PubVPCPrivateSubnet:
Description: PubVPCPrivateSubnet
Value: !Ref PubVPCPrivateSubnetA
SubVPCSecurityGroup:
Description: SubVPCSecurityGroup
Value: !Ref SubVPCLocustSecurityGroup
SubVPCPublicSubnet:
Description: SubVPCPublicSubnet
Value: !Ref SubVPCPublicSubnetA
SubVPCPrivateSubnet:
Description: SubVPCPrivateSubnet
Value: !Ref SubVPCPrivateSubnetA
LocustPubMasterTaskDef:
Description: LocustPubMasterTaskDef
Value: !Ref LocustPubMasterTaskDef
LocustPubSlaveTaskDef:
Description: LocustPubSlaveTaskDef
Value: !Ref LocustPubSlaveTaskDef
LocustSubMasterTaskDef:
Description: LocustSubMasterTaskDef
Value: !Ref LocustSubMasterTaskDef
LocustSubSlaveTaskDef:
Description: LocustSubSlaveTaskDef
Value: !Ref LocustSubSlaveTaskDef
コンテナイメージのビルド&プッシュ
リソースが作成できたらDockerイメージをビルドしてECRにプッシュします
docker build -t hogehoge/locust .
$(aws ecr get-login --no-include-email --region us-east-1)
docker tag hogehoge/locust:latest xxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/hogehoge/locust:latest
docker push xxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/hogehoge/locust:latest
DynamoDBへのアイテムPUT
連番採番用のテーブルに必要なアイテムをPUTしておきます
aws dynamodb put-item --table-name seq_table --item '{"type": {"S": "pub"}, "seq": {"N": "0"}}' --region us-east-1
aws dynamodb put-item --table-name seq_table --item '{"type": {"S": "sub"}, "seq": {"N": "0"}}' --region us-east-1
負荷テスト実施
実際にテストを実施していく手順です。
Publish用masterを起動
Publish用のmasterを起動します。
AWSマネジメントコンソールからサービスにECSを選択 クラスター:LocustPubCluster
のサービスlocust-pub-master
を選択し、タスク数を0から1に変更します。
タスクが起動するとIPアドレス(パブリック,プライベート)が付与されるので控えておきます。
Subscribe用masterを起動
Publish用のmasterと同様の手順で起動します。
Publish用slaveを起動
次にslaveを起動して行きます。
タスク定義の修正
タスク定義を修正し、環境変数等を適切に設定します。
AWSマネジメントコンソールのメニュー「タスク定義」からlocust-pub-slave
を開き、最新のリビジョンを選択した状態から「新しいリビジョンの作成」をクリック
次にタスク定義の詳細が開くので、コンテナlocust-pub-slave
を選択し、コマンドの末尾でmasterサーバーの指定がpub-master.localとなっている箇所を、先ほど起動したmasterコンテナのプライベートIPに変更します。ここはうまくサービスディスカバリを使って自動化したかったのですが、CloudFormationで設定できないようだったので、今回は手作業でやることにしました。
合わせて環境変数IOT_ENDPOINT
を負荷テスト対象のエンドポイントに設定します。
EC2の起動
マネジメントコンソール等から、Auto Scaling グループlocust-EcsInstancePubAsg-xxxxxの
設定値を変更しEC2を起動させます。
slave用サービスのタスク数調整
master用サービスと同様にslave用のタスク数を変更します。 この際タスク定義のリビジョンを先ほど修正した最新版のタスク定義に変更して下さい。
Subscribe用slaveを起動
Publish用のslaveと同様の手順で起動して行きます。
追加で環境変数AWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
にそれぞれアクセスキーIDとシークレットアクセスキーを設定して下さい。
シナリオ実行
Publish・Subscribe両方の準備ができたので、実際にテストを実行して行きます。
http://<Publish用masterのGIP>:8089 にアクセスし、シミュレーションしたいユーザー数とHatch Rate(1秒間に何ユーザー起動するか)を入力します。 なお、画面右上には現在接続されているslaveの台数が表示されます。
同じようにSubscribe側でもテストを実行開始し、しばらく待つと・・・・
テスト状況が表示されました!! 後はシナリオで決めた時間だけテストを流して「Download Data」のタブから各種のデータをDLして分析すればOKです。
まとめ
Locustを使用した負荷テストについて見て来ました。
今回実施したシナリオだと、ネットワーク周りがボトルネックになっている様で(詳細は調査中です)、1コンテナあたり約330ユーザー程度、EC2インスタンス1台(というよりENI1個)あたりを3コンテナを超えたあたりから、AWS IoTへの接続失敗が頻発しだしました。
そのため、ユーザー数は1コンテナあたり300、EC2毎のコンテナ数は3を目安にスケールさせていったのですが、EC2のインスタンス数の上限に引っかかり、2万接続のシナリオが実施できませんでした。 2万接続超のシナリオを実施する場合はEC2の上限緩和申請が必要になるのでご留意下さい。 また、同時接続数をさらに上げていくと今は見えていないボトルネックやAWSのサービス上限に引っかかる可能性もあります。 もしこのブログを参考に負荷テストを実施する場合、構成については十分に検討して頂く様お願いします。 また、上限緩和申請とは別に、AWSへの相談も忘れずにお願いします。
参考
下記のサイトを参考にさせて頂きました