The concepts of reactive programming and reactive computing have been known for a very long time. However, recently thanks to the popularization of the microservice architecture and the implementation of reactive computing support on the Java platform they are experiencing their second youth. Since 2017, we’ve personally heard peans sung at reactive technologies at many java tech conferences around the world. Therefore, here at NubiSoft, we decided to verify the benefits of using these innovations in everyday applications. We mean AVERAGE IT systems, those that are developed most often and are most often operated- not the top ten most popular services in the world, which have completely different performance and scaling needs.
On the topic discussed today, so much hype is created that every less technical person after returning from technical conferences or seeing a few videos on youtube may ask themselves – why are all currently created systems not developed on the basis of a reactive paradigm? Or, should the system that is supposed to support my business and which I order in an outsourcing model should be designed in line with these trends?
In this post, we will try to show that there is not only the one correct answer and that there are no solutions that are ‘one-size-fits-all’. And because on the Internet you can already find many examples designed to confirm the advantages of reactive programming, we will do the opposite today.
Currently, there are several extensions for Java that allow the implementation of reactive computing, but it seems that two of them have gained overwhelming popularity – RxJava2 and Reactor. A fair comparison of them can be found e.g. here. For the experiment described in this article, we used the Reactor (Spring WebFlux). It is included in the Spring Boot 2 that implements the new version of Spring Framework 5.
Reactor widely uses Java 8 API, e.g. CompletableFuture, Stream, or Duration. The reactor also brings two elements of reactive programming namely Flux (for handling n items) and Mono (for 0 or 1 item). The reactor obviously supports non-blocking communication between processes (IPC). It gives great flexibility and is very well suited for microservice environments. It also supports the backpressure mechanism, which gives the possibility of return communication between the producer and the consumer. If the consumer is not able to process the number of events produced by the producer, it will notify him that he can send the number of events that the consumer is able to process. The reactor uses the request mechanism here, informing the producer that at the moment it can process only n requests. But enough talk, let’s check how it works in action!
We will focus on a simple case where the computing node has one single dependency and is dependent on the persistent data storage. Isn’t it the most common scenario? The authors of the article “Optimizing distributed rescala” claim it is, and we all probably feel that a large part of systems being developed is similarly simple. And now try to guess what database we chose for testing … Wrong! We chose two of them. One, NoSQL type, is MongoDB, which has built-in and production support in the form of reactive drivers. It is the favorite platform of choice for everyone who demonstrates the advantage of a reactive programming approach. We point out here that we chose version 3, because, as is apparent from many reports, version 4 still has some performance issues 😉 We chose PostgreSQL as the SQL database. It could not be otherwise – we are lovers of this platform after all! It is worth noting that version 12 outperforms version 11 in almost all – that’s why we used here for testing the version… 11 😀 In case of PostgreSQL the reactive driver is still under development, so there is no production-ready version.
In short, our tests consisted of reading from databases streams of previously-stored random (of uniform distribution) char strings. The records size was set to 1KB, 10KB, and 100KB, while the volume of streams consisted of 10, 100, and 1000 items. From the PostgreSQL we were reading data using both SQL statement and pipelined function – i.e. using RETURN NEXT statement (what in theory should allow for asynchronous processing of data stream). We conducted our test also using the traditional MVC approach (blocking) to compare the gain in performance (i.e. blocking vs reactive). The research was conducted on the dedicated environment presented below on the deployment diagram.
The computing resources composed of t2.2xlarge EC2 instances (with “T2/T3 Unlimited” option enabled) set up in the AWS (Amazon Web Services). Each of the instances consists of 8 vCPUs and 32 GiB RAM. For taking metrics we used, the Gatling, a powerful open-source load and performance testing tool for web applications. We measured the response time of the processing a given stream. First, we measured the scenario with a single user starting his request as soon as he finished the previous one. This should give the answer about the overall throughput of system without concurrency. The results are presented below in [ms].
As we can see for both MongoDB and PostgreSQL the WebFlux version gives worse results than MVC. Additionally, for the MVC approach, MongoDB always fares worse than its competitor. The same is for the WebFlux version using JDBC driver for PG. Only using the reactive driver for PG causes PG to slow down enough that Mongo overtakes it. An overall conclusion could be made here that for both databases, the more the solution tries to be reactive, the more it loses performance. Ok, but these are the results for sequential readings, maybe something will change with concurrent requests for 100, 200 and 500 users. Let us see…
Nope! Even here, we see that for so defined scenario there is no justification for reactive implementation. And once again, MongoDB outperforms PostgreSQL only when the latest one is using his experimental (r2dbc) driver.
The final conclusions can be formulated as follows:
- In the case of processing single (sparse) data streams where the data sources are databases, the use of a reactive approach need not give the expected benefits in increasing processing efficiency.
- Using a NoSQL database instead of an SQL database need not always result in improved overall system performance.
- There are no techniques that are ‘one-size-fits-all’ and always effective and we should not trust and succumb to fashion indefinitely. On the other hand, keeping the right distance allows you to respond to changing circumstances and requirements is TRUELY REACTIVE. This sentence is the basis for both defining the system architecture and the strategy of most martial arts 😉
Another view angles
- I recommend to everyone, regardless he or she is the martial art lover or not, the latest book (Bruce Lee: A Life ) about Bruce Lee living.
- The adaption of reactive processing to the current technologies of microservices is still under active development. Therefore, it cannot be ruled out that in the future the reactive approach could be used more universally.
- It should be noted that the Java reactive programming style, however, is more complicated and less transparent compared to the classical approach. This generates higher software maintenance costs and increases the risk of bugs.
- As a fan of the PostgreSQL database, I am very sad that there is still a limit associated with the use of the phrase RETURN NEXT, which limits the possibilities of pipelined and therefore reactive implementations. As you can find in the documentation of even the latest version (12.2), PG developers warn:
The current implementation of RETURN NEXT and RETURN QUERY stores the entire result set before returning from the function, as discussed above. That means that if a PL/pgSQL function produces a very large result set, performance might be poor: data will be written to disk to avoid memory exhaustion, but the function itself will not return until the entire result set has been generated. A future version of PL/pgSQL might allow users to define set-returning functions that do not have this limitation. Currently, the point at which data begins being written to disk is controlled by the work_mem configuration variable. Administrators who have sufficient memory to store larger result sets in memory should consider increasing this parameter.