具有WebClient的功能区负载平衡器与其余模板不同(未正确平衡)

时间:2018-12-31 14:06:24

标签: load-balancing spring-cloud resttemplate spring-webflux spring-cloud-netflix

我尝试将WebClientLoadBalancerExchangeFilterFunction一起使用:

WebClient配置:

@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 :(得分:2)

您必须配置 Ribbon 配置以修改负载平衡行为(请在下面阅读)。

默认情况下(您已经发现自己),正在使用ZoneAwareLoadBalancer。在source code for 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 {
  //...
}

希望这会有所帮助!

答案 1 :(得分:2)

我做了简单的POC,并且所有操作都与Web客户端和用于默认配置的rest模板完全相同。

其他服务器代码:

@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 中添加:

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!!)
    }
}

application.yaml 中添加:

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')

您可以在服务器的两个或多个实例中尝试使用它,并且与Web客户端和rest模板完全相同。

默认情况下使用zoneAwareLoadBalancer的功能区,如果只有一个区域,则服务器的所有实例都将在“未知”区域中注册。

您可能无法通过Web客户端保持连接。 Web客户端在多个请求中重用同一连接,其余模板则不这样做。如果您的客户端和服务器之间有某种代理,那么您可能会遇到Web客户端重用连接的问题。要验证它,您可以像这样修改Web客户端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负载均衡器进行,并且(默认情况下将使用轮询)路由到该服务的某个Pod