Балансировка нагрузки на стороне клиента gRPC

Автор: Джеймс Ньютон-Кинг (James Newton-King)

Балансировка нагрузки на стороне клиента — это функция, позволяющая клиентам gRPC оптимально распределять нагрузку между доступными серверами. В этой статье рассматривается настройка балансировки нагрузки на стороне клиента для создания масштабируемых высокопроизводительных приложений gRPC в .NET.

Для балансировки нагрузки на стороне клиента требуются следующие компоненты:

  • .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 настраивается путем получения IServiceProvider от внедрения зависимостей.

Настройка сопоставителя

Сопоставитель настраивается с помощью адреса, с которым создается канал. Сопоставитель указывается в схеме URI адреса.

Схема Тип Описание
dns DnsResolverFactory Разрешает адреса путем запроса имени узла для записей адресов DNS.
static StaticResolverFactory Разрешает адреса, указанные приложением. Рекомендуется, если приложению уже известны вызываемые адреса.

Канал не вызывает напрямую URI, соответствующий сопоставителю. Вместо этого создается соответствующий сопоставитель, который используется для разрешения адресов.

Например, при выполнении команды GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure }):

  • Схема dns сопоставляется с DnsResolverFactory. Для канала будет создан экземпляр сопоставителя DNS.
  • Сопоставитель создает запрос DNS для my-example-host и получает два результата: 127.0.0.100 и 127.0.0.101.
  • Подсистема балансировки нагрузки использует 127.0.0.100:80 и 127.0.0.101:80 для создания подключений и выполнения вызовов gRPC.

DnsResolverFactory

DnsResolverFactory создает сопоставитель, предназначенный для получения адресов из внешнего источника. Разрешение DNS обычно используется для балансировки нагрузки экземпляров pod со службами 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.
    • my-example-host — имя узла для разрешения.
    • В адресе нет порта, поэтому вызовы gRPC отправляются в порт 80. Это порт по умолчанию для незащищенных каналов. При необходимости можно указать порт после имени узла. Например, dns:///my-example-host:8080 настраивает вызовы gRPC, которые будут отправляться через порт 8080.
  • Не указывает подсистему балансировки нагрузки. Канал по умолчанию выбирает первую подсистему балансировки нагрузки.
  • Запускает вызов SayHellogRPC:
    • Сопоставитель 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:80 и localhost:81.
  • Регистрирует фабрику с помощью внедрения зависимостей (DI).
  • Настраивает созданный канал с помощью:
    • Адреса static:///my-example-host. Схема static сопоставляется со статическим распознавателем.
    • Задает GrpcChannelOptions.ServiceProvider с помощью поставщика службы внедрения зависимостей.

В этом примере создается объект ServiceCollection для внедрения зависимостей. Предположим, что в приложении уже настроено внедрение зависимостей, например веб-сайт ASP.NET Core. В этом случае типы должны быть зарегистрированы в существующем экземпляре внедрения зависимостей. GrpcChannelOptions.ServiceProvider настраивается путем получения IServiceProvider от внедрения зависимостей.

Настройка подсистемы балансировки нагрузки

Подсистема балансировки нагрузки указывается в service config с помощью коллекции ServiceConfig.LoadBalancingConfigs. Две подсистемы балансировки нагрузки являются встроенными и сопоставляются с именами конфигураций подсистемы балансировки:

Имя. Тип Описание
pick_first PickFirstLoadBalancerFactory Пытается подключиться к адресам до тех пор, пока подключение не будет успешно установлено. Все вызовы gRPC выполняются до первого успешного подключения.
round_robin RoundRobinLoadBalancerFactory Пытается подключиться ко всем адресам. Вызовы gRPC распределяются по всем успешным подключениям с помощью логики циклического перебора.

service config является сокращением для конфигурации службы, которое представлено типом ServiceConfig. Есть несколько способов, с помощью которых канал может получить конфигурацию службы с настроенной подсистемой балансировки нагрузки:

  • Приложение может указать конфигурацию службы, если канал создан с помощью GrpcChannelOptions.ServiceConfig.
  • Кроме того, сопоставитель может разрешить конфигурацию службы для канала. Эта функция позволяет внешнему источнику указать, каким образом вызывающие объекты должны выполнять балансировку нагрузки. Поддержка разрешения конфигурации службы сопоставителем зависит от его реализации. Отключить эту функцию можно с помощью GrpcChannelOptions.DisableResolverServiceConfig.
  • Если конфигурация службы не указана или для нее не настроена подсистема балансировки нагрузки, для канала по умолчанию задается значение 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" });

