負荷テスト
に最終更新このページでは MagicOnion アプリケーションの負荷テストについて説明します。
MagicOnion は gRPC をベースにした独自のプロトコルを使用しているため、一般的な負荷テストツールである JMeter や k6 などのツールを使用して負荷テストを行えません。
これはデメリットのように見えるかもしれませんが、MagicOnion のクライアントは C# (.NET) であり、負荷テスト用の .NET アプリケーションでカスタムシナリオを作成、実行するのが容易というメリットがあります。
特に MagicOnion アプリケーションはリアルタイム通信を使用したステートフルで複数のステップのシナリオを実行することが多く、このような複雑なシナリオを作成する際にコードの再利用を含め、クライアントの実装知識を最大限活用できます。
負荷テストフレームワークを選択する
負荷テストのワーカーやクライアントは MagicOnion クライアントである必要があるため必然的に .NET アプリケーションであることが求められます。これを満たす負荷テストを行うフレームワークとしては以下のようなものがあります。
またフレームワークを使用せず、コンソールアプリケーションや Unity ヘッドレスクライアントなど独自の .NET アプリケーションを実装することも可能です。
DFrame を使用した負荷テスト
DFrame は Cysharp の提供しているオープンソースの分散型の .NET ベースの負荷テストフレームワークです。DFrame では Workload という単位で負荷テストシナリオを記述し、複数のワーカーを使用して負荷をかけることが可能です。
以下は DFrame を使用した負荷テストの記述例です。詳しくは DFrame のドキュメントを参照してください。
using DFrame;
using MagicOnion;
DFrameApp.Run(7312, 7313); // WebUI:7312, WorkerListen:7313
public class SampleWorkload : Workload
{
public override async Task ExecuteAsync(WorkloadContext context)
{
using var channel = GrpcChannel.ForAddress("https://api.example.com");
var apiClient = MagicOnionClient.Create<IAccountService>(channel);
var accessToken = await apiClient.CreateUserAsync($"User-{Guid.CreateVersion7()}", "p@ssword1");
var hubClient = await StreamingHubClient.ConnectAsync<IGreeterHub, IGreeterHubReceiver>(
channel,
new GreeterHubReceiver(),
StreamingHubClientOptions.CreateWithDefault(callOptions: new CallOptions(){
Headers = new Metadata
{
{ "Authorization", $"Bearer {accessToken}" },
}
})
);
var result = await hubClient.HelloAsync();
}
}
DFrame は DFrame のコントローラーとワーカーの通信に MagicOnion を使用しているため、MagicOnion.Client のバージョンの競合に注意してください。
NBomber を使用した負荷テスト
NBomber は NBomber LLC の提供している有償の .NET ベースの分散負荷テストフレームワークです。
以下は NBomber での負荷テストシナリオの記述例です。詳しくは NBomber のドキュメントを参照してください。
var scenario = Scenario.Create("MagicOnionTest", async ctx =>
{
using var channel = GrpcChannel.ForAddress("https://api.example.com");
var createUser = await Step.Run("create", ctx, async () =>
{
var apiClient = MagicOnionClient.Create<IAccountService>(channel);
var accessToken = await apiClient.CreateUserAsync($"User-{Guid.CreateVersion7()}", "p@ssword1");
return Response.Ok();
});
IGreeterHub? hubClient = null;
var receiver = new GreeterHubReceiver();
var connect = await Step.Run("connect", ctx, async () =>
{
hubClient = await StreamingHubClient.ConnectAsync<IGreeterHub, IGreeterHubReceiver>(
channel,
new GreeterHubReceiver(),
StreamingHubClientOptions.CreateWithDefault(callOptions: new CallOptions(){
Headers = new Metadata
{
{ "Authorization", $"Bearer {accessToken}" },
}
})
);
return Response.Ok();
});
var hello = await Step.Run("hello", ctx, async () =>
{
var result = await hubClient.HelloAsync();
return Response.Ok();
});
return Response.Ok();
});