-1

I made two apps with similar code on Spring Boot.

  1. Reactive Netty spring boot webflux r2dbc.
  2. Nonreactive Tomcat spring boot postgres.
I expect reactive one is faster or has the same speed. But it is slower 6th time. I don't see any blocks there. In my opinion it is fully nonblocking app. Why so slowly? For testing I have been using J-METER. 200 threads 200 times getAll 400 strings Latency of response nonreactive app - 190-240 ms. Latency of response reactive app - 1290-1350 ms.

reactive pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>reactive</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>reactive</name>
    <description>reactive</description>
    <properties>
        <java.version>1.8</java.version>
        <mapstruct.version>1.4.2.Final</mapstruct.version>
        <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>io.r2dbc</groupId>
            <artifactId>r2dbc-postgresql</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>
</project>

Spring boot entry point

@SpringBootApplication(exclude = {ReactiveSecurityAutoConfiguration.class})
@Configuration
@EnableR2dbcRepositories
public class ReactiveApplication {

    public static void main(String[] args) {
        SpringApplication.run(ReactiveApplication.class, args);
    }
    
}

application.yml

server:
  port : 8083
spring:
  data:
    r2dbc:
      repositories:
        enabled: true
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/reactive
    username: postgres
    password: 12345
    properties:
      schema: bookshop

logging:
  level:
    org:
      springframework:
        r2dbc: DEBUG

Controller

package com.example.reactive.controller;

import com.example.reactive.entity.Book;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RequestMapping("/book")
public interface BookController {

    @ResponseStatus(code = HttpStatus.OK)
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    Mono<Book> saveBook(@RequestBody Book book);

    @ResponseStatus(code = HttpStatus.OK)
    @GetMapping("/{id}")
    Mono<Book> getBookById(@PathVariable Long id);

    @ResponseStatus(code = HttpStatus.OK)
    @GetMapping("/all")
    Flux<Book> getAllBooks();
}

ControllerImpl

package com.example.reactive.controller.impl;

import com.example.reactive.controller.BookController;
import com.example.reactive.entity.Book;
import com.example.reactive.repository.BookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequiredArgsConstructor
public class BookControllerImpl implements BookController {

    private final BookRepository bookRepository;

    @Override
    public Mono<Book> saveBook(Book book) {
        return bookRepository.save(book);
    }

    @Override
    public Mono<Book> getBookById(Long id) {
        return bookRepository.findById(id);
    }

    @Override
    public Flux<Book> getAllBooks() {
        return bookRepository.findAll();
    }
}

Entity

package com.example.reactive.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Book {
    @Id
    private Long id;
    private String name;
    private String author;
    private String text;
}

Repository

package com.example.reactive.repository;

import com.example.reactive.entity.Book;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;


public interface BookRepository extends ReactiveCrudRepository<Book, Long> {
}

If you need more information, feel free to write comments.

EDITED

After adding thread-pool parameters of min value 50 and max value 100, reactive app become faster. I don't use get All property to compare apps. For testing I have been using J-METER.200 threads 200 times

get 1 different string

Latency of response nonreactive app - middle 6 ms. Latency of response reactive app - middle 10 ms.

post 1 string Latency of response nonreactive app - middle 37 ms. Latency of response reactive app - middle 6 ms!!!!!!!!

Computer parameters Processor Intel® Core™ i 5-10400 CPU @ 2.90 GHz × 12. RAM 16,0 GB.

2 Answers2

3

First, to answer in a more general sense, the reactive stack is - unfortunately - not a one-size-fits-all shortcut to make applications perform better. One of the main benefits of using WebFlux is its ability to handle more concurrent requests when compared to servlet.

Now to the problem at hand. I will assume that the latency values you have given are averages obtained from a fairly heavy load test.

One thing that comes to mind is that the first couple of requests served by a reactive application after its startup always take extra time - this can be attributed to Netty initialization. For this reason, in busy production environments, we have always had some form of automated warmup in place. Although I assume your load tests send so many requests that this should not be a factor.

