Ribbon load balancer with webclient differs from rest template one (not properly balanced)












9















I've tried to use WebClient with 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();
}


Then I've noticed that calls to underlying service are not properly load balanced - there is constant difference of RPS on each instance.



Then I've tried to move back to RestTemplate. And it's working fine.



Config for 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();
}
};
}


Load balancing using WebClient config up to 11:25, then switching back to RestTemplate:



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



Is there a reason why there is such difference and how I can use WebClient to have same amount of RPS on each instance? Clue might be that older instances are getting more requests than new ones.



I've tried bit of debugging and same (defaults like ZoneAwareLoadBalancer) logic is being called.










share|improve this question



























    9















    I've tried to use WebClient with 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();
    }


    Then I've noticed that calls to underlying service are not properly load balanced - there is constant difference of RPS on each instance.



    Then I've tried to move back to RestTemplate. And it's working fine.



    Config for 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();
    }
    };
    }


    Load balancing using WebClient config up to 11:25, then switching back to RestTemplate:



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



    Is there a reason why there is such difference and how I can use WebClient to have same amount of RPS on each instance? Clue might be that older instances are getting more requests than new ones.



    I've tried bit of debugging and same (defaults like ZoneAwareLoadBalancer) logic is being called.










    share|improve this question

























      9












      9








      9


      1






      I've tried to use WebClient with 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();
      }


      Then I've noticed that calls to underlying service are not properly load balanced - there is constant difference of RPS on each instance.



      Then I've tried to move back to RestTemplate. And it's working fine.



      Config for 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();
      }
      };
      }


      Load balancing using WebClient config up to 11:25, then switching back to RestTemplate:



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



      Is there a reason why there is such difference and how I can use WebClient to have same amount of RPS on each instance? Clue might be that older instances are getting more requests than new ones.



      I've tried bit of debugging and same (defaults like ZoneAwareLoadBalancer) logic is being called.










      share|improve this question














      I've tried to use WebClient with 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();
      }


      Then I've noticed that calls to underlying service are not properly load balanced - there is constant difference of RPS on each instance.



      Then I've tried to move back to RestTemplate. And it's working fine.



      Config for 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();
      }
      };
      }


      Load balancing using WebClient config up to 11:25, then switching back to RestTemplate:



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



      Is there a reason why there is such difference and how I can use WebClient to have same amount of RPS on each instance? Clue might be that older instances are getting more requests than new ones.



      I've tried bit of debugging and same (defaults like ZoneAwareLoadBalancer) logic is being called.







      load-balancing spring-cloud resttemplate spring-webflux spring-cloud-netflix






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked Dec 31 '18 at 14:06









      pixelpixel

      9,8182188143




      9,8182188143
























          2 Answers
          2






          active

          oldest

          votes


















          2





          +50









          I did simple POC and everything works exactly the same with web client and rest template for default configuration.



          Rest server code:



          @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"]}")
          }
          }
          }


          In application.yaml add:



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


          Dependencies in gradle:



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


          Rest client code:



          @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


          Dependencies in gradle:



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


          You can try it with two or more instances for server and it works exactly the same with web client and rest template.



          Ribbon use by default zoneAwareLoadBalancer and if you have only one zone all instances for server will be registered in "unknown" zone.



          You might have a problem with keeping connections by web client. Web client reuse the same connection in multiple requests, rest template do not do that. If you have some kind of proxy between your client and server then you might have a problem with reusing connections by web client. To verify it you can modify web client bean like this and run tests:



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


          Of course it's not a good solution for production but doing that you can check if you have a problem with configuration inside your client application or maybe problem is outside, something between your client and server. E.g. if you are using kubernetes and register your services in service discovery using server node IP address then every call to such service will go though kube-proxy load balancer and will be (by default round robin will be used) routed to some pod for that service.






          share|improve this answer


























          • Maybe it will be also helpful to investigate how http headers differ from each other for both solutions ( RestTemplate vs WebClient ). Since I'm afk I won't be able to verify it until next 10 days :/

            – pixel
            Jan 10 at 13:14





















          2














          You have to configure Ribbon config to modify the load balancing behavior (please read below).



          By default (which you have found yourself) the ZoneAwareLoadBalancer is being used. In the source code for ZoneAwareLoadBalancer we read:

          (highlighted by me are some mechanics which could result in the RPS pattern you see):




          The key metric used to measure the zone condition is Average Active Requests, which is aggregated per rest client per zone. It is the
          total outstanding requests in a zone divided by number of available targeted instances (excluding circuit breaker tripped instances).
          This metric is very effective when timeout occurs slowly on a bad zone.



          The LoadBalancer will calculate and examine zone stats of all available zones. If the Average Active Requests for any zone has reached a configured threshold, this zone will be dropped from the active server list. In case more than one zone has reached the threshold, the zone with the most active requests per server will be dropped.
          Once the the worst zone is dropped, a zone will be chosen among the rest with the probability proportional to its number of instances.




          If your traffic is being served by one zone (perhaps the same box?) then you might get into some additionally confusing situations.



          Please also note that without using LoadBallancedFilterFunction the average RPS is the same as when you use it (on the graph all lines converge to the median line) after the change, so globally looking both load balancing strategies consume the same amount of available bandwidth but in a different manner.



          To modify your Ribbon client settings, try following:



          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
          }

          }


          Then don't forget to globally define your Ribbon client config:



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


          Hope this helps!






          share|improve this answer
























          • The problem here is that RestTemplate is using the sameZoneAwareLoadBalancer

            – pixel
            Jan 9 at 14:13











          • did you try adding @RibbonClient(name="lb", configuration = RibbonConfig.class) annotation on your class where the @LoadBallanced @Bean RestTemplate restTemplate() {//...} is?

            – diginoise
            Jan 9 at 15:00











          Your Answer






          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "1"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: true,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: 10,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53988362%2fribbon-load-balancer-with-webclient-differs-from-rest-template-one-not-properly%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          2 Answers
          2






          active

          oldest

          votes








          2 Answers
          2






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes









          2





          +50









          I did simple POC and everything works exactly the same with web client and rest template for default configuration.



          Rest server code:



          @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"]}")
          }
          }
          }


          In application.yaml add:



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


          Dependencies in gradle:



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


          Rest client code:



          @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


          Dependencies in gradle:



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


          You can try it with two or more instances for server and it works exactly the same with web client and rest template.



          Ribbon use by default zoneAwareLoadBalancer and if you have only one zone all instances for server will be registered in "unknown" zone.



          You might have a problem with keeping connections by web client. Web client reuse the same connection in multiple requests, rest template do not do that. If you have some kind of proxy between your client and server then you might have a problem with reusing connections by web client. To verify it you can modify web client bean like this and run tests:



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


          Of course it's not a good solution for production but doing that you can check if you have a problem with configuration inside your client application or maybe problem is outside, something between your client and server. E.g. if you are using kubernetes and register your services in service discovery using server node IP address then every call to such service will go though kube-proxy load balancer and will be (by default round robin will be used) routed to some pod for that service.






          share|improve this answer


























          • Maybe it will be also helpful to investigate how http headers differ from each other for both solutions ( RestTemplate vs WebClient ). Since I'm afk I won't be able to verify it until next 10 days :/

            – pixel
            Jan 10 at 13:14


















          2





          +50









          I did simple POC and everything works exactly the same with web client and rest template for default configuration.



          Rest server code:



          @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"]}")
          }
          }
          }


          In application.yaml add:



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


          Dependencies in gradle:



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


          Rest client code:



          @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


          Dependencies in gradle:



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


          You can try it with two or more instances for server and it works exactly the same with web client and rest template.



          Ribbon use by default zoneAwareLoadBalancer and if you have only one zone all instances for server will be registered in "unknown" zone.



          You might have a problem with keeping connections by web client. Web client reuse the same connection in multiple requests, rest template do not do that. If you have some kind of proxy between your client and server then you might have a problem with reusing connections by web client. To verify it you can modify web client bean like this and run tests:



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


          Of course it's not a good solution for production but doing that you can check if you have a problem with configuration inside your client application or maybe problem is outside, something between your client and server. E.g. if you are using kubernetes and register your services in service discovery using server node IP address then every call to such service will go though kube-proxy load balancer and will be (by default round robin will be used) routed to some pod for that service.






          share|improve this answer


























          • Maybe it will be also helpful to investigate how http headers differ from each other for both solutions ( RestTemplate vs WebClient ). Since I'm afk I won't be able to verify it until next 10 days :/

            – pixel
            Jan 10 at 13:14
















          2





          +50







          2





          +50



          2




          +50





          I did simple POC and everything works exactly the same with web client and rest template for default configuration.



          Rest server code:



          @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"]}")
          }
          }
          }


          In application.yaml add:



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


          Dependencies in gradle:



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


          Rest client code:



          @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


          Dependencies in gradle:



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


          You can try it with two or more instances for server and it works exactly the same with web client and rest template.



          Ribbon use by default zoneAwareLoadBalancer and if you have only one zone all instances for server will be registered in "unknown" zone.



          You might have a problem with keeping connections by web client. Web client reuse the same connection in multiple requests, rest template do not do that. If you have some kind of proxy between your client and server then you might have a problem with reusing connections by web client. To verify it you can modify web client bean like this and run tests:



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


          Of course it's not a good solution for production but doing that you can check if you have a problem with configuration inside your client application or maybe problem is outside, something between your client and server. E.g. if you are using kubernetes and register your services in service discovery using server node IP address then every call to such service will go though kube-proxy load balancer and will be (by default round robin will be used) routed to some pod for that service.






          share|improve this answer















          I did simple POC and everything works exactly the same with web client and rest template for default configuration.



          Rest server code:



          @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"]}")
          }
          }
          }


          In application.yaml add:



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


          Dependencies in gradle:



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


          Rest client code:



          @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


          Dependencies in gradle:



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


          You can try it with two or more instances for server and it works exactly the same with web client and rest template.



          Ribbon use by default zoneAwareLoadBalancer and if you have only one zone all instances for server will be registered in "unknown" zone.



          You might have a problem with keeping connections by web client. Web client reuse the same connection in multiple requests, rest template do not do that. If you have some kind of proxy between your client and server then you might have a problem with reusing connections by web client. To verify it you can modify web client bean like this and run tests:



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


          Of course it's not a good solution for production but doing that you can check if you have a problem with configuration inside your client application or maybe problem is outside, something between your client and server. E.g. if you are using kubernetes and register your services in service discovery using server node IP address then every call to such service will go though kube-proxy load balancer and will be (by default round robin will be used) routed to some pod for that service.







          share|improve this answer














          share|improve this answer



          share|improve this answer








          edited Jan 10 at 11:00

























          answered Jan 10 at 10:44









          Jarosław BelaJarosław Bela

          862




          862













          • Maybe it will be also helpful to investigate how http headers differ from each other for both solutions ( RestTemplate vs WebClient ). Since I'm afk I won't be able to verify it until next 10 days :/

            – pixel
            Jan 10 at 13:14





















          • Maybe it will be also helpful to investigate how http headers differ from each other for both solutions ( RestTemplate vs WebClient ). Since I'm afk I won't be able to verify it until next 10 days :/

            – pixel
            Jan 10 at 13:14



















          Maybe it will be also helpful to investigate how http headers differ from each other for both solutions ( RestTemplate vs WebClient ). Since I'm afk I won't be able to verify it until next 10 days :/

          – pixel
          Jan 10 at 13:14







          Maybe it will be also helpful to investigate how http headers differ from each other for both solutions ( RestTemplate vs WebClient ). Since I'm afk I won't be able to verify it until next 10 days :/

          – pixel
          Jan 10 at 13:14















          2














          You have to configure Ribbon config to modify the load balancing behavior (please read below).



          By default (which you have found yourself) the ZoneAwareLoadBalancer is being used. In the source code for ZoneAwareLoadBalancer we read:

          (highlighted by me are some mechanics which could result in the RPS pattern you see):




          The key metric used to measure the zone condition is Average Active Requests, which is aggregated per rest client per zone. It is the
          total outstanding requests in a zone divided by number of available targeted instances (excluding circuit breaker tripped instances).
          This metric is very effective when timeout occurs slowly on a bad zone.



          The LoadBalancer will calculate and examine zone stats of all available zones. If the Average Active Requests for any zone has reached a configured threshold, this zone will be dropped from the active server list. In case more than one zone has reached the threshold, the zone with the most active requests per server will be dropped.
          Once the the worst zone is dropped, a zone will be chosen among the rest with the probability proportional to its number of instances.




          If your traffic is being served by one zone (perhaps the same box?) then you might get into some additionally confusing situations.



          Please also note that without using LoadBallancedFilterFunction the average RPS is the same as when you use it (on the graph all lines converge to the median line) after the change, so globally looking both load balancing strategies consume the same amount of available bandwidth but in a different manner.



          To modify your Ribbon client settings, try following:



          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
          }

          }


          Then don't forget to globally define your Ribbon client config:



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


          Hope this helps!






          share|improve this answer
























          • The problem here is that RestTemplate is using the sameZoneAwareLoadBalancer

            – pixel
            Jan 9 at 14:13











          • did you try adding @RibbonClient(name="lb", configuration = RibbonConfig.class) annotation on your class where the @LoadBallanced @Bean RestTemplate restTemplate() {//...} is?

            – diginoise
            Jan 9 at 15:00
















          2














          You have to configure Ribbon config to modify the load balancing behavior (please read below).



          By default (which you have found yourself) the ZoneAwareLoadBalancer is being used. In the source code for ZoneAwareLoadBalancer we read:

          (highlighted by me are some mechanics which could result in the RPS pattern you see):




          The key metric used to measure the zone condition is Average Active Requests, which is aggregated per rest client per zone. It is the
          total outstanding requests in a zone divided by number of available targeted instances (excluding circuit breaker tripped instances).
          This metric is very effective when timeout occurs slowly on a bad zone.



          The LoadBalancer will calculate and examine zone stats of all available zones. If the Average Active Requests for any zone has reached a configured threshold, this zone will be dropped from the active server list. In case more than one zone has reached the threshold, the zone with the most active requests per server will be dropped.
          Once the the worst zone is dropped, a zone will be chosen among the rest with the probability proportional to its number of instances.




          If your traffic is being served by one zone (perhaps the same box?) then you might get into some additionally confusing situations.



          Please also note that without using LoadBallancedFilterFunction the average RPS is the same as when you use it (on the graph all lines converge to the median line) after the change, so globally looking both load balancing strategies consume the same amount of available bandwidth but in a different manner.



          To modify your Ribbon client settings, try following:



          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
          }

          }


          Then don't forget to globally define your Ribbon client config:



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


          Hope this helps!






          share|improve this answer
























          • The problem here is that RestTemplate is using the sameZoneAwareLoadBalancer

            – pixel
            Jan 9 at 14:13











          • did you try adding @RibbonClient(name="lb", configuration = RibbonConfig.class) annotation on your class where the @LoadBallanced @Bean RestTemplate restTemplate() {//...} is?

            – diginoise
            Jan 9 at 15:00














          2












          2








          2







          You have to configure Ribbon config to modify the load balancing behavior (please read below).



          By default (which you have found yourself) the ZoneAwareLoadBalancer is being used. In the source code for ZoneAwareLoadBalancer we read:

          (highlighted by me are some mechanics which could result in the RPS pattern you see):




          The key metric used to measure the zone condition is Average Active Requests, which is aggregated per rest client per zone. It is the
          total outstanding requests in a zone divided by number of available targeted instances (excluding circuit breaker tripped instances).
          This metric is very effective when timeout occurs slowly on a bad zone.



          The LoadBalancer will calculate and examine zone stats of all available zones. If the Average Active Requests for any zone has reached a configured threshold, this zone will be dropped from the active server list. In case more than one zone has reached the threshold, the zone with the most active requests per server will be dropped.
          Once the the worst zone is dropped, a zone will be chosen among the rest with the probability proportional to its number of instances.




          If your traffic is being served by one zone (perhaps the same box?) then you might get into some additionally confusing situations.



          Please also note that without using LoadBallancedFilterFunction the average RPS is the same as when you use it (on the graph all lines converge to the median line) after the change, so globally looking both load balancing strategies consume the same amount of available bandwidth but in a different manner.



          To modify your Ribbon client settings, try following:



          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
          }

          }


          Then don't forget to globally define your Ribbon client config:



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


          Hope this helps!






          share|improve this answer













          You have to configure Ribbon config to modify the load balancing behavior (please read below).



          By default (which you have found yourself) the ZoneAwareLoadBalancer is being used. In the source code for ZoneAwareLoadBalancer we read:

          (highlighted by me are some mechanics which could result in the RPS pattern you see):




          The key metric used to measure the zone condition is Average Active Requests, which is aggregated per rest client per zone. It is the
          total outstanding requests in a zone divided by number of available targeted instances (excluding circuit breaker tripped instances).
          This metric is very effective when timeout occurs slowly on a bad zone.



          The LoadBalancer will calculate and examine zone stats of all available zones. If the Average Active Requests for any zone has reached a configured threshold, this zone will be dropped from the active server list. In case more than one zone has reached the threshold, the zone with the most active requests per server will be dropped.
          Once the the worst zone is dropped, a zone will be chosen among the rest with the probability proportional to its number of instances.




          If your traffic is being served by one zone (perhaps the same box?) then you might get into some additionally confusing situations.



          Please also note that without using LoadBallancedFilterFunction the average RPS is the same as when you use it (on the graph all lines converge to the median line) after the change, so globally looking both load balancing strategies consume the same amount of available bandwidth but in a different manner.



          To modify your Ribbon client settings, try following:



          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
          }

          }


          Then don't forget to globally define your Ribbon client config:



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


          Hope this helps!







          share|improve this answer












          share|improve this answer



          share|improve this answer










          answered Jan 9 at 12:02









          diginoisediginoise

          4,64911627




          4,64911627













          • The problem here is that RestTemplate is using the sameZoneAwareLoadBalancer

            – pixel
            Jan 9 at 14:13











          • did you try adding @RibbonClient(name="lb", configuration = RibbonConfig.class) annotation on your class where the @LoadBallanced @Bean RestTemplate restTemplate() {//...} is?

            – diginoise
            Jan 9 at 15:00



















          • The problem here is that RestTemplate is using the sameZoneAwareLoadBalancer

            – pixel
            Jan 9 at 14:13











          • did you try adding @RibbonClient(name="lb", configuration = RibbonConfig.class) annotation on your class where the @LoadBallanced @Bean RestTemplate restTemplate() {//...} is?

            – diginoise
            Jan 9 at 15:00

















          The problem here is that RestTemplate is using the sameZoneAwareLoadBalancer

          – pixel
          Jan 9 at 14:13





          The problem here is that RestTemplate is using the sameZoneAwareLoadBalancer

          – pixel
          Jan 9 at 14:13













          did you try adding @RibbonClient(name="lb", configuration = RibbonConfig.class) annotation on your class where the @LoadBallanced @Bean RestTemplate restTemplate() {//...} is?

          – diginoise
          Jan 9 at 15:00





          did you try adding @RibbonClient(name="lb", configuration = RibbonConfig.class) annotation on your class where the @LoadBallanced @Bean RestTemplate restTemplate() {//...} is?

          – diginoise
          Jan 9 at 15:00


















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Stack Overflow!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53988362%2fribbon-load-balancer-with-webclient-differs-from-rest-template-one-not-properly%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Monofisismo

          Angular Downloading a file using contenturl with Basic Authentication

          Olmecas