Предыдущий код:

  • Задает значение RoundRobinLoadBalancerFactory в конфигурации службы.
  • Запускает вызов SayHellogRPC:
    • DnsResolverFactory создает сопоставитель, который получает адреса для имени узла my-example-host.
    • Подсистема балансировки нагрузки с циклическим перебором пытается подключиться ко всем разрешенным адресам.
    • Вызовы gRPC распределяются равномерно с помощью логики циклического перебора.

Настройка учетных данных канала

Каналу необходимо знать, отправляются ли вызовы gRPC с использованием защиты транспорта. http и https больше не являются частью адреса. В схеме теперь указывается сопоставитель, поэтому при использовании балансировки нагрузки в параметрах канала необходимо настроить 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" });

Использование балансировки нагрузки с клиентской фабрикой 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();

Предыдущий код:

  • Настраивает клиент с адресом балансировки нагрузки.
  • Указывает учетные данные канала.
  • Регистрирует типы DI в приложении IServiceCollection.

Написание пользовательских сопоставителей и подсистем балансировки нагрузки

Балансировка нагрузки на стороне клиента является расширяемой.

  • Реализуйте 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 реализует PollingResolver. PollingResolver является абстрактным базовым типом, который упрощает реализацию сопоставителя с асинхронной логикой путем переопределения 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, который случайным образом выбирает подканал.

Настройка пользовательских сопоставителей и подсистем балансировки нагрузки

При использовании пользовательских сопоставителей и подсистем балансировки нагрузки их необходимо зарегистрировать с помощью внедрения зависимостей. Существует несколько вариантов настройки:

  • Если приложение уже использует внедрение зависимостей, например веб-приложение ASP.NET Core, их можно зарегистрировать с помощью существующей конфигурации внедрения зависимостей. 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.json. Схема file сопоставляется с FileResolverFactory.
    • Подсистема балансировки нагрузки service config имеет имя random. Карты в RandomLoadBalancerFactory.

Почему балансировка нагрузки важна

HTTP/2 приводит к мультиплексированию нескольких вызовов в одном TCP-подключении. Если gRPC и HTTP/2 используются с подсистемой балансировки сетевой нагрузки (NLB), подключение перенаправляется на сервер и все вызовы gRPC отправляются на этот сервер. Другие экземпляры сервера в службе балансировки сетевой нагрузки простаивают.

Подсистемы балансировки сетевой нагрузки представляют собой распространенное решение для распределения нагрузки, так как они являются быстрыми и простыми. Например, Kubernetes по умолчанию использует подсистему балансировки сетевой нагрузки для балансировки соединений между экземплярами pod. Однако подсистемы балансировки сетевой нагрузки не эффективны для распределения нагрузки при использовании с gRPC и HTTP/2.

Прокси-сервер или балансировка нагрузки на стороне клиента

Для gRPC и HTTP/2 можно эффективно использовать балансировку нагрузки с помощью прокси-сервера балансировки нагрузки приложений или балансировки нагрузки на стороне клиента. Оба этих варианта позволяют распределять отдельные вызовы gRPC между доступными серверами. Выбор между прокси-сервером и балансировкой нагрузки на стороне клиента зависит от архитектуры. В обоих случаях есть свои преимущества и недостатки.

  • Прокси-сервер. Вызовы gRPC отправляются на прокси-сервер, он принимает решение о балансировке нагрузки, а вызов gRPC отправляется в финальную конечную точку. Прокси-серверу должны быть известны конечные точки. При использовании прокси-сервера происходит следующее:

    • Добавляется дополнительный сетевой прыжок для вызовов gRPC.
    • Возникает задержка и потребляются дополнительные ресурсы.
    • Прокси-сервер должен быть установлен и настроен правильно.
  • Балансировка нагрузки на стороне клиента. Клиент gRPC принимает решение о балансировке нагрузки при запуске вызова gRPC. Вызов gRPC отправляется непосредственно в финальную конечную точку. При использовании балансировки нагрузки на стороне клиента происходит следующее:

    • Клиент должен быть осведомлен о доступных конечных точках и принимать решения о балансировке нагрузки.
    • В этом случае необходимо выполнить дополнительную настройку клиента.
    • Для высокопроизводительных вызовов gRPC с балансировкой нагрузки не требуется прокси-сервер.

Дополнительные ресурсы