Book Image

Scala Microservices

By : Selvam Palanimalai, Jatin Puri
Book Image

Scala Microservices

By: Selvam Palanimalai, Jatin Puri

Overview of this book

<p>In this book we will learn what it takes to build great applications using Microservices, the pitfalls associated with such a design and the techniques to avoid them. </p><p>We learn to build highly performant applications using Play Framework. You will understand the importance of writing code that is asynchronous and nonblocking and how Play leverages this paradigm for higher throughput. The book introduces Reactive Manifesto and uses Lagom Framework to implement the suggested paradigms. Lagom teaches us to: build applications that are scalable and resilient to failures, and solves problems faced with microservices like service gateway, service discovery, communication and so on. Message Passing is used as a means to achieve resilience and CQRS with Event Sourcing helps us in modelling data for highly interactive applications. </p><p>The book also shares effective development processes for large teams by using good version control workflow, continuous integration and deployment strategies. We introduce Docker containers and Kubernetes orchestrator. Finally, we look at end to end deployment of a set of scala microservices in kubernetes with load balancing, service discovery and rolling deployments. </p><p></p>
Table of Contents (12 chapters)

Isolation

What we currently have is this:

As humans, we are affected by our surroundings. At work, our peers affect us in positive or negative ways. Countries get affected by the peaceful/destructive atmospheres of neighboring countries. Even in the human body, long-standing hypertension can affect the kidneys. Deranged kidney function leads to accumulation of waste products that were normally excreted from the body. These metabolites or waste products then affect brain function. And worse, in the case of kidney dysfunction, water is not excreted effectively, which can lead to cardiac dysfunction. So, if the kidneys start dysfunctioning, they go on to affect everything else.

In the case of the human body, the organs' behavior to impact each other is not by design but rather by designoid. in software are inevitable and they cannot be avoided. Though it is good to write code that can prevent failures, failures can come in all unexpected forms: bug/mistakes in code, network failures, an unstable host due to high CPU/memory utilization, disk failures, JVM crash, thundering herd problem, and many others. How do we design to deal with failures? One strategy to handle failures is to have a backup mechanism. For example, Windows has layered device drivers (https://msdn.microsoft.com/en-us/library/windows/hardware/ff543100(v=vs.85).aspx). So, if one layer is not functional, the next higher layer can start working, thus potentially eliminating downtime. Another example is an octopus which has three hearts and nine brains.

If you are brainy and heartbroken, that should cheer you up.

They have those extra components in place because they want to be resilient and fault tolerant (at least they would like to believe so). Humans have two kidneys and can still survive on one kidney if the other fails. The whole of evolutionary biology teaches us how to be componentized.

But what happens if the backups also start failing? Backups fix the problem and certainly makes it more resilient when compared to the previous state, but they are not full proof. An alternative approach to handle failures is to accept failures and embrace them. Because we accept the fact that failures do happen, we try devising a strategy where other parts of the application remain unaffected by them. Though, of course, we will try everything in the world to prevent them, we accept that failures they are a reality.

A way to remain unaffected by failures in surroundings, is to provide the right isolation across modules. We quantify isolation as isolation in both space and time.

Isolation in space

With software, modules of an application do impact each other for good or bad. CPU utilization, memory consumption, and resources utilized by one part of our monolith significantly affects the application. In our application, an OutOfMemoryError caused by the Stack Overflow engine in our application would destabilize the complete application. Excessive locking in the database or higher CPU on the database server due to higher load (or abuse?) by the LinkedIn engine on the database would obstruct other modules from using the database server.

What we need is isolation in space. That is, for the modules to be completely separated in all forms so that they cannot impact each other. Perhaps splitting our application into different applications, with a separate:

  • LinkedIn engine application
  • GitHub engine application
  • Stack Overflow engine application, and so on

Maybe we could deploy them on different hosts in a cloud service. Sometimes we have multiple instances of the same application running with a load balancer in front to handle higher load. In such cases, these multiple instances could be even in different continents so that a natural calamity at one location does not impact the other instances running on different locations.

Having them as different applications allows failures to be captured, signaled, and managed at a fine-grained level instead of letting them cascade to other components. But this does not fully solve the problem yet. Applications could interact with each other using REST via HTTP. In our case, the Stack Overflow engine might wish to push the consolidated ranks to our main developer-ranking application. If the developer-ranking application was down at that moment, our Stack Overflow engine would not be able to push the data.

If applications interact with each other, then it is required that:

  • They are both alive at that moment
  • The operation was a success

Failure of any one then leads to failure of the operation.

One way of dealing with such a failure is to retry the same operation at a future interval. This adds to extra boiler plate code and could get tedious if there were too many different API calls across applications.

To handle such failures, we need a further graceful solution to tackle this scenario and that is by being isolated in time.

Isolation in time

By isolation in time, we stress that it is not needed for both of the applications to be responsive at the same moment for communication to be possible. This quality has to be treated as first class. We do this by using message-passing as a mode of communication across applications.

When an application sends a message to another via a message broker, the message broker could persist all the incoming messages. By doing this, a few things are clear:

  • If the sender sends a message and the receiver is dead, then the message is not lost but rather persisted by the message carrier (we could use Apache Kafka as the means to do this, for example). When the receiver becomes alive again, it will simply take the message from the queue and do the respective action.
  • If the sender sends a message and immediately dies, we do not need to worry, as the receiver will receive the message and reply back. The original sender can then proceed from where it left off once it is alive.
  • If both the sender and receiver are alive at that moment, then the receiver could quickly respond back. This experience would be very similar to the conventional mode of working with synchronous HTTP calls.

The aforementioned mode of communication is asynchronous by nature. It not occurring or existing at the same time. Another unseen advantage of this is that once the sender sends a message, it does not have to wait for the receiver to reply back. It could go on to do other stuff, thus utilizing the resources effectively. Once the message is received, it will then take the responsive action.

Isolation in time and space makes us very powerful by being location transparent.

Two separate modules, running as different processes on two different continents, can communicate with each other via messages. They can also communicate if they are running on the same process, as have to send the message to themselves. There is no difference in interaction provided the communication protocol remains the same. This makes us very powerful, as we could scale up our application by increasing the RAM and CPU of our process. Or split it to multiple applications running on different hosts. There is no dependency on the location of the sender and receiver. So, by design, the application becomes much more scalable by being independent or transparent of the location.

We will cover in depth the asynchronous mode of communication in Chapter 3, Asynchronous and Non-Blocking.

Overview of application design till now

In designing our search engine to search for the best talent, we have come to a stage where we realize that we are ill equipped to handle many real life issues and our giant application has serious shortcomings, such as:

  • The inability to scale the application spontaneously
  • Failure of one module significantly impacts on other modules
  • Higher development time as the code base grows bigger with increased dependencies

Just like with programming, we need an appropriate design pattern that solves problems to meet our needs.