我尝试将WebClient
与LoadBalancerExchangeFilterFunction
一起使用:
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
:
是否存在为什么存在这种差异的原因,以及如何使用WebClient
在每个实例上具有相同的RPS数量?提示可能是较旧的实例比新的实例收到更多的请求。
我尝试了一些调试,并调用了相同的(默认值为ZoneAwareLoadBalancer
之类的)逻辑。
答案 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