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