aspnetcore/grpc/loadbalancing.md
Client-side load balancing is a feature that allows gRPC clients to distribute load optimally across available servers. This article discusses how to configure client-side load balancing to create scalable, high-performance gRPC apps in .NET.
Client-side load balancing requires:
Grpc.Net.Client version 2.45.0 or later.Client-side load balancing is configured when a channel is created. The two components to consider when using load balancing:
Built-in implementations of resolvers and load balancers are included in Grpc.Net.Client. Load balancing can also be extended by writing custom resolvers and load balancers.
Addresses, connections and other load balancing state is stored in a GrpcChannel instance. A channel must be reused when making gRPC calls for load balancing to work correctly.
[!NOTE] Some load balancing configuration uses dependency injection (DI). Apps that don't use DI can create a xref:Microsoft.Extensions.DependencyInjection.ServiceCollection instance.
If an app already has DI setup, like an ASP.NET Core website, then types should be registered with the existing DI instance.
GrpcChannelOptions.ServiceProvideris configured by getting an xref:System.IServiceProvider from DI.
The resolver is configured using the address a channel is created with. The URI scheme of the address specifies the resolver.
| Scheme | Type | Description |
|---|---|---|
dns | DnsResolverFactory | Resolves addresses by querying the hostname for DNS address records. |
static | StaticResolverFactory | Resolves addresses that the app has specified. Recommended if an app already knows the addresses it calls. |
A channel doesn't directly call a URI that matches a resolver. Instead, a matching resolver is created and used to resolve the addresses.
For example, using GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure }):
dns scheme maps to DnsResolverFactory. A new instance of a DNS resolver is created for the channel.my-example-host and gets two results: 127.0.0.100 and 127.0.0.101.127.0.0.100:80 and 127.0.0.101:80 to create connections and make gRPC calls.The DnsResolverFactory creates a resolver designed to get addresses from an external source. DNS resolution is commonly used to load balance over pod instances that have a Kubernetes headless services.
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" });
The preceding code:
dns:///my-example-host.
dns scheme maps to DnsResolverFactory.my-example-host is the hostname to resolve.dns:///my-example-host:8080 configures gRPC calls to be sent to port 8080.SayHello:
my-example-host.Performance is important when load balancing. The latency of resolving addresses is eliminated from gRPC calls by caching the addresses. A resolver will be invoked when making the first gRPC call, and subsequent calls use the cache.
Addresses are automatically refreshed if a connection is interrupted. Refreshing is important in scenarios where addresses change at runtime. For example, in Kubernetes a restarted pod triggers the DNS resolver to refresh and get the pod's new address.
By default, a DNS resolver is refreshed if a connection is interrupted. The DNS resolver can also optionally refresh itself on a periodic interval. This can be useful for quickly detecting new pod instances.
services.AddSingleton<ResolverFactory>(
sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));
The preceding code creates a DnsResolverFactory with a refresh interval and registers it with dependency injection. For more information on using a custom-configured resolver, see Configure custom resolvers and load balancers.
A static resolver is provided by StaticResolverFactory. This resolver:
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);
The preceding code:
StaticResolverFactory. This factory knows about two addresses: localhost:80 and localhost:81.static:///my-example-host. The static scheme maps to a static resolver.GrpcChannelOptions.ServiceProvider with the DI service provider.This example creates a new xref:Microsoft.Extensions.DependencyInjection.ServiceCollection for DI. Suppose an app already has DI setup, like an ASP.NET Core website. In that case, types should be registered with the existing DI instance. GrpcChannelOptions.ServiceProvider is configured by getting an xref:System.IServiceProvider from DI.
A load balancer is specified in a service config using the ServiceConfig.LoadBalancingConfigs collection. Two load balancers are built-in and map to load balancer config names:
| Name | Type | Description |
|---|---|---|
pick_first | PickFirstLoadBalancerFactory | Attempts to connect to addresses until a connection is successfully made. gRPC calls are all made to the first successful connection. |
round_robin | RoundRobinLoadBalancerFactory | Attempts to connect to all addresses. gRPC calls are distributed across all successful connections using round-robin logic. |
service config is an abbreviation of service configuration and is represented by the ServiceConfig type. There are a couple of ways a channel can get a service config with a load balancer configured:
service config when a channel is created using GrpcChannelOptions.ServiceConfig.service config for a channel. This feature allows an external source to specify how its callers should perform load balancing. Whether a resolver supports resolving a service config is dependent on the resolver implementation. Disable this feature with GrpcChannelOptions.DisableResolverServiceConfig.service config is provided, or the service config doesn't have a load balancer configured, the channel defaults to 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" });
The preceding code:
RoundRobinLoadBalancerFactory in the service config.SayHello:
DnsResolverFactory creates a resolver that gets addresses for the hostname my-example-host.A channel must know whether gRPC calls are sent using transport security. http and https are no longer part of the address, the scheme now specifies a resolver, so Credentials must be configured on channel options when using load balancing.
ChannelCredentials.SecureSsl - gRPC calls are secured with Transport Layer Security (TLS). Equivalent to an https address.ChannelCredentials.Insecure - gRPC calls don't use transport security. Equivalent to an http address.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 client factory can be configured to use load balancing:
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();
The preceding code:
Client-side load balancing is extensible:
Resolver to create a custom resolver and resolve addresses from a new data source.LoadBalancer to create a custom load balancer with new load balancing behavior.[!IMPORTANT] The APIs used to extend client-side load balancing are experimental. They can change without notice.
A resolver:
Resolver and is created by a ResolverFactory. Create a custom resolver by implementing these types.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);
}
}
In the preceding code:
FileResolverFactory implements ResolverFactory. It maps to the file scheme and creates FileResolver instances.FileResolver implements PollingResolver. PollingResolver is an abstract base type that makes it easy to implement a resolver with asynchronous logic by overriding ResolveAsync.ResolveAsync:
file:///c:/addresses.json becomes c:\addresses.json.A load balancer:
LoadBalancer and is created by a LoadBalancerFactory. Create a custom load balancer and factory by implementing these types.Subchannel instances.SubchannelPicker. The channel internally uses the picker to pick addresses when making gRPC calls.The SubchannelsLoadBalancer is:
LoadBalancer.Subchannel instances from addresses.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);
}
}
In the preceding code:
RandomBalancerFactory implements LoadBalancerFactory. It maps to the random policy name and creates RandomBalancer instances.RandomBalancer implements SubchannelsLoadBalancer. It creates a RandomPicker that randomly picks a subchannel.Custom resolvers and load balancers need to be registered with dependency injection (DI) when they are used. There are a couple of options:
GrpcChannelOptions.ServiceProvider.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);
The preceding code:
ServiceCollection and registers new resolver and load balancer implementations.ServiceCollection is built into an IServiceProvider and set to GrpcChannelOptions.ServiceProvider.file:///c:/data/addresses.json. The file scheme maps to FileResolverFactory.service config load balancer name is random. Maps to RandomLoadBalancerFactory.HTTP/2 multiplexes multiple calls on a single TCP connection. If gRPC and HTTP/2 are used with a network load balancer (NLB), the connection is forwarded to a server, and all gRPC calls are sent to that one server. The other server instances on the NLB are idle.
Network load balancers are a common solution for load balancing because they are fast and lightweight. For example, Kubernetes by default uses a network load balancer to balance connections between pod instances. However, network load balancers are not effective at distributing load when used with gRPC and HTTP/2.
gRPC and HTTP/2 can be effectively load balanced using either an application load balancer proxy or client-side load balancing. Both of these options allow individual gRPC calls to be distributed across available servers. Deciding between proxy and client-side load balancing is an architectural choice. There are pros and cons for each.
Proxy: gRPC calls are sent to the proxy, the proxy makes a load balancing decision, and the gRPC call is sent on to the final endpoint. The proxy is responsible for knowing about endpoints. Using a proxy adds:
Client-side load balancing: The gRPC client makes a load balancing decision when a gRPC call is started. The gRPC call is sent directly to the final endpoint. When using client-side load balancing: