Liberent-Dev’s blog

株式会社リベル・エンタテインメントのテックブログです。

DFrameを試してみる2

こんにちわ。 システム開発部のK.Mです。

今回は前回触ったDFrameに関してもう少し深掘りして行きます。
深掘りの前に、前回の実装で良くない実装をしていた部分を修正していきます。

HttpClientは使い回す実装にしよう

前回の記事APIにアクセスする処理を入れていきます。」で説明している箇所にて、指定された回数分ExecuteAsync()のみが動くものかと考えており、HttpClientをSetupAsync()内でnewをして、TeardownAsync()でDisposeしていました。

しかし、今回の調査時に判明したのですが、テスト実施の1セット内でSetupAsync()->ExecuteAsync()->TeardownAsync()という流れになっており、次の2セット目に入った場合に、再度SetupAsync()->ExecuteAsync()->TeardownAsync()を同じように実行する形になっていたため、HttpClientを毎回newしてDisposeする形になっていました。

HttpClientは本来再利用することを推奨されているのですが出来ていなかったので、まずはこちらを修正していきます。

修正内容としては、Workloadのコンストラクタでnewして、デストラクタで一応Disposeするようにしたうえで、SetupAsync()のnew関連の処理と、TeardownAsync()のDispose処理を削除しておきます。

    public SampleWorkload()
    {
        //コンストラクタ
        client = new();
        client.Timeout = TimeSpan.FromSeconds(30); // タイムアウト時間も追加
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-memorypack"));
    }

    ~SampleWorkload()
    {
        //デストラクタ(一応)
        client?.Dispose();
    }

良くない実装は修正したので、本題に入っていきます。

controllerとworker複数起動のやり方

深掘り第1弾は、負荷テストを実施するのに重要なworkerを分散するために公式に記載がある分散テストの実動作を確認していきます。

いきなりの失敗事例ですが、公式の記載がある形で下記のように実装してコンソール上で実行すると、即終了してしまう状態でした。

public class Program
{
    public static void Main(string[] args) {
        DframeOpen(args);
    }

    public static async void DframeOpen(string[] args)
    {
        var builder = DFrameApp.CreateBuilder(7312, 7313);

        // コマンドライン引数対応
        if(args.Length == 0){
            // 通常起動
            await builder.Run();
        }else if (args[0] == "controller"){
            // Controller(WebUI)起動
            await builder.RunControllerAsync();
        }else if (args[0] == "worker"){
            // Worker起動
            await builder.RunWorkerAsync();
        }
    }
}

少し調べてみるとこちらにて

コンパイラ > Asyncなメソッドから呼び出せ
・解説サイト > Taskは使うな
・現実 > やってみたら即終了するじゃねぇか!

という同じような状況になっていたので、今回はコンソールアプリのため、Taskを使って待つようにします。

public class Program
{
    public static void Main(string[] args) {
        Task task = DframeOpen(args);
        task.Wait();
    }

    public static async Task DframeOpen(string[] args)
    {
        var builder = DFrameApp.CreateBuilder(7312, 7313);

        // コマンドライン引数対応
        if(args.Length == 0){
            // 通常起動
            Console.WriteLine("RunAsync!!");
            await builder.RunAsync();
        }else if (args[0] == "controller"){
            // Controller(WebUI)起動
            Console.WriteLine("RunControllerAsync!!");
            await builder.RunControllerAsync();
        }else if (args[0] == "worker"){
            // Worker起動
            Console.WriteLine("RunWorkerAsync!!");
            await builder.RunWorkerAsync();
        }
    }
}

上記の実装にて、正常に起動して勝手に終了するようなことはなくなりました。

正常に動くようになったので、実行時に引数を渡してControllerとWorkerを分けて、更にWorkerを複数個起動してみます。
先にControllerを起動します。

[net7.0]$ ./DFrameSample controller
RunControllerAsync!!
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://0.0.0.0:7312
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://0.0.0.0:7313
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/maeda/DFrame/DFrameSample/bin/Debug/net7.0

起動したことを確認してブラウザでhttp://localhost:7312にアクセスします。

上記のようにブラウザ上では、Workerは0表示となっています。

別のコンソールからWorkerを起動してみます。

[net7.0]$ ./DFrameSample worker
RunWorkerAsync!!
info: DFrame.DFrameWorkerEngine[0]
      Loaded SampleWorkload workload.
info: DFrame.DFrameWorkerEngine[0]
      Starting DFrame worker (WorkerId:abe900ec-a7f0-437c-a8ee-d82cfac594e2)
info: DFrame.DFrameWorkerEngine[0]
      Start to connect Worker -> Controller. Address: http://localhost:7313
info: DFrame.DFrameWorkerEngine[0]
      Connect completed.
info: DFrame.DFrameWorkerEngine[0]
      Waiting command start.

上記のようにWorkerを起動すると管理画面にてWorkerの数が増えます。

