22 Microservice Architecture How to Transform the System Architecture After Microservice

22 Microservice Architecture - How to Transform the System Architecture After Microservice #

Hello, I’m Tang Yang.

In the previous lesson, I introduced the reasons for the evolution from monolithic architecture to microservice architecture. You should understand that when there are scalability issues with resource dependencies in the system, or when the development and deployment costs become difficult to accept due to the monolithic architecture, we consider microserviceization of the entire system.

After microserviceization, the architecture of the vertical e-commerce system will be transformed as follows:

img

In this architecture, the logic related to users, orders, and products is extracted and deployed as independent services. The original web application and queue processing programs will no longer directly depend on the cache and database, but will instead query the information in storage by calling service interfaces.

With the idea and expectation in mind, in order to implement the service-based splitting as soon as possible, you decide to pull together the main development team to jointly develop a splitting plan. However, after detailed discussions, you find that although you have a rough direction for service splitting, there are still many questions, such as:

What principles should be followed when splitting services?

How to determine the boundaries of services? What is the granularity of services?

What problems will arise after microserviceization, and how will we solve them?

Of course, you may also want to know the specific process and steps for microservice splitting. However, this topic involves a lot of knowledge and it is not possible to cover all the content in one lesson. Moreover, the “DDD Practical Course” has already focused on explaining the specific process of microservice splitting, which you can refer to.

The above three points will affect the effectiveness of service splitting, but in actual projects, they are often overlooked by most people. Therefore, they are the focus of this lesson. I hope you can combine the content of this lesson with your own business and think about the ways and methods of business service splitting.

Principles of Microservice Splitting #

Previously, the integrated architecture you maintained was like a large spider web, where different functional modules were intricately intertwined. The relationships between methods were very complex, which made it costly to fix a bug as it could cause multiple other bugs. The weak scalability of the database also limited the scalability of the services.

Considering the above, you need to split the architecture. However, splitting is not as simple as it sounds; it actually involves refactoring or even rewriting the entire project. You need to divide the code into several sub-projects and then assemble these sub-projects using some communication method. This is a major adjustment to the architecture and requires coordination across multiple teams to complete.

Therefore, before starting the splitting process, you need to establish several principles for the split. Otherwise, you may end up with half the result for double the effort and even have a negative impact on the whole project.

Principle 1: Achieve high cohesion and low coupling within each individual service. This means that each service should only perform tasks within its own responsibilities, while delegating functions that are not within its scope to other services. Although this may seem obvious and not worth mentioning, many people often encounter problems in actual development.

For example, in a previous project, there were user service and content service, and the user information included a field called “whether the user is authenticated.” A colleague in the team had the following logic in the content service: if the user authentication field is equal to 1, it means the user is authenticated, and the content weight should be increased. The problem is that the logic to check whether a user is authenticated should be encapsulated within the user service, rather than being determined by the content service. Otherwise, if the authentication logic changes, the content service also needs to change accordingly, which does not meet the requirements of high cohesion and low coupling. Fortunately, we discovered this problem in the code review process and fixed it before the service went live.

Principle 2: Pay attention to the granularity of service splitting, starting with a coarse-grained split and gradually refining it. In the initial stage of service splitting, it is difficult to determine how exactly the services should be divided. However, based on the phrase “microservice,” it seems that the granularity of the services should be small enough, with the saying of “one method, one service.” However, having too many services can also cause problems. For example, an increase in the number of services will increase the cost of operation and maintenance. Moreover, a request that originally required calling multiple methods within a process would now need to make multiple RPC service calls across the network, which will inevitably result in a performance decrease.

Therefore, my recommendation is to start with a relatively coarse-grained granularity during the initial split and then refine the granularity as the team’s understanding of the business and microservices deepens. For example, for a community system, you can start by splitting the business logic related to user relationships into a separate user relationship service, and then independently split functionalities like a blacklist into a separate blacklist service.

Principle 3: The splitting process should try to avoid affecting the daily functional iterations of the product. In other words, the service-oriented split needs to be completed while continuing with product function iterations.

Let me give you an example based on a project I previously maintained. I once split the services during a period when a competitor was rapidly developing. The splitting approach involved stopping all business development, completely overturning and refactoring the project. As a result, we missed the best opportunity for product development and ultimately lost to the competition. Therefore, our splitting could only be carried out based on the existing integrated system, continually separating and deploying business modules. Regarding the order of separation, you can consider the following points:

  1. First, separate relatively independent boundary services (such as SMS services and geolocation services). Starting from non-core services reduces the impact of splitting on existing business and provides the team with an opportunity to practice and make mistakes.

  2. When two services are interdependent, prioritize separating the service being depended upon. For example, if the content service depends on the user service to obtain user information, if you separate the content service first, it will rely on the user module in the integrated architecture. This still does not guarantee the content service’s ability to be quickly deployed.

The correct approach is to clarify the call relationship between services. For example, the content service depends on the user service to get user information, and the interaction service depends on the content service. Therefore, the splitting sequence should be: user service first, followed by content service, and finally the interaction service.

