gRPC client-side load balancing (cân bằng tải phía client)
Client-side load balancing (cân bằng tải phía client) là tính năng cho phép gRPC client phân phối tải tối ưu trên các server có sẵn. Bài viết này thảo luận cách cấu hình client-side load balancing để tạo các ứng dụng gRPC có thể mở rộng và hiệu suất cao trong .NET.
Client-side load balancing yêu cầu:
- .NET 5 trở lên.
Grpc.Net.Clientphiên bản 2.45.0 trở lên.
Cấu hình gRPC client-side load balancing
Client-side load balancing được cấu hình khi tạo channel. Hai thành phần cần xem xét khi sử dụng load balancing:
- Resolver (bộ giải địa chỉ): giải quyết các địa chỉ cho channel. Resolver hỗ trợ lấy địa chỉ từ nguồn bên ngoài. Điều này còn được gọi là service discovery (khám phá dịch vụ).
- Load balancer (bộ cân bằng tải): tạo kết nối và chọn địa chỉ mà lệnh gọi gRPC sẽ sử dụng.
Các triển khai tích hợp sẵn của resolver và load balancer được bao gồm trong Grpc.Net.Client. Load balancing cũng có thể được mở rộng bằng cách viết resolver và load balancer tùy chỉnh.
Địa chỉ, kết nối và trạng thái load balancing khác được lưu trong một instance GrpcChannel. Channel phải được tái sử dụng khi thực hiện các lệnh gọi gRPC để load balancing hoạt động đúng.
Một số cấu hình load balancing sử dụng dependency injection (DI - tiêm phụ thuộc). Các ứng dụng không dùng DI có thể tạo một instance ServiceCollection. Nếu ứng dụng đã có DI setup, như một website ASP.NET Core, thì các type nên được đăng ký với DI instance hiện có. GrpcChannelOptions.ServiceProvider được cấu hình bằng cách lấy IServiceProvider từ DI.
Cấu hình resolver
Resolver được cấu hình bằng địa chỉ mà channel được tạo với. URI scheme (lược đồ URI) của địa chỉ xác định resolver.
| Scheme | Type | Mô tả |
|---|---|---|
dns | DnsResolverFactory | Giải địa chỉ bằng cách truy vấn hostname cho các DNS address records. |
static | StaticResolverFactory | Giải các địa chỉ mà ứng dụng đã chỉ định. Được khuyến nghị nếu ứng dụng đã biết các địa chỉ cần gọi. |
Channel không gọi trực tiếp một URI khớp với resolver. Thay vào đó, một resolver khớp được tạo và dùng để giải địa chỉ.
Ví dụ, khi dùng GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure }):
- Scheme
dnsánh xạ đếnDnsResolverFactory. Một instance DNS resolver mới được tạo cho channel. - Resolver thực hiện DNS query cho
my-example-hostvà nhận được hai kết quả:127.0.0.100và127.0.0.101. - Load balancer sử dụng
127.0.0.100:80và127.0.0.101:80để tạo kết nối và thực hiện lệnh gọi gRPC.
DnsResolverFactory
DnsResolverFactory tạo một resolver được thiết kế để lấy địa chỉ từ nguồn bên ngoài. DNS resolution thường được sử dụng để cân bằng tải trên các pod instance có 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" });Đoạn code trên:
- Cấu hình channel đã tạo với địa chỉ
dns:///my-example-host. - Scheme
dnsánh xạ đếnDnsResolverFactory. my-example-hostlà hostname cần giải.- Không có port được chỉ định trong địa chỉ, vì vậy các lệnh gọi gRPC được gửi đến port 80. Đây là port mặc định cho các channel không bảo mật. Một port có thể được chỉ định tùy chọn sau hostname. Ví dụ:
dns:///my-example-host:8080cấu hình gửi lệnh gọi gRPC đến port 8080. - Không chỉ định load balancer. Channel mặc định là pick first load balancer.
- Bắt đầu lệnh gọi gRPC
SayHello: - DNS resolver lấy địa chỉ cho hostname
my-example-host. - Pick first load balancer cố gắng kết nối đến một trong các địa chỉ đã giải.
- Lệnh gọi được gửi đến địa chỉ đầu tiên mà channel kết nối thành công.
DNS address caching (bộ nhớ đệm địa chỉ DNS)
Hiệu suất rất quan trọng khi load balancing. Độ trễ khi giải địa chỉ được loại bỏ khỏi các lệnh gọi gRPC bằng cách cache địa chỉ. Một resolver sẽ được gọi khi thực hiện lệnh gọi gRPC đầu tiên, và các lệnh gọi tiếp theo sử dụng cache.
Địa chỉ được tự động làm mới nếu kết nối bị gián đoạn. Việc làm mới quan trọng trong các tình huống địa chỉ thay đổi trong runtime. Ví dụ, trong Kubernetes một pod khởi động lại kích hoạt DNS resolver để làm mới và lấy địa chỉ mới của pod.
Theo mặc định, DNS resolver được làm mới nếu kết nối bị gián đoạn. DNS resolver cũng có thể tùy chọn tự làm mới theo khoảng thời gian định kỳ. Điều này có thể hữu ích để phát hiện nhanh các pod instance mới.
services.AddSingleton<ResolverFactory>(
sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));Đoạn code trên tạo DnsResolverFactory với khoảng thời gian làm mới và đăng ký với dependency injection. Để biết thêm thông tin về việc sử dụng resolver tùy chỉnh đã cấu hình, xem phần Cấu hình resolver và load balancer tùy chỉnh.
StaticResolverFactory
Static resolver được cung cấp bởi StaticResolverFactory. Resolver này:
- Không gọi nguồn bên ngoài. Thay vào đó, ứng dụng client cấu hình các địa chỉ.
- Được thiết kế cho các tình huống khi ứng dụng đã biết các địa chỉ cần gọi.
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);Đoạn code trên:
- Tạo
StaticResolverFactory. Factory này biết về hai địa chỉ:localhost:80vàlocalhost:81. - Đăng ký factory với dependency injection (DI).
- Cấu hình channel đã tạo với:
- Địa chỉ
static:///my-example-host. Schemestaticánh xạ đến static resolver. - Đặt
GrpcChannelOptions.ServiceProvidervới DI service provider.
Ví dụ này tạo một ServiceCollection mới cho DI. Giả sử ứng dụng đã có DI setup, như một website ASP.NET Core. Trong trường hợp đó, các type nên được đăng ký với DI instance hiện có. GrpcChannelOptions.ServiceProvider được cấu hình bằng cách lấy IServiceProvider từ DI.
Cấu hình load balancer
Load balancer được chỉ định trong một service config sử dụng collection ServiceConfig.LoadBalancingConfigs. Hai load balancer được tích hợp sẵn và ánh xạ đến tên cấu hình load balancer:
| Tên | Type | Mô tả |
|---|---|---|
pick_first | PickFirstLoadBalancerFactory | Cố gắng kết nối đến các địa chỉ cho đến khi kết nối thành công. Tất cả các lệnh gọi gRPC được thực hiện đến kết nối thành công đầu tiên. |
round_robin | RoundRobinLoadBalancerFactory | Cố gắng kết nối đến tất cả các địa chỉ. Các lệnh gọi gRPC được phân phối trên tất cả các kết nối thành công sử dụng logic round-robin. |
service config là viết tắt của service configuration và được biểu diễn bởi type ServiceConfig. Có một vài cách để channel có thể nhận service config với load balancer đã cấu hình:
- Ứng dụng có thể chỉ định
service configkhi tạo channel bằngGrpcChannelOptions.ServiceConfig. - Ngoài ra, một resolver có thể giải
service configcho một channel. Tính năng này cho phép nguồn bên ngoài chỉ định cách các caller của nó thực hiện load balancing. Việc resolver có hỗ trợ giảiservice confighay không phụ thuộc vào triển khai của resolver. Tắt tính năng này vớiGrpcChannelOptions.DisableResolverServiceConfig. - Nếu không có
service confignào được cung cấp, hoặcservice configkhông có load balancer được cấu hình, channel mặc định là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" });Đoạn code trên:
- Chỉ định
RoundRobinLoadBalancerFactorytrongservice config. - Bắt đầu lệnh gọi gRPC
SayHello: DnsResolverFactorytạo resolver lấy địa chỉ cho hostnamemy-example-host.- Round-robin load balancer cố gắng kết nối đến tất cả các địa chỉ đã giải.
- Các lệnh gọi gRPC được phân phối đều bằng logic round-robin.
Cấu hình channel credentials
Channel phải biết liệu các lệnh gọi gRPC được gửi bằng transport security hay không. http và https không còn là một phần của địa chỉ nữa, scheme hiện chỉ định resolver, vì vậy Credentials phải được cấu hình trong channel options khi sử dụng load balancing.
ChannelCredentials.SecureSsl- Các lệnh gọi gRPC được bảo mật bằng Transport Layer Security (TLS). Tương đương với địa chỉhttps.ChannelCredentials.Insecure- Các lệnh gọi gRPC không sử dụng transport security. Tương đương với địa chỉ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" });Sử dụng load balancing với gRPC client factory
gRPC client factory có thể được cấu hình để sử dụng 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();Đoạn code trên:
- Cấu hình client với địa chỉ load balancing.
- Chỉ định channel credentials.
- Đăng ký các DI type với
IServiceCollectioncủa ứng dụng.
Viết resolver và load balancer tùy chỉnh
Client-side load balancing có thể mở rộng:
- Triển khai
Resolverđể tạo resolver tùy chỉnh và giải địa chỉ từ nguồn dữ liệu mới. - Triển khai
LoadBalancerđể tạo load balancer tùy chỉnh với hành vi load balancing mới.
Các API được dùng để mở rộng client-side load balancing là thử nghiệm. Chúng có thể thay đổi mà không cần thông báo.
Tạo resolver tùy chỉnh
Một resolver:
- Triển khai
Resolvervà được tạo bởiResolverFactory. Tạo resolver tùy chỉnh bằng cách triển khai các type này. - Chịu trách nhiệm giải các địa chỉ mà load balancer sử dụng.
- Có thể tùy chọn cung cấp service configuration.
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)
{
// Tải JSON từ file trên đĩa và deserialize thành endpoints.
var jsonString = await File.ReadAllTextAsync(_address.LocalPath);
var results = JsonSerializer.Deserialize<string[]>(jsonString);
var addresses = results.Select(r => new BalancerAddress(r, _port)).ToArray();
// Chuyển kết quả về cho channel.
Listener(ResolverResult.ForResult(addresses));
}
}
public class FileResolverFactory : ResolverFactory
{
// Tạo FileResolver khi URI có scheme 'file'.
public override string Name => "file";
public override Resolver Create(ResolverOptions options)
{
return new FileResolver(options.Address, options.DefaultPort, options.LoggerFactory);
}
}Trong đoạn code trên:
FileResolverFactorytriển khaiResolverFactory. Nó ánh xạ đến schemefilevà tạo các instanceFileResolver.FileResolvertriển khaiPollingResolver.PollingResolverlà abstract base type giúp dễ dàng triển khai resolver với logic bất đồng bộ bằng cách overrideResolveAsync.- Trong
ResolveAsync: - File URI được chuyển thành local path. Ví dụ:
file:///c:/addresses.jsonthànhc:\addresses.json. - JSON được tải từ đĩa và chuyển đổi thành tập hợp địa chỉ.
- Listener được gọi với kết quả để cho channel biết địa chỉ có sẵn.
Tạo load balancer tùy chỉnh
Một load balancer:
- Triển khai
LoadBalancervà được tạo bởiLoadBalancerFactory. Tạo load balancer tùy chỉnh và factory bằng cách triển khai các type này. - Nhận địa chỉ từ resolver và tạo các instance
Subchannel. - Theo dõi trạng thái kết nối và tạo
SubchannelPicker. Channel nội bộ sử dụng picker để chọn địa chỉ khi thực hiện lệnh gọi gRPC.
SubchannelsLoadBalancer là:
- Abstract base class triển khai
LoadBalancer. - Quản lý việc tạo các instance
Subchanneltừ địa chỉ. - Giúp dễ dàng triển khai chính sách picking tùy chỉnh trên tập hợp các 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)
{
// Chọn một subchannel ngẫu nhiên.
return PickResult.ForSubchannel(_subchannels[Random.Shared.Next(0, _subchannels.Count)]);
}
}
}
public class RandomBalancerFactory : LoadBalancerFactory
{
// Tạo RandomBalancer khi tên là 'random'.
public override string Name => "random";
public override LoadBalancer Create(LoadBalancerOptions options)
{
return new RandomBalancer(options.Controller, options.LoggerFactory);
}
}Trong đoạn code trên:
RandomBalancerFactorytriển khaiLoadBalancerFactory. Nó ánh xạ đến tên policyrandomvà tạo các instanceRandomBalancer.RandomBalancertriển khaiSubchannelsLoadBalancer. Nó tạoRandomPickerđể chọn ngẫu nhiên một subchannel.
Cấu hình resolver và load balancer tùy chỉnh
Resolver và load balancer tùy chỉnh cần được đăng ký với dependency injection (DI) khi sử dụng. Có một vài lựa chọn:
- Nếu ứng dụng đang sử dụng DI, chẳng hạn như web app ASP.NET Core, chúng có thể được đăng ký với cấu hình DI hiện có.
IServiceProvidercó thể được lấy từ DI và truyền vào channel bằngGrpcChannelOptions.ServiceProvider. - Nếu ứng dụng không sử dụng DI thì tạo:
ServiceCollectionvới các type được đăng ký.- Service provider sử dụng
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);Đoạn code trên:
- Tạo
ServiceCollectionvà đăng ký các triển khai resolver và load balancer mới. - Tạo channel được cấu hình để sử dụng các triển khai mới:
ServiceCollectionđược build thànhIServiceProvidervà đặt choGrpcChannelOptions.ServiceProvider.- Địa chỉ channel là
file:///c:/data/addresses.json. Schemefileánh xạ đếnFileResolverFactory. - Tên load balancer trong
service configlàrandom. Ánh xạ đếnRandomLoadBalancerFactory.
Tại sao load balancing quan trọng
HTTP/2 ghép nhiều lệnh gọi trên một kết nối TCP duy nhất. Nếu gRPC và HTTP/2 được sử dụng với network load balancer (NLB - bộ cân bằng tải mạng), kết nối được chuyển tiếp đến một server và tất cả các lệnh gọi gRPC được gửi đến server đó. Các server instance khác trên NLB sẽ nhàn rỗi.
Network load balancer là giải pháp phổ biến cho load balancing vì chúng nhanh và nhẹ. Ví dụ, Kubernetes mặc định sử dụng network load balancer để cân bằng kết nối giữa các pod instance. Tuy nhiên, network load balancer không hiệu quả trong việc phân phối tải khi sử dụng với gRPC và HTTP/2.
Proxy hay client-side load balancing?
gRPC và HTTP/2 có thể được cân bằng tải hiệu quả bằng cách sử dụng application load balancer proxy hoặc client-side load balancing. Cả hai tùy chọn này đều cho phép các lệnh gọi gRPC riêng lẻ được phân phối trên các server có sẵn. Việc quyết định giữa proxy và client-side load balancing là lựa chọn kiến trúc. Mỗi cái đều có ưu và nhược điểm.
- Proxy: Các lệnh gọi gRPC được gửi đến proxy, proxy đưa ra quyết định load balancing và lệnh gọi gRPC được gửi đến endpoint cuối cùng. Proxy chịu trách nhiệm biết về các endpoint. Sử dụng proxy thêm:
- Thêm một network hop vào các lệnh gọi gRPC.
- Độ trễ và tiêu thụ thêm tài nguyên.
- Proxy server phải được setup và cấu hình đúng cách.
- Client-side load balancing: gRPC client đưa ra quyết định load balancing khi một lệnh gọi gRPC được bắt đầu. Lệnh gọi gRPC được gửi trực tiếp đến endpoint cuối cùng. Khi sử dụng client-side load balancing:
- Client chịu trách nhiệm biết về các endpoint có sẵn và đưa ra quyết định load balancing.
- Cần cấu hình thêm phía client.
- Các lệnh gọi gRPC hiệu suất cao, có cân bằng tải loại bỏ nhu cầu proxy.