gRPC 客户端负载均衡

作者:James Newton-King

客户端负载均衡功能允许 gRPC 客户端以最佳方式在可用服务器之间分配负载。 本文介绍了如何配置客户端负载均衡,以在 .NET 中创建可缩放的高性能 gRPC 应用。

使用客户端负载均衡需要具备以下组件:

  • .NET 5 或更高版本。
  • Grpc.Net.Client 版本 2.45.0 或更高版本。

配置 gRPC 客户端负载均衡

客户端负载均衡是在创建通道时配置的。 使用负载均衡时需要考虑两个组件:

  • 解析程序,用于解析通道的地址。 解析程序支持从外部源获取地址。 这也被称为服务发现。
  • 负载均衡器,用于创建连接,并选取 gRPC 调用将使用的地址。

解析程序和负载均衡器的内置实现包含在 Grpc.Net.Client 中。 也可以通过编写自定义解析程序和负载均衡器来扩展负载均衡。

地址、连接和其他负载均衡状态存储在 GrpcChannel 实例中。 在进行 gRPC 调用时,必须重用通道,以使负载均衡正常工作。

注意

某些负载均衡配置使用依赖项注入 (DI)。 不使用 DI 的应用可以创建 ServiceCollection 实例。

如果应用已设置 DI(如 ASP.NET Core 网站),则应向现有 DI 实例注册类型。 GrpcChannelOptions.ServiceProvider 是通过从 DI 获取 IServiceProvider 来配置的。

配置解析程序

解析程序是使用创建通道时所用的地址配置的。 地址的 URI 方案指定解析程序。

Scheme 类型 说明
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.100127.0.0.101
  • 负载均衡器使用 127.0.0.100:80127.0.0.101:80 创建连接并进行 gRPC 调用。

DnsResolverFactory

DnsResolverFactory 创建一个解析程序,旨在从外部源获取地址。 DNS 解析通常用于对具有 Kubernetes 无外设服务的 Pod 实例进行负载均衡。

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。 这是非安全通道的默认端口。 可以选择在主机名后面指定端口。 例如,dns:///my-example-host:8080 将 gRPC 调用配置为发送到端口 8080。
  • 不指定负载均衡器。 通道默认选取第一个负载均衡器。
  • 启动 gRPC 调用 SayHello
    • DNS 解析程序为主机名 my-example-host 获取地址。
    • 选取第一个负载均衡器以尝试连接到一个已解析的地址。
    • 调用被发送到通道成功连接到的第一个地址。
DNS 地址缓存

在使用负载均衡时,性能非常重要。 通过缓存地址,从 gRPC 调用中消除了解析地址的延迟。 进行第一次 gRPC 调用时,将调用解析程序,后续调用将使用缓存。

如果连接中断,则会自动刷新地址。 如果是在运行时更改地址,那么刷新非常重要。 例如,在 Kubernetes 中,重启的 Pod 会触发 DNS 解析程序刷新并获取 Pod 的新地址。

默认情况下,如果连接中断,则会刷新 DNS 解析程序。 DNS 解析程序还可以根据需要定期刷新。 这对于快速检测新的 pod 实例很有用。

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:80localhost:81
  • 使用依赖项注入 (DI) 来注册工厂。
  • 使用以下内容配置已创建的通道:
    • 地址 static:///my-example-hoststatic 方案映射到静态解析程序。
    • 使用 DI 服务提供程序设置 GrpcChannelOptions.ServiceProvider

本示例为 DI 创建了一个新的 ServiceCollection。 假设一个应用已设置 DI,比如一个 ASP.NET Core 网站。 在这种情况下,类型应被注册到现有的 DI 实例。 GrpcChannelOptions.ServiceProvider 是通过从 DI 获取 IServiceProvider 来配置的。

配置负载均衡器

负载均衡器是使用 ServiceConfig.LoadBalancingConfigs 集合在 service config 中指定的。 两个负载均衡器都是内置的,映射到负载均衡器配置名称:

名称 Type 说明
pick_first PickFirstLoadBalancerFactory 尝试连接到地址,直到成功建立连接。 gRPC 调用都是针对第一次成功连接进行的。
round_robin RoundRobinLoadBalancerFactory 尝试连接到所有地址。 gRPC 调用是使用轮循机制逻辑分布在所有成功的连接上的。

service config 是“service configuration”的缩写形式,用 ServiceConfig 类型表示。 有几种方法可以让通道获取配置了负载均衡器的 service config

  • 当使用 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 调用使用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();

前面的代码:

  • 使用负载均衡地址配置客户端。
  • 指定通道凭据。
  • 向应用的 IServiceCollection 注册 DI 类型。

编写自定义解析程序和负载均衡器

客户端负载均衡具有可扩展性,可以:

  • 实现 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 可实现 PollingResolverPollingResolver 是一个抽象基类,通过重写 ResolveAsync 可以轻松地通过异步逻辑实现解析程序。
  • ResolveAsync中:
    • 文件 URI 被转换为本地路径。 例如,file:///c:/addresses.json 重命名为 c:\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 = 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) 中注册。 有以下几种方式:

  • 如果应用已经在使用 DI,例如 ASP.NET Core Web 应用,则可以在现有 DI 配置中注册它们。 可以从 DI 解析 IServiceProvider,然后使用 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 并注册新的解析程序和负载均衡器实现。
  • 创建一个配置为使用新实现的通道:
    • ServiceCollection 内置于 IServiceProvider 中,并设置为 GrpcChannelOptions.ServiceProvider
    • 通道地址为 file:///c:/data/addresses.jsonfile 方案映射到 FileResolverFactory
    • service config 负载均衡器名称为 random。 映射到 RandomLoadBalancerFactory

为什么负载均衡很重要

HTTP/2 在单个 TCP 连接上多路复用多个调用。 如果将 gRPC 和 HTTP/2 与网络负载均衡器 (NLB) 结合使用,连接将被转发到服务器,并且所有 gRPC 调用都将被发送到该服务器。 NLB 上的其他服务器实例处于空闲状态。

网络负载均衡器是负载均衡的一种常见解决方案,因为它们速度快,并且是轻量级的。 例如,Kubernetes 默认使用网络负载均衡器来均衡 Pod 实例之间的连接。 然而,与 gRPC 和 HTTP/2 一起使用时,网络负载均衡器不能有效地分配负载。

代理或客户端负载均衡怎么样?

可以使用应用程序负载均衡器代理或客户端负载均衡方式有效地对 gRPC 和 HTTP/2 进行负载均衡。 这两种方式都允许在所有可用服务器中分布各个 gRPC 调用。 在代理和客户端负载均衡之间做出决策是一种体系结构选择。 每个选项都有优缺点。

  • 代理:gRPC 调用被发送到代理,代理做出负载均衡决策,然后 gRPC 调用被发送到最终终结点。 代理负责了解终结点。 使用代理添加:

    • 一个用于 gRPC 调用的附加网络跃点。
    • 延迟并消耗其他资源。
    • 代理服务器必须进行正确设置和配置。
  • 客户端负载均衡:启动 gRPC 调用时,gRPC 客户端做出负载均衡决策。 gRPC 调用被直接发送到最终终结点。 使用客户端负载均衡时:

    • 客户端负责了解可用终结点并做出负载均衡决策。
    • 需要额外的客户端配置。
    • 高性能、经过负载均衡的 gRPC 调用不需要代理。

其他资源