Балансировщик нагрузки ленты с веб-клиентом отличается от шаблона первого покоя (неправильно сбалансирован) - PullRequest
0 голосов
/ 31 декабря 2018

Я пытался использовать WebClient с LoadBalancerExchangeFilterFunction:

WebClient config:

@Bean
public WebClient myWebClient(final LoadBalancerExchangeFilterFunction lbFunction) {
    return WebClient.builder()
            .filter(lbFunction)
            .defaultHeader(ACCEPT, APPLICATION_JSON_VALUE)
            .defaultHeader(CONTENT_ENCODING, APPLICATION_JSON_VALUE)
            .build();
} 

Тогда я заметил, что вызовы базовой службы не выполняются должным образомбалансировка нагрузки - существует постоянная разница RPS для каждого экземпляра.

Затем я попытался вернуться к RestTemplate.И все работает нормально.

Конфигурация для RestTemplate:

private static final int CONNECT_TIMEOUT_MILLIS = 18 * DateTimeConstants.MILLIS_PER_SECOND;
private static final int READ_TIMEOUT_MILLIS = 18 * DateTimeConstants.MILLIS_PER_SECOND;

@LoadBalanced
@Bean
public RestTemplate restTemplateSearch(final RestTemplateBuilder restTemplateBuilder) {
    return restTemplateBuilder
            .errorHandler(errorHandlerSearch())
            .requestFactory(this::bufferedClientHttpRequestFactory)
            .build();
}

private ClientHttpRequestFactory bufferedClientHttpRequestFactory() {
    final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
    requestFactory.setConnectTimeout(CONNECT_TIMEOUT_MILLIS);
    requestFactory.setReadTimeout(READ_TIMEOUT_MILLIS);
    return new BufferingClientHttpRequestFactory(requestFactory);
}

private ResponseErrorHandler errorHandlerSearch() {
    return new DefaultResponseErrorHandler() {
        @Override
        public boolean hasError(ClientHttpResponse response) throws IOException {
            return response.getStatusCode().is5xxServerError();
        }
    };
}

Балансировка нагрузки с использованием конфигурации WebClient до 11:25, затем переключение обратно на RestTemplate:

web-client-vs-rest-template-load-balancing

Есть ли причина, почему существует такая разница, и как я могу использовать WebClient, чтобы иметь одинаковое количество RPS в каждом экземпляре?Подсказка может заключаться в том, что старые экземпляры получают больше запросов, чем новых.

Я попробовал немного отладки, и вызывается такая же (по умолчанию, как ZoneAwareLoadBalancer) логика.

Ответы [ 2 ]

0 голосов
/ 10 января 2019

Я сделал простой POC, и все работает точно так же с веб-клиентом и шаблоном отдыха для конфигурации по умолчанию.

Код оставшегося сервера:

@SpringBootApplication
internal class RestServerApplication

fun main(args: Array<String>) {
    runApplication<RestServerApplication>(*args)
}

class BeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
    override fun initialize(context: GenericApplicationContext) {
        serverBeans().initialize(context)
    }
}

fun serverBeans() = beans {
    bean("serverRoutes") {
        PingRoutes(ref()).router()
    }
    bean<PingHandler>()
}

internal class PingRoutes(private val pingHandler: PingHandler) {
    fun router() = router {
        GET("/api/ping", pingHandler::ping)
    }
}

class PingHandler(private val env: Environment) {
    fun ping(serverRequest: ServerRequest): Mono<ServerResponse> {
        return Mono
            .fromCallable {
                // sleap added to simulate some work
                Thread.sleep(2000)
            }
            .subscribeOn(elastic())
            .flatMap {
                ServerResponse.ok()
                    .syncBody("pong-${env["HOSTNAME"]}-${env["server.port"]}")
            }
    }
}

В application.yaml add:

context.initializer.classes: com.lbpoc.server.BeansInitializer

Зависимости в gradle:

implementation('org.springframework.boot:spring-boot-starter-webflux')

Остальной код клиента:

@SpringBootApplication
internal class RestClientApplication {
    @Bean
    @LoadBalanced
    fun webClientBuilder(): WebClient.Builder {
        return WebClient.builder()
    }

    @Bean
    @LoadBalanced
    fun restTemplate() = RestTemplateBuilder().build()
}

fun main(args: Array<String>) {
    runApplication<RestClientApplication>(*args)
}

class BeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
    override fun initialize(context: GenericApplicationContext) {
        clientBeans().initialize(context)
    }
}

fun clientBeans() = beans {
    bean("clientRoutes") {
        PingRoutes(ref()).router()
    }
    bean<PingHandlerWithWebClient>()
    bean<PingHandlerWithRestTemplate>()
}

