gRPC クライアント側の負荷分散

作成者: James Newton-King

クライアント側の負荷分散は、gRPC クライアントで、使用可能なサーバー間で負荷を最適に分散できるようにする機能です。 この記事では、.NET でスケーラブルで高パフォーマンスの gRPC アプリを作成するように、クライアント側の負荷分散を構成する方法について説明します。

クライアント側の負荷分散に必要なものは次のとおりです。

重要

この機能はプレビュー段階にあります。 他の gRPC 機能との統合は完全ではないため、テストが必要です。

現在、クライアント側の負荷分散は次のような状態です。

  • NuGet.org 上のプレリリース バージョンの Grpc.Net.Client でのみ使用できます。
  • gRPC クライアント ファクトリと一緒に使用する場合はサポートされません。

gRPC クライアント側の負荷分散を構成する

クライアント側の負荷分散は、チャネルの作成時に構成されます。 負荷分散を使用する際に考慮する必要がある 2 つのコンポーネントは、次のとおりです。

  • リゾルバー。チャネルのアドレスを解決します。 リゾルバーでは、外部ソースからのアドレスの取得がサポートされます。 これはサービス検出とも呼ばれます。
  • ロード バランサー。接続を作成し、gRPC 呼び出しで使用されるアドレスを選択します。

リゾルバーとロード バランサーの組み込み実装は、Grpc.Net.Client に含まれています。 負荷分散は、カスタム リゾルバーとロード バランサーを記述することで拡張することもできます。

アドレス、接続およびその他の負荷分散状態は、GrpcChannel インスタンスに格納されます。 負荷分散を正しく機能させるには、gRPC 呼び出しを行う際にチャネルを再利用する必要があります。

リゾルバーを構成する

リゾルバーは、チャネルが作成されるアドレスを使用して構成されます。 アドレスの 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 クエリを作成し、localhost:80localhost:81 の 2 つの結果を取得します。
  • ロード バランサーでは、localhost:80localhost:81 を使用して接続を作成し、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 にマップされます。
  • ロード バランサーは指定しません。 チャネルの既定値は、第一候補のロード バランサーです。
  • gRPC 呼び出し SayHello を開始します。
    • DNS リゾルバーでは、ホスト名 my-example-host のアドレスを取得します。
    • 第一候補のロード バランサーでは、解決済みのアドレスのいずれかへの接続を試行します。
    • 呼び出しは、チャネルが正常に接続された最初のアドレスに送信されます。

負荷分散を行う際にはパフォーマンスが重要になります。 アドレスの解決の待機時間は、アドレスをキャッシュすることで gRPC 呼び出しから除外されます。 最初の gRPC 呼び出しを行う際にリゾルバーが呼び出され、それ以降の呼び出しでキャッシュが使用されます。 接続が中断された場合、アドレスは自動的に更新されます。 実行時にアドレスが変更されるシナリオでは、更新が重要になります。 たとえば、Kubernetes では、再起動されたポッドによって DNS リゾルバーがトリガーされ、ポッドの新しいアドレスが更新されて取得されます。

StaticResolverFactory

静的リゾルバーは StaticResolverFactory によって提供されます。 このリゾルバーは次のようなものです。

  • 外部ソースは呼び出しません。 代わりに、クライアント アプリによってアドレスが構成されます。
  • アプリで呼び出すアドレスが既に認識されている状況向けに設計されています。
var factory = new StaticResolverFactory(addr => new[]
{
    new DnsEndPoint("localhost", 80),
    new DnsEndPoint("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:80localhost:81 という 2 つのアドレスが認識されています。
  • ファクトリを依存関係の挿入 (DI) に登録します。
  • 以下を使用して、作成されたチャネルを構成します。
    • アドレス static:///my-example-hoststatic スキームが静的リゾルバーにマップされます。
    • 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 は service configuration の省略形であり、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 呼び出しがトランスポート セキュリティを使用して送信されるかどうかを認識している必要があります。 httphttps はアドレスの一部ではなくなりました。このスキームでは現在、リゾルバーが指定されているため、負荷分散を使用する際にはチャネル オプションで Credentials を構成する必要があります。

  • ChannelCredentials.SecureSsl - gRPC 呼び出しは、トランスポート層セキュリティ (TLS) で保護されます。 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" });

カスタム リゾルバーとロード バランサーを作成する

クライアント側の負荷分散は拡張可能です。

  • Resolver を実装してカスタム リゾルバーを作成し、新しいデータ ソースからアドレスを解決します。
  • 新しい負荷分散動作でカスタム ロード バランサーを作成するには、LoadBalancer を実装します。

重要

クライアント側の負荷分散の拡張に使用される API は試験段階です。 予告なしに変更される場合があります。

カスタム リゾルバーを作成する

リゾルバーは次のようなものです。

  • Resolver を実装し、ResolverFactory によって作成されます。 これらの型を実装して、カスタム リゾルバーを作成します。
  • ロード バランサーで使用されるアドレスを解決する役割があります。
  • 必要に応じて、service configuration を指定できます。
public class FileResolver : Resolver
{
    private readonly Uri _address;
    private Action<ResolverResult> _listener;

    public ExampleResolver(Uri address)
    {
        _address = address;
    }

    public override async Task RefreshAsync(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 DnsEndPoint(r, 80));

        // Pass the results back to the channel.
        _listener(ResolverResult.ForResult(addresses, serviceConfig: null));
    }

    public override void Start(Action<ResolverResult> listener)
    {
        _listener = listener;
    }
}

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);
    }
}

上のコードでは以下の操作が行われます。

  • FileResolverFactory は、ResolverFactory を実装します。 file スキームにマップされ、FileResolver インスタンスを作成します。
  • FileResolver は、Resolver を実装します。 RefreshAsync:
    • ファイルの URI はローカル パスに変換されます。 たとえば、file:///c:/addresses.jsonc:\addresses.json になります。
    • JSON はディスクから読み込まれ、アドレスのコレクションに変換されます。
    • リスナーは、アドレスが使用可能であることをチャネルに認識させるために、結果と共に呼び出されます。

カスタム ロード バランサーを作成する

ロード バランサーは次のようなものです。

  • 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 = readySubchannels;
        }

        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 option)
    {
        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 が使用されていない場合は、以下のものを作成します。
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 を作成し、新しいリゾルバーとロード バランサーの実装を登録します。
  • 新しい実装を使用するように構成されたチャネルを作成します。
    • ServiceCollectionIServiceProvider に組み込まれ、GrpcChannelOptions.ServiceProvider に設定されます。
    • チャネル アドレスは file:///c:/data/addresses.json です。 file スキームが FileResolverFactory にマップされます。
    • サービス構成のロード バランサー名は 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 呼び出しでは、プロキシが不要になります。

その他のリソース