Когда растет количество запросов на сервис, так что один сервер перестает справляться, возникает необходимость в горизонтальном масштабировании сервиса на нескольких серверах, и настройки балансировки нагрузки, который будет распределять запросы между ними.

Сегодня мы реализуем различные алгоритмы балансировки нагрузок в .NET, и обсудим преимущества и недостатки каждого из них.


Round Robin (Циклический перебор)

Самый простой способ сбалансировать нагрузку на сервер. Запросы распределяются по циклу, во все сервера из списка конфигурации.

Начнем с простого примера, 1 сервер с пропускной способностью 1000 RPS

В случае, когда количество запросов на сервис мал, достаточна 1 экземпляра сервиса для работоспособности системы.

Допустим, наш сервис берет слишком много ресурсов сервера. Для демонстрации этого ограничил пропускную способность до 5 RPS.

{
  "UpstreamPathTemplate": "/Limited/Service/Request",
  "UpstreamHttpMethod": [ "Get" ],
  "DownstreamPathTemplate": "/api/Service/Request",
  "DownstreamScheme": "http",
  "DownstreamHostAndPorts": [
    {
      "Host": "ratelimiting",
      "Port": 80
    }
  ],
  "RateLimitOptions": {
    "EnableRateLimiting": true,
    "Period": "1s",
    "PeriodTimespan": 2,
    "Limit": 5
  }
}

Тут шлюз Ocelot за Period 1s ограничивает все запросы на конечную точку до пяти запросов (Limit)

Часть запросов на сервер дают ошибку 429 (Слишком много запросов)

Мы можем добавить несколько серверов чтобы балансировщик распределял запросы между ними

Реализация:

Hidden text

Создаем ноду сервиса.

Создайте Web API проект, не забудьте поставить галочку "Добавить поддержку Docker".

 Создайте ServiceController:

[Route("api/[controller]")]
[ApiController]
[EnableRateLimiting("fixed")]
public class ServiceController : ControllerBase
{
    [HttpGet("Request")]
    public async Task<IActionResult> Request()
    {
        string request = HttpContext.Request.GetDisplayUrl();
        return Ok($"Success_from_{request}");
    }
}

В Program добавьте RateLimiter из пространства имен Microsoft.AspNetCore.RateLimiting.

...
builder.Services.AddRateLimiter(rateLimiterOptions =>
{
   rateLimiterOptions.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
   rateLimiterOptions.AddFixedWindowLimiter("fixed", options =>
   {
      options.PermitLimit = int.Parse(builder.Configuration["RateLimit"]!);
      options.Window = TimeSpan.FromSeconds(1);
   });
});
...
app.UseRateLimiter();
...

В этом случае мы ограничиваем пропускную способность не на стороне шлюза Ocelot, а в самом сервисе инструментами .NET. Это удобно в случае, когда для разных экземпляров сервиса, в зависимости от мощности машины, нужны разные конфигурации Limit-а

Создаем шлюз Ocelot

Создайте Web API проект

Добавляем nuget пакет Ocelot:

dotnet add package Ocelot

добавляем Ocelot в Program

...
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration);
...
await app.UseOcelot();
...

Создаем конфигурацию ocelot.json

{
  "UpstreamPathTemplate": "/LimitedRoundRobin/Service/Request",
  "UpstreamHttpMethod": [ "Get" ],
  "DownstreamPathTemplate": "/api/Service/Request",
  "DownstreamScheme": "http",
  "DownstreamHostAndPorts": [
    {
      "Host": "limitedroundrobinnode1",
      "Port": 80
    },
    {
      "Host": "limitedroundrobinnode2",
      "Port": 80
    },
    {
      "Host": "limitedroundrobinnode3",
      "Port": 80
    }
  ],
  "LoadBalancerOptions": {
    "Type": "RoundRobin"
  }
}

В демонстрации мы видим, 3 сервера (слабый, средний, мощный). Часть запросов не обрабатываются из-за rate limiter middleware.

Мы можем это обойти, с помощью алгоритма Weighted Round Robin. Идея заключается в том, что для каждого сервера задается вес, и серверам с большим весом направляется больше запросов. Но проблема в том что, Ocelot не предоставляет встроенной поддержки weighted round robin, как это делает, например, Nginx. Я обошел это так:

Hidden text

