ECS で JMeter を動かしてみる

ECS を使った負荷テストについてです。 負荷テストでは十分な負荷を発生させることができれば 実行環境はある程度自由に選択できるので Docker で色々なツールを実行する というのも選択肢の一つになると思います。

Locust など新しめのツールを ECS で使うという記事は他にも書かれているのを見かけたのですが JMeter はあまり見かけないので書いてみます。 JMeter をそんなに使いたいというわけではないのですが、すでに作ったシナリオを利用したいなど使うことはあるかなと思います。

aws.amazon.com

ECS は去年少し触ってみてからあまり使う機会がなくてすでに EKS も使えるようになっていました。

aws.amazon.com

Kubernetes を負荷テストに使うという記事も見かけたことがあるので、Kubernetes 使う方が GCP などでも活用しやすいかもしれません。

cloud.google.com

今回は ECS まだあまりつかったことがなくてもう少し使い方を覚えたいということで ECS でやってみてます。 それから JMeter 以外のツールもこれから使うと思うのでいくらか触れています。

今回使ったテンプレートや Dockerfile などは下のリポジトリにおいています。

github.com

分散負荷テストのための構成

負荷テストツールでは大きな負荷をかけられるようにするため多くは分散テスト (複数のサーバーから負荷をかける) ができるようになっています。 ツールによってこの仕組みが色々違っているので使うものに合わせて実行環境を用意する必要があります。

JMeter

JMeter のウェブページはここです。

jmeter.apache.org

JMeter を動かすときの構成を図にしてみました。

f:id:tkaaad97:20190127193538p:plain

JMeter に外からアクセスする必要はないためプライベートサブネット内にクラスターをおいています。 図に書いてないですが ECR からのイメージダウンロードや負荷テスト時にはインターネットアクセスは必要なので NAT を使っています。 図ではマスターも同じ ECS クラスターで実行していますが、 マスターは手動でコマンドを色々変えて実行したいということもあると思います。 マスターだけパブリックサブネットに踏み台のような EC2 サーバーを用意してそこから実行するというやり方もあると思います。

JMeter ではまずスレーブをサーバーモードで立ち上げて マスターを実行するときにスレーブのIPアドレスとポートの一覧を指定して接続できるようにするという流れになります。 また JMeter の場合はマスターのプロセスを実行したときに負荷テストもスタートして計測結果がマスターに集められログのファイルに保存されます。

JMeter の場合は

  • スレーブの IP とポートの一覧を取得できるようにする
  • マスター実行時に結果をログに出力するか S3 などにアップロードする
  • マスター・スレーブ間で通信できるようにポートなど設定する

ということが必要そうです。

ECS上のタスクのIPとポートは ecs-cli から取得することができるようです。 色々取得方法があると思いますがサービスディスカバリに登録するようにして aws-cli から取得するということもできました。

github.com

上のリポジトリVPC、サブネット、NAT、ECS クラスター、オートスケーリンググループなどの CFn テンプレートの例が公開されていました。 これを真似して一通り構築することができました。

Locust

Locust のページはこちらです。

locust.io

Locust ではマスターを先に立ち上げ、スレーブの起動時にマスターのIPとポートを指定して接続することになります。 Locust のマスターはウェブサーバーとしても機能してウェブページから負荷テストをスタートさせられます。 負荷テスト結果もウェブページから見ることができます。

ECS を使った例を色々と探していて Locust の記事がいくつか見つかったため参考にさせてもらいました。

dev.classmethod.jp

Locust と ECS を使った負荷テストについてこちらの記事で詳しく解説されていて参考になりました。CFn テンプレートも記載されています。

マスターへのインターネットからアクセスが可能なようにマスターのみ Fargate モードでパブリック IP を付けて動作させているようです。

マスターとスレーブはそれぞれタスク定義を分けて別のサービスにしているようです。

現在ではサービスディスカバリも利用できるようになっているのでスレーブからマスターへのアクセスは サービスディスカバリで行うこともできると思います。

inokara.hateblo.jp

こちらの記事も同じく ECS と Locust を使った例です。

こちらの記事では docker-compose.yml でマスター・スレーブ構成を定義して使うようにしていました。

ecs-cli では docker-compose.yml の定義をタスクとして実行するという機能があるようです。

docs.aws.amazon.com

docker-compose を使うことができるとローカルでも同じ構成で確認できて便利だと思います。

ただし一つのタスク定義内に入れられるコンテナ数は10以下に制限されるという点には注意する必要があると思います。

大きな負荷が必要な場合にはタスク定義を分けておいたほうがいいと思います。

Fargate と EC2 どちらを使うか

構成は Fargate の方が簡単になるとは思います。 アカウント毎の Fargate タスク数の制限が前はデフォルト20まででしたが今は50になっていて少し使いやすくなっています。 料金も値下げされたようです。

ただ EC2 で動かす場合もクラスター構築の CFn テンプレートなどを用意しておけばある程度簡単にセットアップできると思います。 また EC2 の場合はスポットインスタンスを使って安く使うことができる場合があります。 状況によってどちらがいいかというのは変わりそうなので EC2 での手順も用意しておいて Fargate で簡単に対応できる要件であれば Fargate でやるなど選択することになるかなと思います。