Principle 4: The definition of service interfaces should be scalable. After service splitting, since services are deployed as independent processes, communication between services is no longer limited to method calls within a process but rather involves inter-process network communication. In this communication model, it is important to ensure that service interfaces are defined in a scalable way; otherwise, unexpected errors may occur during service changes.

In a previous project, an interface of a particular microservice had three parameters. In a business development, a colleague in the team changed this interface to have four parameters, and the places where the interface was called were also modified accordingly. However, after this service went live, it kept throwing errors, and we had to roll back.

As you have probably guessed, this error occurred because after the interface was put into production, it was modified to have four parameters, but the calling side was still using the interface with three parameters, which caused the errors. Therefore, it is best for the parameter types of service interfaces to be enclosed in a wrapper class. This way, if you need to add parameters, you don’t need to change the interface signature, but only need to add fields in the class.

Problems and Solutions of Microservices #

So, according to these principles, after splitting the system into microservices, can we solve all the problems once and for all? Of course not.

Microservices is just an architectural approach. After effective splitting, it can help achieve agile development and deployment of services. However, because the originally integrated application architecture has been split into multiple distributed services communicating over the network, in order to coordinate the normal operation of multiple services in a distributed environment, certain complexity is inevitably introduced. These complexities mainly manifest in the following aspects:

  1. Service interface invocation is no longer a method call within the same process, but a cross-process network invocation, which increases the response time of the interface. At this point, we need to choose an efficient service invocation framework. Meanwhile, the interface caller needs to know which machines the services are deployed on and which ports they are using. This information needs to be stored in a distributed and consistent storage. Therefore, a service registry is needed. This is something I mentioned in lectures 24. However, here I want to emphasize that the registry manages the complete lifecycle of services, including checking the service’s live status.

  2. There are complex interdependencies among multiple services. One service depends on multiple other services and is also relied upon by multiple services. Once the performance of the dependent service becomes problematic and a large number of slow requests are generated, the working thread pool of the dependent service will be filled, resulting in performance problems. Then, the problem will spread along the dependency network, gradually spreading upwards until the entire system fails.

To avoid this from happening, we need to introduce a service governance system. For problematic services, we can use methods such as circuit-breaking, degradation, flow control, and timeout control to restrict the problems to a single service and protect other services in the service network from being affected.

  1. After splitting services into multiple processes, in the call chain of a request, multiple services are involved. Therefore, if the response time of this request increases or errors occur, it can be difficult to know which service caused the problem. Furthermore, once the entire system fails, the external appearance may be that all services have problems at the same time, making it difficult for you to determine the root cause. This requires the introduction of distributed tracing tools and more detailed server-side monitoring reports.

I delve into these topics in detail in lectures 25 and 30. Here, I want to emphasize that monitoring reports focus on the macro performance of dependent services and resources, while distributed tracing focuses on analysis of performance bottlenecks in individual slow requests. These two aspects need to be combined to help you troubleshoot problems.

The problems introduced in development by microservices will be the main topics discussed in the “Distributed Services” and “Maintenance” sections that follow.

Overall, microservices is a big topic. While you may be able to split microservices in a short time, you may spend a considerable amount of time perfecting the service governance system. The following content will cover the principles and usage of some commonly used microservices middleware. You can better understand the upcoming content by following these steps:

  1. Quickly deploy and run the middleware to develop a sensory understanding of it.
  2. Read the basic principles and architecture design sections in its documentation.
  3. If necessary, read its source code to deepen your understanding. This can help you troubleshoot faults caused by the middleware and solve performance problems when maintaining your microservices.

Summary of the Course #

In this lesson, in order to better guide you in service-oriented decomposition, I introduced the principles of microservice decomposition, which are quite clear. Here, I would like to extend some content:

  1. “Conway’s Law” states that the design of a system is a direct result of the communication structure between its organizational units. In simple terms, the way your team is organized will determine the architecture of your system.

If your team is divided into server-side development, DBA, operations, and testing teams, then your architecture will be integrated, with all teams responsible for one large system. With a large number of team members, the cost of communication will be high. However, if you want to achieve a microservice architecture, your team should also be divided according to business boundaries, with each module being taken care of by an autonomous small team consisting of developers, testers, operations, and DBAs. This way, communication will only occur within this small team, and the cost of communication will be significantly reduced.

  1. One goal of microservice architecture is to reduce development costs, including the cost of communication, so the number of members within a small team should not be too large.

According to Amazon CEO Jeff Bezos’ “Two-Pizza Rule,” if two pizzas are not enough to feed your team, then your team is too big and needs to be divided. Therefore, an ideal small team consists of 6 to 8 people, including developers, operations, and testers.

  1. If your team is not large and not yet ready for microservice architecture, but you still feel that the cost of development and deployment is high, a compromise solution is to prioritize the separation of engineering.

For example, if you are using the Java language, you can divide the code into different sub-projects based on business boundaries, and then rely on jar packaging between sub-projects. This reduces the code size of each sub-project and can reduce packaging time. Moreover, the code within each sub-project can achieve high cohesion and low coupling, to some extent reducing development costs. This can be considered as a conservative strategy.