ちなみにWorker2個起動した場合は、当然ですが2表示になります。

Workerの数字部分をクリックするとWorker一覧画面が表示されるようになっていました。

PC2台用意して確認してみる。

今まではPC1台にてControllerとWorkerを別々に立ち上げていましたが、負荷テストをするとなるとWorkerを複数台立ち上げる必要があるので、PC1台だとリソースが足りなくなるのでPC複数台での動作を確認していきます。
クラウド環境で実施する場合でも、Worker用に複数台PCが必要になってきます。)

1台目のPC(以降、PC_A)にてControllerを起動し、2台目のPC(以降、PC_B)にてWorkerを複数個立ち上げて確認していきます。

立ち上げる前に諸々設定が必要になってきます。
実装内にてWorkerを起動する際にControllerのIPなどを定義する必要があり、サンプルでは下記のようになっています。

else if (args[0] == "worker")
{
    // worker connect to (controller) address.
    // You can also configure from appsettings.json via builder.ConfigureWorker((ctx, options) => { options.ControllerAddress = "" });
    await builder.RunWorkerAsync("http://foobar:5556");
}

IPを指定する箇所は外部ファイル(appsettings.jsonや自前実装)から読み込むとかをしないと、毎回ビルドする必要が出てきますが、今回は一先ずローカルのIPを固定で指定していきます。

まず今回も失敗事例になりますが、RunWorkerAsync("接続先IP:Controllerポート")という形でPC_AとPC_Bで分けて起動してみましたが、workerがcontrollerへ接続する際にgrpcのエラーが出て繋がりませんでした。

しかし、サンプルをよくよく見てみると

var builder = DFrameApp.CreateBuilder(5555, 5556); // portWeb, portListenWorker
中略
await builder.RunWorkerAsync("http://foobar:5556")

となっており、Worker起動時に渡すIPアドレスはControllerのIPアドレスだが、ポートがWorkerのポートになっていたので、 RunWorkerAsync("接続先IP:Workerポート") にすることで、PC_AとPC_BのControllerとWorkerが繋がることが、確認出来ました。

よくサンプルを見れば分かるのですが、WorkerがContorllerに繋げにいくという思い込みで、ポートもControllerのポートにしていたが違っていたというのが結論です。

PC2台体制でRequest、Repeat、Duration、Infiniteを確認

準備が出来たので2台で確認と行きたいところですが、まずは1台でRequest、Repeat、Duration、Infiniteのそれぞれの動きを確認しておきます。

Request
■Worker1台の場合
設定値①

Concurrency = 10
Total Request = 10
Worker Limit = No Limit

結果①:Worker1台で10個のWorkloadが起動して、各Workloadが1Requestを実施する

設定値②

Concurrency = 3
Total Request = 12
Worker Limit = No Limit

結果②:Worker1台で3個のWorkloadが起動して、各Workloadが4Requestを実施する

■worker2台の場合
設定値①

Concurrency = 10
Total Request = 10
Worker Limit = No Limit

結果①:Worker1台毎に5個のWorkloadが起動して、各Workloadが1Requestを実施する

設定値②

Concurrency = 3
Total Request = 12
Worker Limit = No Limit

結果②:Worker1台毎で3個のWorkloadが起動して、各Workloadが2Requestを実施する

DFrame内でWorkerが増える毎に綺麗に分散するような形になっています

Repeat
追加設定値

Increase TotalRequest 増加合計リクエスト数
Increase Worker 増加Worker
Repeat Count 繰り返す数

■Worker1台の場合
設定値①

Concurrency = 10
Total Request = 10
Worker Limit = No Limit
Increase TotalRequest = 4
Increase Worker = 4
Repeat Count = 4

RepeatCountで指定した回数連続で実行されて、各回毎にIncreaseTotalRequestが増えるような動きをする
(ウォームアップ的な使い方が想定出来る)

結果①

  • 1セット目
    • Worker1台で10個のWorkloadが起動して、各Workloadが1Requestを実施する
  • 2セット目
    • Worker1台で10個のWorkloadが起動して、各Workloadが1Request以上(合計のRequestが14回になるように)を実施する
  • 3セット目
    • Worker1台で10個のWorkloadが起動して、各Workloadが1Request以上(合計のRequestが18回になるように)を実施する
  • 4セット目
    • Worker1台で10個のWorkloadが起動して、各Workloadが2Request以上(合計のRequestが22回になるように)を実施する

■Worker2台の場合
設定値①

Concurrency = 10
Total Request = 10
Worker Limit = No Limit
Increase TotalRequest = 4
Increase Worker = 4
Repeat Count = 4

ReoeatCountで指定した回数連続で実行されて、各回毎にIncreaseTotalRequestとIncreaseWorkerが増えるような動きをする。
(WorkerLimitをNoLimitにしていることで最初からWorker2台フルで使用されており、この場合だとIncreaseWorkerが無駄な値になっている)