今回は手順を色々覚えようということで EC2 で試しました。

Dockerfile

JMeter を動かすため Docker イメージには JRE が必要になります。 OpenJDK のイメージが公開されていたのでこれを元にその他必要なものを入れました。 Java のイメージの作り方に詳しくないのでもっとイメージサイズを小さくする方法があるのかもしれません。

hub.docker.com

FROM openjdk:11-jre-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        awscli curl groff-base jq less unzip wget zip \
    && rm -rf /var/lib/apt/lists/*

RUN mkdir /jmeter \
    && cd /jmeter \
    && wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.0.tgz \
    && tar --strip-components=1 -xvf apache-jmeter-5.0.tgz \
    && rm apache-jmeter-5.0.tgz

ENV JMETER_HOME /jmeter

ENV PATH $JMETER_HOME/bin:$PATH

WORKDIR /work

ADD scenarios /work/scenarios
ADD start-slave.sh /usr/local/bin
ADD run-master.sh /usr/local/bin

ENTRYPOINT []
CMD ["start-slave.sh"]

スレーブ起動用のスクリプトは以下のようになっています。 java.rmi.server.hostname にプライベート IP を指定するため EC2 メタデータ API で取得しています。 この方法は Fargate の場合は使えないので Fargate で使う場合修正必要だと思います。

#!/bin/bash

set -x

SERVER_IP=$(curl -q http://169.254.169.254/latest/meta-data/local-ipv4)
SERVER_PORT=1099

jmeter -Dserver_port=$SERVER_PORT -Jserver.rmi.ssl.disable=true \
    -D"java.rmi.server.hostname=$SERVER_IP" \
    -j /dev/stdout -s "$@"

タスク定義

パラメーターなど省略しますがタスク定義の CFn テンプレートは下のような感じにしました。 ネットワークモードは最初デフォルトのブリッジで試していたのですがマスターとスレーブ間の通信が上手くいかずホストモードを使うようにしました。 1099のポートを固定で使用しているため、タスクの配置が制限されてしまうのでできれば動的ポートマッピングにしたかったんですが RMI 関連の知識がないため上手くやる方法を見つけられませんでした。

マスターのタスク起動時にスレーブのIP一覧や負荷をかけるホストのIPをオーバーライドのパラメーターで渡します。 下のような感じで cli から実行しました。

aws ecs run-task --cluster {クラスタ名} \
    --task-definition jmeter-master \
    --overrides '{"containerOverrides":[{"name":"jmeter-master","command":["run-master.sh","-GTARGET_HOST={ターゲットIP}","-GTARGET_PATH={ターゲットパス}","-R{スレーブIP一覧}"]}]}'

スレーブのタスクはサービスにして動かしたのですが スレーブごとにパラメーターを変えて動かしたい場合などもあるかもしれないので run-task で一つづつ起動するやり方でもいいと思います。

MasterTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
        Family: 'jmeter'
        Cpu: !Ref 'ContainerCpu'
        Memory: !Ref 'ContainerMemory'
        NetworkMode: 'host'
        ContainerDefinitions:
            - Name: 'jmeter-master'
                Cpu: !Ref 'ContainerCpu'
                Memory: !Ref 'ContainerMemory'
                Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepositoryName}'
                LogConfiguration:
                    LogDriver: awslogs
                    Options:
                        awslogs-region: !Ref 'AWS::Region'
                        awslogs-group: !Ref 'LogGroup'
                        awslogs-stream-prefix: !Ref 'AWS::StackName'
                EntryPoint:
                    - run-master.sh

SlaveTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
        Family: 'jmeter'
        Cpu: !Ref 'ContainerCpu'
        Memory: !Ref 'ContainerMemory'
        NetworkMode: 'host'
        ContainerDefinitions:
            - Name: 'jmeter-slave'
                Cpu: !Ref 'ContainerCpu'
                Memory: !Ref 'ContainerMemory'
                Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepositoryName}'
                PortMappings:
                    - ContainerPort: 1099
                      HostPort: 1099
                LogConfiguration:
                    LogDriver: awslogs
                    Options:
                        awslogs-region: !Ref 'AWS::Region'
                        awslogs-group: !Ref 'LogGroup'
                        awslogs-stream-prefix: !Ref 'AWS::StackName'
                EntryPoint:
                    - run-slave.sh

まとめ

色々と時間がかかってしまいましたが ECS で JMeter を動かすことができました。 CFn テンプレートなどで手順をまとめておくと色々な要件で使いまわすことができて気軽に試すことができると思います。 JMeter 以外のツールを使う場合も Docker イメージとタスク定義あたりを変えることで活用できると思います。

その他に負荷テストを提供しているサービスも色々あるようです。 CodePipeline とも連携している BlazeMeter というサービスでも JMeter などが使えるようです。 おそらくこうしたサービスだと環境の構築は自分でやらなくていいようになっているのだと思います。

aws.amazon.com