loading...

SpringBoot2 Blocking Web vs Reactive Web

bufferings profile image Mitz Updated on ・4 min read

Hello, I'm Mitz. This is the first post on dev.to. Nice to meet you :)

Buzzwords into my ToolBox

As many of you've heard Microservices, Blockchain, etc, there're a variety of tech buzzwords and they come and go often. Therefore, it's important for us not to believe them without even checking, but to see for ourselves how they work then put them into our toolbox. That's why I tried using a part of Reactive Programming this time.

One of the benefit of Reactive Programming is that we can use machine resources effectively. For example, in case of a web application, a server can handle more requests than blocking style application with less threads.

Reactive

SpringBoot2 Reactive Web

SpringBoot2, which was released at the beginning of this month, has introduced "Reactive Web" feature. So I tried comparing "Spring Web" and "Spring Reactive Web". "Spring Web" is based on a conventional blocking style with Servlet, and "Spring Reactive Web" is a new style with reactive programming.

Spring Web Reactive
(Image from https://spring.io/ )

Demo Apps Architecture

I found a great article which compares SpringBoot1 and SpringBoot2:
Raw Performance Numbers - Spring Boot 2 Webflux vs. Spring Boot 1.
I did almost the same thing to compare SpringBoot2 Blocking Web vs Reactive Web. The architecture is like this:

Architecture

There're 3 apps:

  • delay-service
  • blocking-app
  • reactive-app

The source code is here:
https://github.com/bufferings/webflux-demo-201803

delay-service

The delay-service emulates an outside API with some latency. We can set the latency with a path parameter:

  @GetMapping("/{delayMillis}")
  public Mono<String> get(@PathVariable int delayMillis) {
    return Mono.just("OK")
        .delayElement(Duration.ofMillis(delayMillis));
  }

blocking-app

The blocking-app is a simple Spring Web app, which calls delay-service in a blocking manner and returns it with a blocking manner:

  private static final String DELAY_SERVICE_URL = "http://localhost:8080";

  private final RestTemplate client;

  public BlockingApp(RestTemplateBuilder builder) {
    client = builder.rootUri(DELAY_SERVICE_URL).build();
  }

  @GetMapping("/{delayMillis}")
  public String get(@PathVariable String delayMillis) {
    String result = client.getForObject("/" + delayMillis, String.class);
    return "Blocking:" + result;
  }

reactive-app

The reactive-app is a Spring Web Reactive app, which calls delay-service with a reactive client and returns Mono:

  private static final String DELAY_SERVICE_URL = "http://localhost:8080";

  private final WebClient client = WebClient.create(DELAY_SERVICE_URL);

  @GetMapping("/{delayMillis}")
  public Mono<String> get(@PathVariable String delayMillis) {
    return client.get()
        .uri("/" + delayMillis)
        .retrieve()
        .bodyToMono(String.class)
        .map(s -> "Reactive:" + s);
  }

Let's check the performance!

Start delay-service:

./gradlew -p apps/delay-service clean bootRun

curl -w "\n%{time_total}s\n" localhost:8080/1000
# returns "OK" after 1000ms

curl -w "\n%{time_total}s\n" localhost:8080/2000
# returns "OK" after 2000ms

Start blocking-app:

./gradlew -p apps/blocking-app clean bootRun

curl -w "\n%{time_total}s\n" localhost:8081/2000
# returns "Blocking:OK" after 2000ms

Start reactive-app:

./gradlew -p apps/reactive-app clean bootRun

curl -w "\n%{time_total}s\n" localhost:8082/2000
# returns "Reactive:OK" after 2000ms

Now, all three apps are running.

Load Test Scenario

I used Gatling(https://gatling.io/) for the load test.

The scenario is something like this "1000 users call the API 30 times with 1 to 2 sec intervals". I would like to set the latency of delay-service as 300ms.

  val myScenario = scenario("Webflux Demo").exec(
    repeat(30) {
      exec(
        http("request_1").get(targetUrl)
      ).pause(1 second, 2 seconds)
    }
  )
  setUp(myScenario.inject(rampUsers(simUsers).over(30 seconds)))

Gatling to blocking-app

./gradlew -p apps/load-test -DTARGET_URL=http://localhost:8081/300 \
    -DSIM_USERS=1000 gatlingRun

With VisualVM, we can see the worker threads count increase up to 200 which is the default maxThread value of the Tomcat.
blocking-app threads

Gatling to reactive-app

./gradlew -p apps/load-test -DTARGET_URL=http://localhost:8082/300 \
    -DSIM_USERS=1000 gatlingRun

It only use 4 threads to handle the request.
reactive-app threads

Load Test Result

Here's the result.

As you can see, for 1000 users both apps work nicely with around 300ms response time as we expected. But for 3000 & 6000 users, the 95 percentile of blocking-app becomes worse.

On the other hand, reactive-app keeps the good response speed around 400ms and it shows about 2000rps with my laptop(Core i7-7500U 2.7GHz/16GB RAM).

Interesting result!

blocking-app

with 1000 users:
blocking1000

with 3000 users:
blocking3000

with 6000 users:
blocking6000

reactive-app

with 1000 users:
reactive1000

with 3000 users:
reactive3000

with 6000 users:
reactive6000

What's left

Increasing Tomcat maxThreads

Since I decided to try with the default config, the Tomcat threads count reached to 200 which is the default value of maxThreads. Probably tuning maxThread would improve the blocking-app performance.

Separating the env

Since I tried this demo all in my laptop, all the apps affected each other regarding the resource usage. So separating each apps into several machine might show different result.

Conclusion

In conclusion, we could know how SpringBoot2 Reactive Web handles requests efficiently. But I recommend you to check it by yourself. Especially Reactive Web style programming requires Java engineer to change their mindset to some extent I think.

Anyway, it's so interesting and I'm feeling now SpringBoot2 Reactive Web is in my toolbox. Thank you!

Posted on Mar 31 '18 by:

bufferings profile

Mitz

@bufferings

I like Java, SpringBoot, Thymeleaf, Docker, Scrum, DDD and love my daughters.

Discussion

markdown guide