Comparing the execution model of Spring Boot, Akka and NodeJs

There are many technologies to choose from for implementing a backend service and it's not immediately obvious which to choose.

This article will compare the execution model of a few popular technologies, which is one of the key points to consider.

Note I chose a rather vague term "technologies" as each technology describe themselves differently, framework, toolkit, runtime etc. "technologies" felt apt.

To illustrate the differences I have implemented the same simple service in each of the technologies under consideration. These are Spring Boot 1.5.x [1], NodeJs [2], Spring Boot 2.x [3] and Akka [4].The service contains a single '/books' endpoint that retrieves data from a Mongo DB.

Spring Boot 1.5.x

Spring Boot 1.5.x has one-thread-per-client-request execution module, synchronous and blocking.

The following sequences of steps occur when the service [1] receives a client request `curl -X GET http://localhost:8080/books`:

1. Tomcat (Spring Boot 1.5.x default servlet container) detects an incoming client request connection and assigns a thread from the available servlet thread-pool.
2. The Spring MVC controller handles the GET request, calling the Spring Data repository.
3. Spring Data repository calls the external Mongo to retrieve the data.
4. The call stack return and the client gets the JSON response.
5. the thread is returned to the servlet thread-pool ready to handle another client request.

The sequence of steps is executed sequentially one after another. This is why we say the request is handled "synchronously". Since this is a sequence of commands being executed this is also known as imperative programming.

In step 3, the call to the external Mongo may take some time to complete. The thread waiting for the completion is said to be "blocked" and the operation is classed as "blocking".

As you can see from this execution model, Spring Boot 1.5.x can handle as many concurrent client requests as there are threads available in the configured servlet thread-pool.

fig.1 thread per request
per thread execution model





Positives