internal class PingRoutes(private val pingHandlerWithWebClient: PingHandlerWithWebClient) {
    fun router() = org.springframework.web.reactive.function.server.router {
        GET("/api/ping", pingHandlerWithWebClient::ping)
    }
}

class PingHandlerWithWebClient(private val webClientBuilder: WebClient.Builder) {
    fun ping(serverRequest: ServerRequest) = webClientBuilder.build()
        .get()
        .uri("http://rest-server-poc/api/ping")
        .retrieve()
        .bodyToMono(String::class.java)
        .onErrorReturn(TimeoutException::class.java, "Read/write timeout")
        .flatMap {
            ServerResponse.ok().syncBody(it)
        }
}

class PingHandlerWithRestTemplate(private val restTemplate: RestTemplate) {
    fun ping(serverRequest: ServerRequest) = Mono.fromCallable {
        restTemplate.getForEntity("http://rest-server-poc/api/ping", String::class.java)
    }.flatMap {
        ServerResponse.ok().syncBody(it.body!!)
    }
}

In application.yaml add:

context.initializer.classes: com.lbpoc.client.BeansInitializer
spring:
  application:
    name: rest-client-poc-for-load-balancing
logging:
  level.org.springframework.cloud: DEBUG
  level.com.netflix.loadbalancer: DEBUG
rest-server-poc:
  listOfServers: localhost:8081,localhost:8082

Зависимости в gradle:

implementation('org.springframework.boot:spring-boot-starter-webflux')
implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')

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

По умолчанию используется лента zoneAwareLoadBalancer, и если у вас есть только одна зона, все экземпляры для сервера будут зарегистрированы в «неизвестной» зоне.

Возможно, у вас возникла проблема с сохранением соединений через веб-клиент.Веб-клиент повторно использует одно и то же соединение в нескольких запросах, остальные шаблоны этого не делают.Если у вас есть какой-то прокси между вашим клиентом и сервером, у вас могут возникнуть проблемы с повторным использованием соединений через веб-клиент.Чтобы проверить это, вы можете изменить bean-компонент веб-клиента следующим образом и запустить тесты:

@Bean
@LoadBalanced
fun webClientBuilder(): WebClient.Builder {
    return WebClient.builder()
        .clientConnector(ReactorClientHttpConnector { options ->
            options
                .compression(true)
                .afterNettyContextInit { ctx ->
                    ctx.markPersistent(false)
                }
        })
}

Конечно, это не хорошее решение для производства, но, выполнив его, вы можете проверить, есть ли у вас проблемы с конфигурацией внутри вашего клиентского приложения.или, возможно, проблема снаружи, что-то между вашим клиентом и сервером.Например, если вы используете kubernetes и регистрируете свои сервисы в обнаружении сервисов, используя IP-адрес узла сервера, то каждый вызов такого сервиса будет проходить через балансировщик нагрузки kube-proxy и будет (по умолчанию используется циклический перебор) направляться в какой-то модуль.сервис.

0 голосов
/ 09 января 2019

Вам необходимо настроить Лента config для изменения режима балансировки нагрузки (см. Ниже).

По умолчанию (который вы нашли сами) используется ZoneAwareLoadBalancer.В исходном коде для ZoneAwareLoadBalancer мы читаем:
(, выделенный мной, - это некоторые механизмы, которые могут привести к шаблону RPS, который вы видите ):

Ключевым показателем, используемым для измерения состояния зоны, является Среднее число активных запросов , которое агрегируется для каждого остального клиента на зону.Это общее количество невыполненных запросов в зоне, деленное на количество доступных целевых экземпляров (исключая отключенные выключатели).Этот показатель очень эффективен, когда таймаут происходит медленно в плохой зоне.

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

Если ваш трафик обслуживается одной зоной(возможно, та же самая коробка?), тогда вы можете попасть в некоторые дополнительные запутанные ситуации.

Обратите также внимание, что без использования LoadBallancedFilterFunction среднее значение RPS такое же , как и при его использовании (на графике все линии сходятся к средней линии) после изменения, поэтому в глобальном масштабеобе стратегии балансировки нагрузки потребляют одинаковое количество доступной полосы пропускания, но по-разному.

Чтобы изменить настройки клиента ленты, попробуйте следующее:

public class RibbonConfig {

  @Autowired
  IClientConfig ribbonClientConfig;

  @Bean
  public IPing ribbonPing (IClientConfig config) {
    return new PingUrl();//default is a NoOpPing
  }

  @Bean
  public IRule ribbonRule(IClientConfig config) {
    return new AvailabilityFilteringRule(); // here override the default ZoneAvoidanceRule
  }

}

Тогда не забудьте глобальноопределите конфигурацию клиента ленты:

@SpringBootApplication
@RibbonClient(name = "app", configuration = RibbonConfig.class)
public class App {
  //...
}

Надеюсь, это поможет!

...