As we have seen so far, microservices are not easy to author, and they come with many cross-cutting concerns and challenges. It is always important to ask yourself Why? and Do I really need it? before implementing any design patterns.
At a high level, some benefits of this approach are listed as follows:
- Scalability
- Availability
- Development speed
- Improved data storage
- Monitoring
- Deployment
In the following sections, we will dive into the details of each.
Scalability
In the monolithic approach, you scale all or nothing. In microservices, it is easier to scale individual parts of the application and address specific performance gaps as they arise. If a vaccine becomes widely available and customers are encouraged to book an appointment online, then we are sure to experience a large load during the first few weeks. Our customer microservice might not be too affected by that, but we will need to scale our booking and appointments and payments services.
We can scale horizontally, which means that we can allocate more CPU and RAM when the load increases. Alternatively, we can scale vertically by spawning more instances of the service to be load balanced. The better method is relative to the service’s needs. Using the right hosting platforms and infrastructure allows us to automate this process.
Availability
Availability means the probability of a system being operational at a given time. This metric goes hand in hand with the ability to scale, but it also addresses the reliability of the underlying code base and hosting platform. The code base plays a big part in that, so we want to avoid, as much as possible, a single point of failure. A single point of failure affects the entire system if it fails at any point. For example, we will be exploring the gateway pattern, where we will aggregate all services behind one point of entry. For our distributed services to remain available, this gateway must be always online.
This can be achieved by having vertical instances that balance the load and distribute the expected responsiveness of the gateway and, by extension, the underlying services.
Development speed
Given that the application has been broken into domains, developers can focus their efforts on ensuring that their set of features is being developed efficiently. This also contributes to how quickly features can be added, tested, and deployed. It will now become a practical approach to have one team per subdomain. Additionally, it becomes much easier to scope the requirements for a domain and focus on fewer functional requirements for a piece of work. Each team can now be independent and own the service from development to deployment.
This allows Agile and DevOps methodologies to be easier to implement, and it is easier to scope resource requirements per team. Of course, we have seen that services will still need to communicate, so we will still have to orchestrate the integration between the teams. So, while each team is independent, they will still need to make their code and documentation available and easy enough to access. Version control also becomes important since the services will be updated over time, but this must be a managed process.
Improved data storage
Our monolithic application uses one database for the entire application. There are situations where you might end up using one database for multiple microservices, but this is generally discouraged, and a database-per-service approach is preferred. Services must be autonomous and independently developed, deployed, and scaled. This is best accomplished if each service has its own data storage. It makes even more sense when you consider that the type of data being stored might influence the type of data storage that is used. Each service might require a different type of data store, ranging from relational database storage such as Microsoft SQL Server to document-based database storage such as Azure Cosmos DB. We want to ensure that changes to a data store will only affect the associated microservice.
Of course, this will bring its own challenges where data will need to be synchronized across the services. In the monolith, we could wrap all steps inside one transaction, which might lead to performance issues for potentially long-running processes. With microservices, we face the challenge of orchestrating distributed transactions, which also introduces performance risks and threatens the immediate consistency of our data. At this point, we must turn to the concept of eventual consistency. This means that a service publishes an event when its data changes and subscribing services use that event as a signal to update their own data. This approach is made possible through event-sourcing patterns. We accept the risk that, for a period, data might be inconsistent across subdomains. Message queue systems such as Kafka, RabbitMQ, and Azure Service Bus are generally used to accomplish this.
Monitoring
One of the most important aspects of a distributed system is monitoring. This allows us to proactively ensure uptime and mitigate against failures. We need to be able to view the health of our service instances. We also begin to think about how we can centralize logs and performance metrics in a unified manner, sparing us the task of going to each environment manually. Tools such as Kibana, Grafana, or Splunk allow us to create a rich dashboard and visualize all sorts of information about our services.
One very important bit of information is a health check. Sometimes, a microservice instance can be running but is failing to handle requests. For example, it might have run out of database connections. With health checks, we can see a quick snapshot of the service’s health and have that data point returned to the dashboard.
Logging is also a crucial tool for monitoring and troubleshooting. Typically, each microservice would write its own logs to files in its environment. From these logs, we can see information about errors, warnings, information, and debug messages. However, this is not efficient for a distributed system. At this point, we use a log aggregator. This gives us a central area to search and analyze the logs from the dashboards. There are a few log aggregators you can choose from such as LogStash, Splunk, or PaperTrail.
Deployment
Each microservice needs to be independently deployable and scalable. This includes all the security, data storage, and additional assets that our services use. They must all live on physical or virtual servers, whether on-premises or in the cloud. Ideally, each physical server will have its own memory, network, processing, and storage. A virtual infrastructure might have the same physical server with the appropriate resource allocations per service. Here, the idea is that each microservice instance is isolated from the other and will not compete for resources.
Now, each microservice will need its own set of packages and supporting libraries. This then becomes another challenge when provisioning different machines (physical or virtual) and their operating systems. We then seek to simplify this by packaging each microservice as a container image and deploying it as a container. The container will then encapsulate the details of the technology used to build a service and provide all the CPU, memory, and microservice dependencies needed for operation. This makes the microservice easy to move between testing and production environments and provides environment consistency.
Docker is the go-to container management system and works hand in hand with container orchestration services. Orchestration becomes necessary to run multiple containers across multiple machines. We need to start the correct containers at the correct time, handle storage considerations, and address potential container failures. All of these tasks are not practical to handle manually, so we enlist the services of Kubernetes, Docker Swarm, and Marathon to automate these tasks. It is best to have all the deployment steps automated and be as cost-effective as possible.
Then, we look to implement an integrated pipeline that can handle the continuous delivery of our services, with as minimal effort as possible, while maintaining the highest level of consistency possible.
We have explored quite a bit in this section. We reviewed why we might consider using a microservices approach in our development efforts. Also, we investigated some of the most used technologies for this approach. Now, let us turn our attention to justifying our use of microservices.