結果①

  • 1セット目
    • Worker1台毎に5個のWorkloadが起動して、各Workloadが1Requestを実施する
  • 2セット目
    • Worker1台毎に7個のWorkloadが起動して、各Workloadが1Requestを実施する
  • 3セット目
    • Worker1台毎に9個のWorkloadが起動して、各Workloadが1Requestを実施する
  • 4セット目
    • Worker1台毎に10個のWorkloadが起動して、各Workloadが1Request以上(合計のRequestが22回になるように)を実施する

別のケースの設定値

Concurrency = 10
Total Request = 10
Worker Limit = 1
Increase TotalRequest = 4
Increase Worker = 4
Repeat Count = 4

WorkerLimitを1指定すると初回は1Workerで起動して、次のリクエスト時にIncreaseWorker数分増加する(今回はWorker2台でやったので2回目以降は常にWorker2台で動くことになるが、後述していますが、Workerが4台あれば、3回目は3台、4回目は4台になる)

結果

  • 1セット目(Worker1台)
    • Worker1台で10個のWorkloadが起動して、各Workloadが1Requestを実施する
  • 2セット目(Worker2台)
    • Worker1台毎に7個のWorkloadが起動して、各Workloadが1Requestを実施する
  • 3セット目(Worker2台)
    • Worker1台毎に9個のWorkloadが起動して、各Workloadが1Requestを実施する
  • 4セット目(Worker2台)
    • Worker1台毎に10個のWorkloadが起動して、各Workloadが1Request以上(合計のRequestが22回になるように)を実施する

■Worker4台用意してみる。
設定値

Concurrency = 10
Total Request = 10
Worker Limit = 1
Increase TotalRequest = 4
Increase Worker = 1
Repeat Count = 4

結果

  • 1セット目(Worker1台)
    • Worker1台で10個のWorkloadが起動して、各Workloadが1Requestを実施する(合計10Request)
  • 2セットお目(Worker2台)
    • Worker1台毎に7個のWorkloadが起動して、各Workloadが1Requestを実施する(合計14Request)
  • 3セット目(Worker3台)
    • Worker1台毎に6個のWorkloadが起動して、各Workloadが1Requestを実施する(合計18Request)
  • 4セット目(Worker4台)
    • Worker1台毎に6個か5個のWorkloadが起動して、各Workloadが1Request以上を実施する(合計22回Request)

Duration
追加設定値

Execute Time(seconds) 実行時間

実行時間で指定された秒数間リクエストが実施される機能です。
Execute Time(seconds)に10指定すると、10秒間リクエストが永遠と動く形になります。
10秒経つと自動でリクエストがストップされます。

Infinite
こちらはストップするまで、永遠にシナリオ実行する機能です。

■2台のPCで確認する。
1台と実施した時と変わらない形で動くことを確認出来ました。
クラウド環境であれば、実行ファイルを動かせる環境であれば複数台サーバーを立ててControllerとWorkerをコンソールにて実行すれば、負荷テスト実施は容易に出来そうです。

tips

今回触っていて、抑えておきたい点がありましたので下記しておきます。

  • ExecuteAsync()内にsleep処理を入れるとsleepした分も結果の数値上にカウントされているようなので要注意。シナリオで少し待たせる対応を入れている場合は、その時間を差し引くことが必要になってきます。

  • DFrameの動きを見る感じだと下記のようになっており、Concurrencyを満たすためにWorker内でWorkloadが必要数分起動しているようです。

  • 自分のマシン環境が良くないのか、ControllerのWebUIでCANCEL(STOP)を押しても少し待ってから結果が表示されるので、止まったのかなと感じる時がありました。

出力されるものからグラフや図的なものが作れないか

深掘り第2弾として、なるべくDFrameのソース改造せずに既存DFrame機能からグラフとか図で分かりやすい負荷状況の出力物が出せないか、色々確認してみましたが簡単に対応する方法は無さそうでした。

現状、テスト完了後に表示される値は上記のように集計されたものが表示され、90%や95%ileはありますが、あまり参考にすることは無いとは思いますが98%や99%ileを知りたい場合の手段がなかったり、1リクエスト毎の結果が欲しい場合に困る場合がありそうです。

DFrameのWorkloadクラスにExecuteAsync()とは別にComplete()という1セット終了した際に呼ばれるものもあるので、ExecuteAsync()の1回毎に必要な情報を別に出力したり、Complete()の1セット毎に必要な情報を別に出力するという方法が、DFrame側のソースとは関係無く対応出来そうな内容ではありますが、DFrameのソースをしっかり見る必要がありそうなので、何か進展があれば今後記事化していきたいと考えております。

最後に

リベル・エンタテインメントでは、このような最新技術などの取り組みに興味のある方を募集しています。
もしご興味を持たれましたら下記サイトにアクセスしてみてください。
https://liberent.co.jp/recruit/