Author: Abhijit Das
We have been building IT applications for several years, and each year, they are improving. Many technologies, architectural patterns, and best practices have evolved over time. Microservices are one of those architectural patterns that have emerged to address the need for applications to be stable, robust, always available, scalable, and fault-tolerant.
What is Microservices Architecture?
It is an architectural pattern that helps in creating IT systems consisting of self-contained, loosely coupled applications. This allows the applications to be developed, deployed, scaled, and maintained independently of each other. Each microservice is independent and can connect to other services to exchange messages. When these applications function together, they create larger IT systems.
What are the key benefits of Microservice Architecture?
Systems built following microservice patterns come with the following features/benefits:
- Highly available
- Technology agnostic
- Support faster release cycle
- Faster time to market
- Easier to understand and maintain
- Improve data security
- Optimized use of team resources and skills
- Better hardware/resource utilization
However, if you are starting to implement microservices or decoupling capabilities from a monolith into microservices, there are a few prerequisites that need to be adopted to ensure success in your microservice implementation roadmap. The microservice architecture consists of more components that are susceptible to frequent change. Due to its distributed nature, it requires additional effort to maintain, deploy, monitor, secure, and load balance. There is also increased network traffic, leading to higher network latency when communicating between services. Another challenge is handling distributed transactions. To address these challenges, it is crucial to have the right infrastructure in place to automate processes and reduce operational overhead.
Below are the necessary components to build a robust microservices application. These components address various problems encountered in distributed applications.
The API gateway provides a unified interface for external clients to communicate with the microservices. It can authenticate, authorize, track, and block potential attacks, cache and log usage statistics, monitor messaging, and perform load balancing. By handling these tasks, the API gateway enables microservices to remain lightweight.
There are proprietary cloud platform providers like Amazon and Microsoft, as well as open-source tools such as Netflix Zuul Server and Spring cloud gateway can function as Gateway Server wherein all the client requests have to pass through it. The security and all network request/response operations are taken care of by the gateway server.
In a real application, multiple instances of a service will be running on different nodes. The number of running instances fluctuates depending on the load. The IP and port number of each instance vary and may change if we replace any instance with a new one. If a client application makes a service call based on a specific host and port, it may fail if the serving instance has changed. To overcome this problem, a service registry can be used, which helps in maintaining a list of endpoints for all active instances. The load balancer fetches meta-information about the healthy nodes using service discovery to route the requests. Client applications can connect to a single gateway URL, thus hiding the internal complexity. Eureka Server and Eureka Clients from Spring Cloud help overcome this challenge.
The system load may change depending on usage patterns, such as seasonal or promotional offers on a shopping site, deadlines for business or compliance within an organization, or peak and non-peak usage during working and non-working hours. During peak hours, more running instances need to be created (scale up) to serve more users. Similarly, the number of running instances can be reduced (scale down) during off-peak hours to release resources and reduce costs. Additionally, there is a need to evenly distribute request traffic among all serving nodes to optimize compute resource utilization and enhance system availability. Load balancers can distribute incoming requests to all available nodes. Kubernetes has built-in support for load balancing, and cloud providers such as AWS and Azure offer load balancer services that can be implemented.
Businesses require robust and fault-tolerant applications. A microservice application is designed with failure in mind. However, completely avoiding failures in a large microservice application is nearly impossible. Therefore, it is essential to design the application to minimize the business impact when some services fail. Implementing a circuit breaker pattern becomes necessary to handle unresponsive services and provide graceful fallback functionality. Additionally, once the fault is rectified, the service should automatically resume serving the business. Quick failure isolation is crucial to mitigate the risk of application-level failures.
Spring Cloud Resilience or Netflix Hystrix can be utilized to implement circuit breaker patterns and incorporate fault tolerance functionality. These tools help prevent complete failures of software applications.
Logging is essential for debugging and troubleshooting applications. In a complex environment with many instances running simultaneously, troubleshooting becomes challenging. Therefore, a centralized log storage and visualization system is crucial, where all log files can be fetched and accessed. The ideal approach to implement logging in microservices applications is by using a log aggregator, which allows viewing all logs in one place instead of being scattered across different components or services. Tools such as Splunk or ELK (Elasticsearch and Kibana) are ideal for searching, filtering, and querying logs. Another tool for this purpose is Application Insight in the Azure platform.
A microservice application consists of numerous services and components, which introduces operational overhead in managing such a complex system. Additionally, the increased integration touchpoints create more potential points of failure. Therefore, it is crucial to continuously monitor the entire system and establish mechanisms to send alerts when failures occur or there are indications of potential failures. Effective monitoring of the infrastructure and services is necessary for early issue detection, failure prevention, and diagnostic support. Implementing a robust monitoring system is essential to monitor data from a centralized location. This system tracks key health parameters and system-level metrics such as storage, CPU, and memory utilization, as well as application-level and business-level metrics such as transactions per second, transaction failure frequency, and percentages. Examples of monitoring tools include Prometheus, Kibana, and Grafana.
Cloud-native microservices are distributed and decentralized. Each service has its own database, which facilitates the separation of concerns, improves data quality, and enhances security. Any data stored within a service’s database should be treated as a single source of truth. The data should be made available to other services as required through APIs. This is crucial for maintaining data cohesion and avoiding service coupling.
Synchronous inter-service communications block threads and utilize compute resources, even when they are idle, waiting for another service to reply to a prior request. This communication pattern leads to a buildup of threads and degrades the system’s responsiveness. To address this issue, a solution is to design for more asynchronous and event-driven communication that does not require waiting for another service. The use of messaging systems such as Kafka, ActiveMQ, RabbitMQ, AWS SQS, and Azure Service Bus are popular options available for this purpose.
Continuous delivery is a software development method in which releases are performed in small sets of changes at higher frequencies. It requires a DevOps culture and the automation of a significant portion of the delivery process, including build, testing, quality checking, deployment, and verification. Deploying frequent updates in small increments reduces the likelihood of errors, and even if they occur, they are relatively easier to rectify. There are numerous popular CI/CD tools available to choose from, such as Jenkins, GitHub Actions, TeamCity, and others.
Containers are popular for running microservices because they offer security, portability, and faster startup times compared to VM. A container provides an isolated and consistent runtime environment for the microservice, along with all its dependencies. Containers can be easily ported across different environments. The container runtime is hosted on an operating system, and containers run within this runtime. Multiple containers, each containing different microservices, can be deployed on a single system, enabling better utilization of compute resources compared to running services on a VM.
We need to manage container instances, including tasks such as creating new instances, deleting unnecessary containers, recovering from failed containers, rolling out updates, and configuring inter-service communication beyond container boundaries. Managing the lifecycle of containers becomes a complex task when there are hundreds of microservices running, requiring manual efforts to start, stop, deploy, manage, and scale containers. Therefore, automatic container orchestration is necessary. Docker Swarm and Kubernetes engines provide programmable orchestration mechanisms to efficiently handle a large number of containers.
In summary, microservices have emerged as the most widely accepted distributed development paradigm. In this article, we provided a brief overview of all the components, tools, and design patterns involved in building a microservices application.