Migrate a monolithic application into microservices
"The Rise of Microservices" - with the popular orchestrator platforms like Kubernetes, Apache Mesos it becomes easy to deploy and manage microservices at scale. We need to have a proper plan and thought process before going into a full microservice way. It is not always recommended to go with microservices for anything. There are advantages and disadvantages associated with it. But, here we are assuming that you are already convinced and your business will benefit from the same.
These are assumptions we are considering before proceeding with the migration plan. These are just for example purposes and it might be a little different in your case.
i> Application is running as a single module.
ii> Application is using a database of a single Postgresql instance.
iii> Redis is being used for Caching.
iv> We are migrating the backend APIs only. Product UI will be the same and there will be on adhoc product changes during this period.
v> Observability/monitoring of the monolithic application is in place.
We will discuss about five necessary key points.
1. Identifying the logical components.
First we need to isolate the business use cases. We can approach it in different
ways,
If we are already feeling the need of migration and your running giant monolithic application is becoming a bottleneck of scale then critical modules should be prioritised first. But, there will be chances of errors and it can affect the production directly. We can pick a service which has less dependency to the core module. The components that are the best for migration are thus determined by which,
a. which product features are used by the most users.
b. are used most frequently.
c. has the fewest dependencies on other components.
d. are performing too slowly.
2. Containerization
The great question is how to move a large monolithic application into a container and when we think about containers, only Docker comes to mind. Once we finalize the module, then we need to fit it into a container. Each of the services should follow a standard Dockerfile. Since containers share the same OS kernel, it provides less insolation than a VM-only solution. Few common security measures we need to consider like containers should be running with non-root users. We need to optimize the size of the image as much as possible. Docker images should not be created from scratch and all projects should utilize the base Docker images. We also need to take care of build time and by using caching from the registry we can make it faster.
3. Managing Databases
We are assuming that each of the microservices will have a database based on the use cases. Let’s say currently we are having a single Postgresql (assuming we have structured data) host and have a lot of dependency into a single database. In a true microservice environment each service should have its own database. When we are breaking the app into multiple modules then with creating the databases for the services we need to migrate the tables as well. Data integrity, types of data, data to be copied etc are a choice of applications, but our infra should be ready to support it.
Now let’s imagine a situation when data size is large in the existing database and we can not have downtime for long hours to perform the database full dump and restoration. In that case, first we can take a dump of the schema and restore the same to the remote database and it should not take much time. Next, we can start dumping the data (assume we start it at 5:00 Hrs) and it ends at 10:00 Hrs. Now, live users would be coming to the product and writing into the database and we can not get the data after 5:00 Hrs from restoring the dump. So, to fix this, we can configure the Kafka broker endpoints into the controller of the application and send the database events into a Kafka stream during the interval (5:00 Hrs to 10:00 Hrs). Later we will have consumers ready to consume from Kafka and write into the new database asynchronously.
In case of in-memory datastores like Redis, there is no need to migrate the Redis data given that we are using it for temporary caching purposes. Only difference is that this time multiple services will be using it instead of a single monolithic app. It's possible to use the same Redis for multiple microservices, just we need to make sure to prefix our redis cache keys to avoid conflict between all microservices. As Redis is single threaded so it's discouraged to use multiple databases in the same redis instance (i.e one for each microservice) and in this case the best option would be to use a Redis host for each microservice, then we
can easily flush one of them without touching others (this is to make sure when something goes wrong on one of our redis, there is no impact on other microservices). We also need to take care of HA from an infra point of view.
4. Observability
Observability is the key to succeed in the microservice journey. It becomes very difficult to debug when we have multiple services. Monitoring a microservice is very different from monitoring a monolithic app. We should have insights of microservices and centralized logging in place before starting the migration. Below are the five points to keep in mind,
a. End to end tracing of the services. It is important to have visibility of the calls made by microservices. Service mesh like Istio and a dashboard like Kiali on top of it should be good.
b. Resource utilization for the service containers. We should allocate the resources in a way that we pay less and use maximum.
c. Database metrics and slow query logs for databases should be available. We also need to check and avoid any table lock.
d. Alerting should be set up with a threshold. Alerts on resource utilisation, API downtime, API response time, metrics such as p90, p99 etc are important to have.
e. A centralized logging stack to collect the logs of all microservices and segregate the logs based on the microservice name. We can consider the ELK (Elasticsearch + Kibana + Logstash) stack. A large set of logs without proper visualisation is useless. Having charts, dashboards etc are as important as collecting the logs. Kibana provides good features to create custom dashboards.
5. Continuous Integration & Continuous Deployment (CI/CD)
There will be a major change in the CI/CD compared to the existing pipeline that you have for the monolithic deployment. You can go with the GitOps approach where Git is the single source of truth. GitOps is not a single product and CI/CD is just a part of it. There are solutions like Github Actions, Gitlab etc and we will talk about it more in our upcoming posts. The six key points that you can consider before setting up your CI/CD.
a. Separate pipeline for each microservices.
b. Rolling update and Rollback strategy. Rolling update gives you zero downtime by incrementally updating your service instances/containers/pods and a Rollback strategy is to revert back to the previous version.
c. Image scan and static code analyzer integrated into the Continuous integration to ensure the security of the application.
d. Use a reusable template like Helm chart (If using Kubernetes).
e. Use a feature flag for a new release. You can set the flag as an environmental variable while building the image in the Continuous integration step.
f. A Better branching strategy. We should have less branches and tag a release before deployment. Committing daily is a good practice to follow.
In our next post we will talk about creating a Kubernetes cluster for your microservice deployments.