Nguon: Microsoft Learn · .NET 8.0

gRPC client-side load balancing (cân bằng tải phía client)

Nguồn: gRPC client-side load balancing

Bởi James Newton-King

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:

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:

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.

SchemeTypeMô tả
dnsDnsResolverFactoryGiải địa chỉ bằng cách truy vấn hostname cho các DNS address records.
staticStaticResolverFactoryGiả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 }):

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.

csharp
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:

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.

csharp
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:

csharp
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:

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ênTypeMô tả
pick_firstPickFirstLoadBalancerFactoryCố 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_robinRoundRobinLoadBalancerFactoryCố 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:

csharp
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:

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. httphttps 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.

csharp
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:

csharp
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:

Viết resolver và load balancer tùy chỉnh

Client-side load balancing có thể mở rộng:

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:

csharp
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:

Tạo load balancer tùy chỉnh

Một load balancer:

SubchannelsLoadBalancer là:

csharp
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:

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:

csharp
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ạ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.