{
"UpstreamPathTemplate": "/WeightedRoundRobin/Service/Request",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/Service/Request",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "limitedroundrobinnode1",
"Port": 80
},
{
"Host": "limitedroundrobinnode2",
"Port": 80
},
{
"Host": "limitedroundrobinnode2",
"Port": 80
},
{
"Host": "limitedroundrobinnode3",
"Port": 80
},
{
"Host": "limitedroundrobinnode3",
"Port": 80
},
{
"Host": "limitedroundrobinnode3",
"Port": 80
}
],
"LoadBalancerOptions": {
"Type": "RoundRobin"
}
}

Мы видим, что слабый сервер обрабатывает 1 запрос за цикл, средний сервер 2, сильный сервер 3

Нужно отметить, что это демонстрация в идеальных условиях. В реальности, мощность серверов не постоянное значение, и было бы неплохо реализовать алгоритм, который меняет вес динамически.

Least connections

Шлюз распределяет новые подключение к серверу с наименьшим количеством активных соединений.

В ocelot для реализации алгоритма просто нужно указать Type LeastConnection в LoadBalancerOptions

"LoadBalancerOptions": {
  "Type": "LeastConnection"
}

Можно еще написать свой собственный Balancer, оптимизируя алгоритм Least connections, задав вес для серверов так, чтобы слишком слабым серверам не направлять запросы вообще. Ocelot позволяет нам это делать

Все примеры из этой статьи я выложил на своем сайте, можете пройти по ссылке http://sandbox.codewithaman.net/LoadBalancerDemonstration и потыкать самим и сравнить

В Ocelot можно настроить кеширование на стороне шлюза (не на стороне сервиса) для конкретной конечной точки

"FileCacheOptions": {
    "TtlSeconds": 10
  }

Это значит, для API запрос кэшируется в течении 10 секунд, после чего кэш сбрасывается

Кроме того Ocelot позволяет настраивать обнаружение сервисов, с помощью которого ваши алгоритмы будут следить за работоспособностью ваших серверов

Комментарии (11)


  1. Evengard
    19.12.2023 14:09

    Странно, а запросы на который выдан код 429 не ретраятся на других инстансах автоматически?


    1. Amangeldi Автор
      19.12.2023 14:09

      Да, если вы настроили Quality of Service

      "QoSOptions": {
              "ExceptionsAllowedBeforeBreaking": 0,
              "DurationOfBreak": 10000,
              "TimeoutValue": 2000
            }

      QoSOptions - опция качества обслуживания, которые включают в себя настройки, позволяющие определить, сколько исключений допускается перед переключением в режим "паузы" (break) и как долго будет длиться эта пауза.

      Если вам нужна более сложная логика обработки ошибок, включая перенаправление в зависимости от конкретных статусов кодов ошибок, вам может потребоваться реализовать собственный middleware


  1. kostyakinderofficial
    19.12.2023 14:09

    Как Ocelot реализует Least Connection? Откуда он знает, сколько запросов обрабатывает сервис?


    1. raptor
      19.12.2023 14:09

      Скорее всего содержит в себе словарик со счетчиками. И считает нагрузку от себя на другие сервисы.


    1. Amangeldi Автор
      19.12.2023 14:09

      Вот их реализация: Ссылка

      Обрати внимание на метод Release. Он должен срабатывать каждый раз, когда сервис возвращает ответ шлюзу

      Тут их тесты (документация для разработчиков), по ним вы сможете понять как это работает


  1. Klass
    19.12.2023 14:09

    а где вы такие анимации создавали?


    1. Amangeldi Автор
      19.12.2023 14:09

      Adobe After Effects


  1. sdramare
    19.12.2023 14:09

    А зачем это нужно если есть Nginx?


    1. Amangeldi Автор
      19.12.2023 14:09

      .NET разработчики, знакомые с экосистемой .NET, могут легко освоить и начать использовать Ocelot без необходимости изучения сложных конфигураций Nginx

      Настройка авторизации между шлюзом и сервисами .NET разработчику будет гораздо легче с Ocelot

      Плюс, не думаю что сильно распространена практика использования nginx в windows серверах

      Нужно смотреть от конкретных требований и опыта команды. Если для ваших задач использование Nginx обоснована, используйте Nginx


      1. E1ektr0
        19.12.2023 14:09

        .net и windows, brrrrr.

        Docker, Linux, nginx


      1. sdramare
        19.12.2023 14:09

        Понял вашу позицию, спасибо. Замечу только, что сейчас, в 2023 году, сама практика использования .net на windows серверах является не сильно распространеной.