gRPC クライアント側の負荷分散
Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
作成者: James Newton-King
クライアント側の負荷分散は、gRPC クライアントで、使用可能なサーバー間で負荷を最適に分散できるようにする機能です。 この記事では、.NET でスケーラブルで高パフォーマンスの gRPC アプリを作成するように、クライアント側の負荷分散を構成する方法について説明します。
クライアント側の負荷分散に必要なものは次のとおりです。
- .NET 5 以降。
Grpc.Net.Client
バージョン 2.45.0 以降。
gRPC クライアント側の負荷分散を構成する
クライアント側の負荷分散は、チャネルの作成時に構成されます。 負荷分散を使用する際に考慮する必要がある 2 つのコンポーネントは、次のとおりです。
- リゾルバー。チャネルのアドレスを解決します。 リゾルバーでは、外部ソースからのアドレスの取得がサポートされます。 これはサービス検出とも呼ばれます。
- ロード バランサー。接続を作成し、gRPC 呼び出しで使用されるアドレスを選択します。
リゾルバーとロード バランサーの組み込み実装は、Grpc.Net.Client
に含まれています。 負荷分散は、カスタム リゾルバーとロード バランサーを記述することで拡張することもできます。
アドレス、接続およびその他の負荷分散状態は、GrpcChannel
インスタンスに格納されます。 負荷分散を正しく機能させるには、gRPC 呼び出しを行う際にチャネルを再利用する必要があります。
注意
一部の負荷分散構成では、依存関係の挿入 (DI) が使われます。 DI を使わないアプリでは、ServiceCollection インスタンスを作成できます。
ASP.NET Core Web サイトなど、アプリで既に DI がセットアップされている場合は、既存の DI インスタンスに型を登録する必要があります。 GrpcChannelOptions.ServiceProvider
は、DI から IServiceProvider を取得して構成されます。
リゾルバーを構成する
リゾルバーは、チャネルが作成されるアドレスを使用して構成されます。 アドレスの URI スキームにより、リゾルバーが指定されます。
Scheme | Type | 説明 |
---|---|---|
dns |
DnsResolverFactory |
DNS アドレス レコードのホスト名を照会して、アドレスを解決します。 |
static |
StaticResolverFactory |
アプリで指定されたアドレスを解決します。 アプリで呼び出すアドレスが既に認識されている場合に推奨されます。 |
チャネルでは、リゾルバーに一致する URI を直接呼び出しません。 代わりに、一致するリゾルバーが作成され、アドレスの解決に使用されます。
たとえば、GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure })
を使用する場合は次のようになります。
dns
スキームがDnsResolverFactory
にマップされます。 チャネルに対して DNS リゾルバーの新しいインスタンスが作成されます。- リゾルバーでは
my-example-host
に対して DNS クエリを作成し、127.0.0.100
と127.0.0.101
の 2 つの結果を取得します。 - ロード バランサーでは、
127.0.0.100:80
と127.0.0.101:80
を使用して接続を作成し、gRPC 呼び出しを行います。
DnsResolverFactory
DnsResolverFactory
では、外部ソースからアドレスを取得するように設計されたリゾルバーを作成します。 DNS 解決は一般に、Kubernetes ヘッドレス サービスを持つポッド インスタンスで負荷分散するために使用されます。
var channel = GrpcChannel.ForAddress(
"dns:///my-example-host",
new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
上記のコードでは次の操作が行われます。
- アドレス
dns:///my-example-host
を使用して、作成されたチャネルを構成します。dns
スキームがDnsResolverFactory
にマップされます。my-example-host
は解決するホスト名です。- アドレスにポートが指定されていないため、gRPC 呼び出しはポート 80 に送信されます。 これは、セキュリティで保護されていないチャネルの既定のポートです。 必要に応じて、ホスト名の後に 1 つのポートを指定できます。 たとえば、
dns:///my-example-host:8080
は gRPC 呼び出しをポート 8080 に送信するように構成します。
- ロード バランサーは指定しません。 チャネルの既定値は、第一候補のロード バランサーです。
- gRPC 呼び出し
SayHello
を開始します。- DNS リゾルバーでは、ホスト名
my-example-host
のアドレスを取得します。 - 第一候補のロード バランサーでは、解決済みのアドレスのいずれかへの接続を試行します。
- 呼び出しは、チャネルが正常に接続された最初のアドレスに送信されます。
- DNS リゾルバーでは、ホスト名
DNS アドレスのキャッシュ
負荷分散を行う際にはパフォーマンスが重要になります。 アドレスの解決の待機時間は、アドレスをキャッシュすることで gRPC 呼び出しから除外されます。 最初の gRPC 呼び出しを行う際にリゾルバーが呼び出され、それ以降の呼び出しでキャッシュが使用されます。
接続が中断された場合、アドレスは自動的に更新されます。 実行時にアドレスが変更されるシナリオでは、更新が重要になります。 たとえば、Kubernetes では、再起動されたポッドによって DNS リゾルバーがトリガーされ、ポッドの新しいアドレスが更新されて取得されます。
既定では、接続が中断されると、DNS リゾルバーが更新されます。 DNS リゾルバーは、必要に応じて定期的に自身を更新することもできます。 これは、新しいポッド インスタンスをすばやく検出する場合に役立ちます。
services.AddSingleton<ResolverFactory>(
sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));
上記のコードでは、更新間隔を指定して DnsResolverFactory
を作成し、依存関係の挿入に登録します。 カスタム構成されたリゾルバーの使用方法の詳細については、「カスタム リゾルバーとロード バランサーを構成する」を参照してください。
StaticResolverFactory
静的リゾルバーは StaticResolverFactory
によって提供されます。 このリゾルバーは次のようなものです。
- 外部ソースは呼び出しません。 代わりに、クライアント アプリによってアドレスが構成されます。
- アプリで呼び出すアドレスが既に認識されている状況向けに設計されています。
var factory = new StaticResolverFactory(addr => new[]
{
new BalancerAddress("localhost", 80),
new BalancerAddress("localhost", 81)
});
var services = new ServiceCollection();
services.AddSingleton<ResolverFactory>(factory);
var channel = GrpcChannel.ForAddress(
"static:///my-example-host",
new GrpcChannelOptions
{
Credentials = ChannelCredentials.Insecure,
ServiceProvider = services.BuildServiceProvider()
});
var client = new Greet.GreeterClient(channel);
上記のコードでは次の操作が行われます。
StaticResolverFactory
を作成します。 このファクトリでは、localhost:80
とlocalhost:81
という 2 つのアドレスが認識されています。- ファクトリを依存関係の挿入 (DI) に登録します。
- 以下を使用して、作成されたチャネルを構成します。
- アドレス
static:///my-example-host
。static
スキームが静的リゾルバーにマップされます。 - DI サービス プロバイダーを使用して
GrpcChannelOptions.ServiceProvider
を設定します。
- アドレス
この例では、DI 用に新しい ServiceCollection ファイルを作成します。 ASP.NET Core Web サイトのようなアプリに、DI が既にセットアップされているとします。 その場合は、型を既存の DI インスタンスに登録する必要があります。 GrpcChannelOptions.ServiceProvider
は、DI から IServiceProvider を取得して構成されます。
ロード バランサーを構成する
ロード バランサーは、ServiceConfig.LoadBalancingConfigs
コレクションを使用して service config
で指定されます。 2 つのロード バランサーが組み込まれており、次のようにロード バランサーの構成名にマップされます。
名前 | 種類 | 説明 |
---|---|---|
pick_first |
PickFirstLoadBalancerFactory |
接続が正常に確立されるまで、アドレスへの接続を試行します。 gRPC 呼び出しはすべて最初に成功した接続に対して行われます。 |
round_robin |
RoundRobinLoadBalancerFactory |
すべてのアドレスへの接続を試行します。 gRPC 呼び出しは、ラウンドロビン ロジックを使用して、成功したすべての接続全体に分散されます。 |
service config
は、サービス構成の省略形であり、ServiceConfig
型で表されます。 ロード バランサーが構成された service config
を取得するには、次の 2 つの方法があります。
- アプリでは、
GrpcChannelOptions.ServiceConfig
を使用してチャネルが作成されるときにservice config
を指定できます。 - または、リゾルバーでチャネルの
service config
を解決できます。 この機能を使用すれば、外部ソースで、呼び出し元が負荷分散を行う方法を指定できます。 リゾルバーでservice config
の解決がサポートされるかどうかは、そのリゾルバーの実装によって異なります。GrpcChannelOptions.DisableResolverServiceConfig
でこの機能を無効にします。 service config
が指定されていない場合、またはservice config
にロード バランサーが構成されていない場合、チャネルの既定値はPickFirstLoadBalancerFactory
になります。
var channel = GrpcChannel.ForAddress(
"dns:///my-example-host",
new GrpcChannelOptions
{
Credentials = ChannelCredentials.Insecure,
ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() } }
});
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
上記のコードでは次の操作が行われます。
service config
でRoundRobinLoadBalancerFactory
を指定します。- gRPC 呼び出し
SayHello
を開始します。DnsResolverFactory
では、ホスト名my-example-host
のアドレスを取得するリゾルバーを作成します。- ラウンドロビン ロード バランサーでは、解決済みのすべてのアドレスへの接続を試行します。
- gRPC 呼び出しは、ラウンドロビン ロジックを使用して均等に分散されます。
チャネルの資格情報を構成する
チャネルでは、gRPC 呼び出しがトランスポート セキュリティを使用して送信されるかどうかを認識している必要があります。 http
と https
はアドレスの一部ではなくなりました。このスキームでは現在、リゾルバーが指定されているため、負荷分散を使用する際にはチャネル オプションで Credentials
を構成する必要があります。
ChannelCredentials.SecureSsl
- gRPC 呼び出しは、ChannelCredentials.SecureSsl
で保護されます。https
アドレスと同等です。ChannelCredentials.Insecure
-gRPC 呼び出しではトランスポート セキュリティは使用されません。http
アドレスと同等です。
var channel = GrpcChannel.ForAddress(
"dns:///my-example-host",
new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
gRPC クライアント ファクトリで負荷分散を使う
gRPC クライアント ファクトリは、負荷分散を使うように構成できます。
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("dns:///my-example-host");
})
.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);
builder.Services.AddSingleton<ResolverFactory>(
sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));
var app = builder.Build();
上記のコードでは次の操作が行われます。
- 負荷分散のアドレスを使ってクライアントを構成します。
- チャネルの資格情報を指定します。
- DI 型をアプリの IServiceCollection に登録します。
カスタム リゾルバーとロード バランサーを作成する
クライアント側の負荷分散は拡張可能です。
Resolver
を実装してカスタム リゾルバーを作成し、新しいデータ ソースからアドレスを解決します。- 新しい負荷分散動作でカスタム ロード バランサーを作成するには、
LoadBalancer
を実装します。
重要
クライアント側の負荷分散の拡張に使用される API は試験段階です。 予告なしに変更される場合があります。
カスタム リゾルバーを作成する
リゾルバーは次のようなものです。
Resolver
を実装し、ResolverFactory
によって作成されます。 これらの型を実装して、カスタム リゾルバーを作成します。- ロード バランサーで使用されるアドレスを解決する役割があります。
- 必要に応じて、サービス構成を指定できます。
public class FileResolver : PollingResolver
{
private readonly Uri _address;
private readonly int _port;
public FileResolver(Uri address, int defaultPort, ILoggerFactory loggerFactory)
: base(loggerFactory)
{
_address = address;
_port = defaultPort;
}
public override async Task ResolveAsync(CancellationToken cancellationToken)
{
// Load JSON from a file on disk and deserialize into endpoints.
var jsonString = await File.ReadAllTextAsync(_address.LocalPath);
var results = JsonSerializer.Deserialize<string[]>(jsonString);
var addresses = results.Select(r => new BalancerAddress(r, _port)).ToArray();
// Pass the results back to the channel.
Listener(ResolverResult.ForResult(addresses));
}
}
public class FileResolverFactory : ResolverFactory
{
// Create a FileResolver when the URI has a 'file' scheme.
public override string Name => "file";
public override Resolver Create(ResolverOptions options)
{
return new FileResolver(options.Address, options.DefaultPort, options.LoggerFactory);
}
}
上のコードでは以下の操作が行われます。
FileResolverFactory
は、ResolverFactory
を実装します。file
スキームにマップされ、FileResolver
インスタンスを作成します。FileResolver
は、PollingResolver
を実装します。PollingResolver
は抽象基本型であり、ResolveAsync
をオーバーライドすることで、非同期ロジックを使用してリゾルバーを簡単に実装できます。- In:
ResolveAsync
- ファイルの URI はローカル パスに変換されます。 たとえば、
file:///c:/addresses.json
がc:\addresses.json
になります。 - JSON はディスクから読み込まれ、アドレスのコレクションに変換されます。
- リスナーは、アドレスが使用可能であることをチャネルに認識させるために、結果と共に呼び出されます。
- ファイルの URI はローカル パスに変換されます。 たとえば、
カスタム ロード バランサーを作成する
ロード バランサーは次のようなものです。
LoadBalancer
を実装し、LoadBalancerFactory
によって作成されます。 これらの型を実装して、カスタム ロード バランサーとファクトリを作成します。- リゾルバーからアドレスが与えられ、
Subchannel
インスタンスを作成します。 - 接続に関する状態を追跡し、
SubchannelPicker
を作成します。 チャネルでは、gRPC 呼び出しを行う際に、ピッカーを内部で使用してアドレスを選択します。
SubchannelsLoadBalancer
は次のようなものです。
LoadBalancer
を実装する抽象基本クラス。- アドレスからの
Subchannel
インスタンスの作成を管理します。 - サブチャネルのコレクションに対してカスタムのピッキング ポリシーを簡単に実装できるようにします。
public class RandomBalancer : SubchannelsLoadBalancer
{
public RandomBalancer(IChannelControlHelper controller, ILoggerFactory loggerFactory)
: base(controller, loggerFactory)
{
}
protected override SubchannelPicker CreatePicker(List<Subchannel> readySubchannels)
{
return new RandomPicker(readySubchannels);
}
private class RandomPicker : SubchannelPicker
{
private readonly List<Subchannel> _subchannels;
public RandomPicker(List<Subchannel> subchannels)
{
_subchannels = subchannels;
}
public override PickResult Pick(PickContext context)
{
// Pick a random subchannel.
return PickResult.ForSubchannel(_subchannels[Random.Shared.Next(0, _subchannels.Count)]);
}
}
}
public class RandomBalancerFactory : LoadBalancerFactory
{
// Create a RandomBalancer when the name is 'random'.
public override string Name => "random";
public override LoadBalancer Create(LoadBalancerOptions options)
{
return new RandomBalancer(options.Controller, options.LoggerFactory);
}
}
上のコードでは以下の操作が行われます。
RandomBalancerFactory
は、LoadBalancerFactory
を実装します。random
ポリシー名にマップされ、RandomBalancer
インスタンスを作成します。RandomBalancer
は、SubchannelsLoadBalancer
を実装します。 サブチャネルをランダムに選択するRandomPicker
を作成します。
カスタム リゾルバーとロード バランサーを構成する
カスタム リゾルバーとロード バランサーを使用する場合は、それらを依存関係の挿入 (DI) に登録する必要があります。 これには次の 2 つのオプションがあります。
- ASP.NET Core Web アプリなどのアプリで既に DI が使用されている場合は、既存の DI 構成で登録できます。 IServiceProvider を DI から解決し、
GrpcChannelOptions.ServiceProvider
を使用してチャネルに渡すことができます。 - アプリで DI が使用されていない場合は、以下のものを作成します。
- 型が登録されている ServiceCollection。
- BuildServiceProvider を使用するサービス プロバイダー。
var services = new ServiceCollection();
services.AddSingleton<ResolverFactory, FileResolverFactory>();
services.AddSingleton<LoadBalancerFactory, RandomLoadBalancerFactory>();
var channel = GrpcChannel.ForAddress(
"file:///c:/data/addresses.json",
new GrpcChannelOptions
{
Credentials = ChannelCredentials.Insecure,
ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new LoadBalancingConfig("random") } },
ServiceProvider = services.BuildServiceProvider()
});
var client = new Greet.GreeterClient(channel);
上記のコードでは次の操作が行われます。
ServiceCollection
を作成し、新しいリゾルバーとロード バランサーの実装を登録します。- 新しい実装を使用するように構成されたチャネルを作成します。
ServiceCollection
はIServiceProvider
に組み込まれ、GrpcChannelOptions.ServiceProvider
に設定されます。- チャネル アドレスは
file:///c:/data/addresses.json
です。file
スキームがFileResolverFactory
にマップされます。 service config
ロード バランサー名はrandom
です。RandomLoadBalancerFactory
にマップされます。
負荷分散が重要な理由
HTTP/2 により、1 つの TCP 接続での複数の呼び出しが多重化されます。 gRPC と HTTP/2 がネットワーク ロード バランサー (NLB) で使用されている場合、接続はサーバーに転送され、すべての gRPC 呼び出しはその 1 つのサーバーに送信されます。 NLB 上の他のサーバー インスタンスはアイドル状態です。
ネットワーク ロード バランサーは、高速で軽量であるため、負荷分散のための一般的なソリューションです。 たとえば、Kubernetes では既定でネットワーク ロード バランサーを使用して、ポッド インスタンス間の接続のバランスを取ります。 ただし、gRPC と HTTP/2 で使用する場合、ネットワーク ロード バランサーは負荷を分散するのに有効ではありません。
プロキシまたはクライアント側の負荷分散とは
gRPC と HTTP/2 は、アプリケーション ロード バランサーのプロキシまたはクライアント側の負荷分散を使用して、効果的に負荷分散することができます。 どちらのオプションでも、個々の gRPC 呼び出しを使用可能なサーバーに分散させることができます。 プロキシとクライアント側の負荷分散のどちらかに決定することは、アーキテクチャを選択することです。 それぞれに利点と欠点があります。
プロキシ: gRPC 呼び出しがプロキシに送信され、プロキシによって負荷分散が決定され、gRPC 呼び出しが最終的なエンドポイントに送信されます。 プロキシには、エンドポイントを認識する役割があります。 プロキシを使用すると、次のものが追加されます。
- gRPC 呼び出しへの追加のネットワーク ホップ。
- 待機時間。また、追加のリソースが消費されます。
- プロキシ サーバーがセットアップされ、正しく構成されている必要があります。
クライアント側の負荷分散: gRPC クライアントでは、gRPC 呼び出しが開始されたときに負荷分散の決定を行います。 gRPC 呼び出しは最終的なエンドポイントに直接送信されます。 クライアント側の負荷分散を使用する場合は、次のようになります。
- クライアントには、使用可能なエンドポイントについて認識し、負荷分散の決定を行う役割があります。
- 追加のクライアント構成が必要です。
- 高パフォーマンスで負荷分散された gRPC 呼び出しでは、プロキシが不要になります。
その他のリソース
ASP.NET Core