Wydajność Webflux (ReactiveStack) vs Web (ServletStack)

braintelligence.pl 5 lat temu

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. Nie jest to produkcyjna aplikacja, ale wystarczy do porównania obu stacków.

Co użyliśmy? ?

  • Kod źródłowy w Kotlinie.
  • Zbudujesz to Gradle’em.
  • A testy wydajnościowe puścisz w Gatlingu (Scala).

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).

Tak więc mamy dwie aplikacje reaktywne oraz jedną blokującą. 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, iż 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ą, iż 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, iż 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 – web vs webflux ?

Web – w tym samym momencie 2000 użytkowniów robiących 200 requestów każdy.

Webflux – w tym samym momencie 2000 użytkowniów robiących 200 requestów każdy.

Web – w tym samym momencie 7500 użytkowniów robiących 50 requestów każdy.

Webflux– w tym samym momencie 7500 użytkowniów robiących 50 requestów każdy.

Co widzimy?

  • Przy 7500 użytkownikach zbliżyłem się do ograniczeń swojego sprzętu – stąd też większa ilość błędów.
  • Reaktywny stack obsłużył requesty mniej więcej dwa razy szybciej.
  • Reaktywny był bardziej responsywny. Czasy odpowiedzi są lepsze. Jest to najbardziej widoczne przy dużym obciążeniu.

Krótki wstęp do Webfluxa ?

Main photo by JR Korpa on Unsplash

Idź do oryginalnego materiału