Servlet vs Reactive (WebFlux) - Wydajność

Wydajność reaktywnego stacku (hands-on)

Featured image

Reaktywne programowanie od jakiegoś czasu jest często poruszanym tematem tak też, aby namacalnie poczuć różnicę w wydajności reaktywnego/blokującego stacku zrobiłem dla Ciebie ten mały projekt. Na początek powiemy sobie co jest zrobione oraz pokażę Ci drobne różnice implementacyjne w trzech aplikacjach jakie tu znajdziesz.

Co mamy?

product-store - reaktywna aplikacja (WebFlux), która zwraca nam produkty. Z ustawionym opóźnieniem 100ms.

spring-boot-web - blokująca aplikacja wraz z RestTemplate gdzie tworzymy zapytanie do product-store.

spring-boot-webflux - tak samo tylko reaktywnie. Korzystamy z WebClienta (czyli reaktywnego zamiennika na RestTemplate).

Kotlin + Gradle + Gatling (testy wydajnościowe - Scala)

Projekt znajdziesz na Githubie.

Różne implementacje

product-store - zaimplementowane po staremu w nowym wydaniu. Znajdziemy tutaj stare i dobre adnotacje @RestController @RequestMapping oraz inne. Słowem - wszystko co znamy z blokującego stacku. Jedyna różnica jest taka, że obiekty są opakowane w Mono, albo Flux. Flux to trochę jak taka lista, która nie ma końca. Można by to nazwać strumieniem danych. Z drugiej strony Mono to po prostu zero lub jeden element, w tym przypadku jest to produkt.

/** RestController **/
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createProduct(@RequestBody newProduct: Mono<NewProduct>): Mono<Product> =
    productService.createProduct(newProduct)

/** ProductService **/
fun createProduct(product: Mono<NewProduct>): Mono<Product> =
    product.delayElement(Duration.ofMillis(100)).map {
        Product(
            name = it.name,
            unitPrice = it.unitPrice
        )
    }

spring-boot-web - zwykła blokująca aplikacja wraz z RestTemplate. Tworzymy tutaj request w postaci Product(name, unitPrice), a w zwrotce (od product-store) dostajemy dodatkowo randomowy uuid Product(id, name, unitPrice).

/** RestController **/
@PostMapping
@ResponseStatus(HttpStatus.OK)
fun createProduct(@RequestBody newProduct: NewProduct): Product? =
    productService.createProduct(newProduct)

/** RestClient **/
private val restTemplate = restTemplateBuilder.build()

fun createProduct(newProduct: NewProduct): Product? = restTemplate.postForEntity(
    "$productStoreBaseUrl/products",
    HttpEntity(newProduct),
    Product::class.java
).body

spring-boot-webflux - to samo co powyżej z tą różnicą, że reaktywnie. Jest tu najwięcej nowych zabawek. Po pierwsze używamy tutaj DSLa (RouterFunctionDsl) od springa do tworzenia RouterFunctions, czyli to router { }. Druga rzecz to użycie Scope Functions od Kotlina. Daje nam to tyle, że przekazujemy sobie obiekty w łańcuchu wywołań i nie musimy robić tymczasowych zmiennych. Do tego oddzielamy dwie minimalnie różniące się logiki jedno to zapytanie, a drugie to odpowiedź jaką zwracamy z naszego API. To czy warto to rozdzielić to oceń sam.

/** Router (albo RouterFunction) - czyli to samo co RestController **/
@Bean
fun router() = router {
        accept(APPLICATION_JSON).nest {
            POST("/products", productHandler::createProduct)
        }
    }

/** ReactiveRestClient **/
val webClient: WebClient = WebClient.builder()
    .baseUrl(productStoreBaseUrl)
    .build()

fun createProduct(req: ServerRequest): Mono<ServerResponse> = run {
    webClient.post().uri("/products")
        .body(req.bodyToMono(NewProduct::class.java))
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(NewProduct::class.java)
}.let {
    ServerResponse.ok().body(it)
}

Powyższe snippety to tylko wycinki najistotniejszych części aplikacji.

Tutaj znajdziesz kompletny kod.

Czas na wyniki - Servlet vs Reactive?

Servlet - 2000 użytkowniów robiących 200 requestów każdy. web

Webflux - 2000 użytkowniów robiących 200 requestów każdy. webflux

Servlet - 7500 użytkowniów robiących 50 requestów każdy. web

Webflux - 7500 użytkowniów robiących 50 requestów każdy. webflux

Wyniki

Projekt znajdziesz na Githubie.