- Simple to develop (Spring Boot's convention-over-configuration that "just run").
- Easy to understand synchronous/imperative programming.
- Good choice if concurrent traffic is predictable and there are adequate hardware resources available.
- Good choice if the service makes mandatory blocking calls to service a client request [7].

Negatives

- Servlet thread pool needs to be large enough to not be a bottleneck.
- Need to align other resources that are potential bottlenecks e.g. DB connection pools, client connection pools.
- Does not scale well for large concurrent traffic.

NodeJs

Node has a single threaded event-loop [5] execution model, asynchronous and non-blocking.
The following sequences of steps occur when the service [2] receives a client request  `curl -X GET http://localhost:8080/books`:

1. The incoming client-request connection is detected and added to the "poll" phase queue.
2. When the even-loop enters the "poll" phase it will process this, handled by the 'express' route defined in ./routes/books.js
3. The 'express' route handles the GET request, calling ./module/mongo.js. internal module findAll().
4. The./modules/mongo.js uses the 'mongoose' findAll() which returns a Promise.
5. The external Mongo eventually completes and this new event is added "poll" phase queue.
6. When the even-loop enters the "poll" phase again, Promise.then() is invoked.
7. The 'express' route can now give the client a JSON response.

The subtle difference between this execution model and a traditional web server is in step 4. The call to get the Promise object returns immediately and the even-loop completes this "poll" phase task. Before step 5, the single threaded event-loop is ready to process other incoming client request connections.

Operations that allow the event-loop to continue without waiting are known as "non-blocking". Node has 3 styles of non-blocking operations, these are: promises, callbacks and await/async.

Since steps 5,6 and 7 are executed at some point in the future, the execution is said to be "asynchronous"

Many Node modules give synchronous alternatives. Compare fs.read with fs.readsync.

As you can see from this execution model, Node is able to handle many concurrent client requests with fewer resources compared with a traditional web server so long as the code does not block the event-loop.


fig.2 Node event-loop phases











Positives

- Very good scalability when non-blocking IO used, efficiently uses resources.
- Non-blocking considerations are very visible in Node development, Node modules often including promises, callbacks and await/async options.
- Support for reactive streams, able to handle streaming large files, able to deal with back pressure something data flow between systems.
- A large pool of Javascript developers.

Negatives

- Care needs to be taken not to block the event-loop [6] otherwise the service becomes unavailable.
- Unhandled exceptions may exit the Node process, bringing the service down.
- Node modules may offer different async operation styles potentially resulting in inconsistencies.
- The potential for deeply nested callback structures. Callback hell.
- The single threaded nature of JS makes Node not a well suited for intensive computational tasks, which could take advantage of parallelism.

 Spring Boot 2.x

Spring Boot 2.x allows for the traditional one-thread-per-client-request and an event-loop asynchronous and non-blocking execution model. Some considerations were for backward compatibility and to allow an easy migration path for existing Spring Boot 1.5.x code.

The service [3] was implemented using the new event-loop, asynchronous and non-blocking style.

The following sequences of steps occur when the service [3] receives a client request  `curl -X GET http://localhost:8080/books`:

1. Netty (Spring Boot 2.x default web server) detects an incoming client request connection and assigns a thread to handle the request.
2. The Spring WebFlux controller handles the GET request, calling the Reactive Spring Data repository.
3. Reactive Spring Data repository opens an unbounded stream/Flux on the external Mongo.
4. Eventually, the streamed data is gathered and the client gets the JSON response.

This execution model is similar to Node, where at step 3 the original thread is returned to the Netty server thread-pool ready to handle a new incoming client request connection.

fig.3 webFlux (controllers API) + Reactive Spring Data : fully reactive




Positives

- Much smaller thread-pool needed compared with a traditional web server.
- Spring WebFlux also has a functional API allowing route declarations in a functional style similar to Akka routes.
- Support for reactive streams, able to handle streaming large files,  able to deal with back pressure something data flow between systems.

Negatives

- Having both styles means having bloated libraries potentially resulting in inconsistencies and misuse.
- Although Java 9 has adopted the Reactive Streams API, Spring has added its own concepts Flux and Mono from its project Reactor. Still fairly new.
- If service dependencies include blocking calls then Spring MVC probably the best choice [7].

Akka

Akka uses an actor-model [8] execution model, asynchronous and non-blocking.
An Akka application is composed of actors, each actor processes immutable messages from its mailbox/queue sequentially.

The following sequences of steps occur when the service [4] receives a client request  `curl -X GET http://localhost:8080/books`:

1. An incoming client request connection is detected and the Akka Http route handles the GET request. An immutable message sent to the BooksActor's reference's mailbox using ask/? expecting Future to be returned.
2. The BooksActor receives the message and calls BookRepository.findAll() which returns a Future, piping the Future back to the sender.
3. The BookRepository object uses a connected MongoCollection to find() the data.

Step 1 is executed from a thread from the default-dispatcher thread-pool. And once the message is sent, the thread returns to the thread-pool ready to handle new incoming client request connections. Step 2 is also executed from a thread from the default-dispatcher which may or may not be the same thread as used in step 1. The long-running MongoCollection find() Future executes from a thread created from a "bulkhead" thread-pool which is designed not to block the default-dispatcher thread-pool.

Since Akka is a lower level toolkit compared with the other technologies there where many options to implement this behavior.

Akka's execution model gives it great ability to handle large concurrent traffic as well as leverage parallelism.

fig.4 Actor messaging







Positives

- Actors are cheap, enabling systems with millions of actors. Given the potential to build real-time systems.
- The actor model is naturally well suited for intensive computational tasks.
- Support for reactive streams, able to handle streaming large files,  able to deal with back pressure something data flow between systems.
- Akka is a powerful toolkit which has many features beyond the scope of this article. e.g. remote actors, actor lifetime management, self-healing etc.

Negatives

- Documentation and examples not as extensive as alternatives.
- The lower level toolkit, requires greater experience take advantage of features correctly.
- Good for more specialised use cases, other technologies are simpler to use.

Conclusion

From the perspective of the execution model, there are no "winners" per say. As the sections above show, each technology is a viable tool depending on the demands of the domain.

A related topic is if you should have a polyglot micro-services architecture that uses different technologies to suit particular services. The intuitive thought is why not? If each service has a known API. However, there are many aspects beyond the scope of this article to consider, e.g. skillsets available in the organisation, devOps concerns, architectural concerns for real-time systems using the more advanced features of Akka Actors, etc.

Resources


Comments

Popular posts from this blog

Comparing REST and GraphQL