For the above reason, I am inclined to believe that the cause of this slowdown could be simply down to excessive logging - setting the package org.springframework.r2dbc to DEBUG level logs every single SQL statement executed - and in the context of a load test, excessive logging wreaks havoc on performance and pollutes results.


Edit

For reference, I have built two similar apps with postgres based on your post. One servlet with JPA, and one reactive with R2DBC.

I have executed a 30-second Gatling load test on both, simulating 200 constant concurrent users. The actual request sent was either an insert or a get operation, both had equal weight when it came to choosing.

The load test in question:

public class ReactiveApplicationLoadTest extends Simulation {
    private static final String BASE_URL = "http://localhost:8083";
    private static final String BOOK_URI = "/book";

    HttpProtocolBuilder httpProtocol = http
        .baseUrl(BASE_URL);

    ScenarioBuilder scenario = scenario("Get All Books").randomSwitch()
        .on(
            Choice.withWeight(1, exec(
                http("GET_ONE")
                    .get(BOOK_URI + "/1")
            )),
            Choice.withWeight(1, exec(
                http("INSERT")
                    .post(BOOK_URI)
                    .header("Content-Type", "application/json")
                    .body(StringBody("{\"name\": \"book\", \"author\": \"author\", \"text\": \"text\"}"))
            ))
        );

    {
        setUp(
            scenario.injectClosed(constantConcurrentUsers(200).during(30)).protocols(httpProtocol)
        );
    }
}

My computer's relevant specifications:

  • CPU: intel i7 11700K
  • RAM: 32GB DDR4 @ 3600MHz

The source code of the two apps:

The results:

My results lean heavily in the favor of the reactive application, both in response times and the number of KO'd requests.

You might notice that I did not include a "get all" request in the load test. The reason for this is that the sheer amount of items created in the load test don't mix well with this broad query, especially in a reactive context - this might be the source of the results you are seeing.

Takeaway

Consider adding some form of pagination to your "get all" endpoints in both apps OR omit these calls from your load test, and then try again.

cpigeon
  • 175
  • 7
  • Thank you for answer. 1) Tests are 200 threads make 200 requests. So I think warming have a place there. 2) I have made my logging to OFF status- nothing changes. – Игорь Ходыко Oct 20 '22 at 06:03
  • 1
    @ИгорьХодыко I have edited my answer - I performed similar load tests on similar apps. – cpigeon Oct 20 '22 at 15:32
  • I see your result and it looks good. I was checking post and get of one item. And it doesn't work for my application. Reactive app has the same result for post, but worst for get. – Игорь Ходыко Oct 23 '22 at 08:52
  • 1
    @ИгорьХодыко Can't do much more without more info - your hardware, load test etc. What I can do is share my own source code for the two apps, which I've now included in the answer. – cpigeon Oct 23 '22 at 10:22
  • I have some changes. Declaring of thread-pool improved situation by two times. But reactive app is worse yet. – Игорь Ходыко Oct 23 '22 at 12:40
  • I edit the question. add computer params and some changes. Do you think result is normal? Can you check result of post and get separatly on your apps? – Игорь Ходыко Oct 24 '22 at 06:11
  • @ИгорьХодыко Those results are looking good! I would say that individual requests to a reactive app have a tiny bit more overhead than to a servlet app, but as you increase concurrent requests, the reactive app starts to come out ahead, so imo your updated results are in line with that. At a glance, my Gatling load tests were much more brutal on concurrency, hence the clear "winner". – cpigeon Oct 24 '22 at 10:13
  • Please check your GET and POST requests separately. I think that get is have a bad result in your case too. But POST has so big difference, that summary is perfect. – Игорь Ходыко Oct 24 '22 at 18:10
  • I've invented my GET methods work in one thread, but POST work in different. I see it in log. But why? – Игорь Ходыко Oct 26 '22 at 16:39
0

This issue was solved by adding properties of thread-pool to application.yml

  spring:
    r2dbc:  
      pool:
        initial-size: 10
        max-size: 20

Also There is was used non-blocking subscription to repository

Mono.defer(()-> Mono.from(bookRepository.findAllBookWithDelayById(id)
                             .subscribeOn(Schedulers.boundedElastic())));

Application became 1.8 times faster then non-reactive.