preface
I saw an e-book about the anti-mode of micro-service on the Internet and felt the content was very good, so I decided to translate it into Chinese by stages. The purpose of the translation is also to help friends who want to have a deeper understanding of micro-service. Due to the limited English level, if there is any wrong translation, I hope to comment on it.
The English table of books is as follows
The Chinese contents of the books are as follows: 1, Data-driven migration anti-pattern 1.1, Too much data migration 1.2, Feature segmentation first, Data migration last 2, Timeout anti-pattern 2.1, Use timeout 2.2, Use breaker pattern 3, Sharing anti-pattern 3.1, Too much dependence 3.2, Sharing code technology 4. Anti-pattern 4.1, Problems with microservice reporting 4.2, Asynchronous Event Pushing 5, Sand trap 5.1, Analyzing scope and functionality of services 5.2, Analyzing database transactions 5.3, Analyzing Service Orchestration 6, Developer trap without cause 7, Bandwagon Trap Other architectural patterns 9 static Contract Trap 10 Are we there yet 11 REST Trap
Data-driven migration antipatterns
Microservices create a large number of small, distributed, single-purpose services, each with its own data. This service and data coupling supports a bounded context and an architecture with no shared data, where each service and its corresponding data is a separate piece, completely independent of all other services. A service exposes only one explicit interface (service contract). Bounded contexts allow developers to quickly and easily develop, test, and deploy with minimal dependencies.
Adopting the data-driven migration anti-pattern occurs primarily when you migrate from a monolithic application to a microservice architecture. The main reason we call it an anti-pattern is that initially we thought it was a good idea to create a microservice, where the service and the corresponding data are separate microservices, but this can lead you down the wrong path, leading to high risk, excess cost and extra migration effort.
There are two main goals for single application migration to microservices architecture:
- The first goal is to split the functionality of monolithic applications into small, single-purpose services.
- The second goal is to migrate data from individual applications to small databases (or stand-alone services) that each service owns.
The figure below shows a typical migration that looks like service code and corresponding data are migrated simultaneously.
Above three service comes from monomer application division, and is still divided into three databases, it is a process of natural evolution, because in each service between the database and use the most key of bounded context, however, we encounter problems is based on a process that will lead us into the data migration of anti-patterns.
1.1 Too much Data migration
This is the main problem that migration path, it is difficult to in a can be clearly divided into each service granularity, from a more coarse-grained services start, refine work step by step, and want to know more about relevant business knowledge, constantly adjust the granularity of service, we found that the left to see figure 1-1 service granularity is too thick, You may need to split it into two smaller services, or you may find that the two services on the left are too fine-grained and need to be merged. Data migration is more complex and error-prone than source code migration, and it is best to only migrate data once, because data migration is a high-risk task.
Our microservice segmentation is the migration of application code and data. See Figure 1-2.
1.2 Function segmentation first, data migration last
This pattern is primarily an avoidance approach, focusing on migrating the functionality of the service first, while also paying attention to the bounded context between the service and the data. We can adjust the service by merging and splitting until we are satisfied, at which point we can migrate data.
As shown in figure 1-3, on the left side of all three services have been carried on the migration and break up, but all the services are still using the same database, and if this is a temporary intermediate solution can be used as an option, then we need to know more about how to use the service, and accept what kind of request data, and so on.
In Figure 1-3, notice how the left-most service is found to be too coarse-grained and split into two services. Once the service granularity is finalized, the next step is to migrate the data in a way that avoids repeated data migrations.
The timeout anti-pattern
Microservices are a distributed architecture in which all of the components (that is, services) are deployed as separate applications and communicate through some kind of remote access protocol. One of the challenges of distributed applications is managing the availability of remote services and their responses. Although service availability and service response both involve communication of services, they are two very different things. Service availability is the ability of a service consumer to connect to a service and be able to send requests, and service response is concerned with the response time of the service.
As shown in figure 2-1, if the service consumers cannot connect to the service provider, by will be notified in the millisecond time and feedback, this time the service consumer can choose to be direct return error information or retry, but if the service provider receives the request but do not respond, In this case the service consumer can choose to wait indefinitely or set a timeout, which may seem like a good idea, but it leads to a timeout anti-pattern.
2.1 Usage Timeout
You might be confused. Isn’t setting a timeout a good thing? In most cases, setting the timeout incorrectly can cause problems. For example, when you are shopping online and you submit an order, the service keeps processing and does not return, and you submit the order again when the time expires. Obviously, the server needs more complicated logic to handle the problem of repeated order submission.
So how much time out is appropriate?
- The first is to calculate the service timeout based on the database timeout.
- The second is to calculate the maximum processing time under load and multiply it by two as the timeout.
In Figure 2-2, the average response time is 2 seconds in the usual case, and the maximum time is 5 seconds in the case of high concurrency, because the timeout time can be set to 10 seconds using the double technique service.
The solution in Figure 2-2 May seem perfect, requiring every service consumer to wait 10 seconds just to determine that the service is not responding. In most cases, the user will not wait more than two or three seconds before waiting for the submit button or before abandoning and closing the screen. There has to be a better way.
2.2 Using circuit breaker mode
Compared with the above method of overtime, the use of circuit breaker more safe way, this kind of design patterns, like home appliances of the fuse, when the load is too large, or circuit failure or abnormal occurs, the current will continue to rise, in order to prevent the rise of current possible damage or expensive some important components in the circuit, and even cause fire burned circuit. The fuse will fuse and cut off the current when the current rises to a certain height and heat, thus protecting the safe operation of the circuit.
Figure 2-3 illustrates how the breaker mode works. When the service remains responsive, the breaker will close, allowing requests to pass. If the remote service suddenly becomes unresponsive, the circuit breaker opens, preventing the request from passing through until the service responds again. Unlike the fuses in your home, of course, the circuit breaker itself can constantly monitor service.
The advantage of the breaker mode over setting a timeout is that the consumer can immediately know that the service has become unresponsive instead of waiting for a timeout, and the consumer will be unresponsive within milliseconds instead of waiting 10 seconds to get the same information.
Additional circuit breaker can monitor in several ways, the easiest way is to the remote service for simple heart examination, this way just tell the breaker service is alive, but if you want to obtain detailed information service to survive, you need on a regular basis (such as 10 seconds) to obtain a service details, and there’s a way to monitor real-time users, This way can be adjusted dynamically, once the threshold is reached, the breaker can enter a semi-open state, and a certain number of requests can be set to go through (say 1 of 10).
Sharing antipatterns
Microservices are a shared-nothing architecture. I prefer to call it share-as-little-as-possible because there is always some code that is shared between microservices. For example, instead of providing an authentication microservice, the authentication code is packaged into a JAR file: security.jar, which can be used by other services. If security checks are a service-level function, each service that receives a request checks security, which is a great way to improve performance.
However, if used too often you end up with a dependency nightmare, as shown in Figure 3-1, where each service depends on multiple custom shared libraries.
This level of sharing not only breaks the bounded context of each service, but also introduces several issues, including overall reliability, change control, testability, and deployability.
3.1 Too Much Dependence
Sharing is a common problem in object-oriented software development, especially when moving from a single layered architecture to a microservice architecture. Figure 3-2 shows abstract classes and sharing, which are eventually shared in most monolithic layered architectures.
Creating abstract classes and interfaces is the most important practice of object-oriented programming, so how do we deal with code shared by hundreds of services?
The main goal of the microservice architecture is to share as little as possible, which helps maintain the bounded context of the service and allows us to test and deploy it quickly. The greater the dependencies between services, the more difficult the isolation of services, and therefore the more difficult it is to test and deploy them individually.
3.2 Techniques for sharing code
The best way to avoid this antipattern is to not share code, but in practice there is always some code that needs to be shared, so where does that code go?
Figure 3-3 shows the four most basic techniques:
- Shared project
- The Shared library
- copy
- Service merge
Arrive report antipattern
There are four ways to handle reports in a microservice architecture.
- database pull model
- HTTP pull model
- batch pull model
- event-based push model
The first three pull data from a service’s database, so this anti-pattern is called “rearch-in Reporting “. Since the first three of these anti-patterns appear, let’s look at why they cause trouble.
4.1 Problems reported by microservices
There are two main problems:
- How to get the latest data in a timely manner
- Maintain a bounded context between services and data
In the microservice architecture system, the first method is to use the database fetch model. Users directly fetch data from the database of the service, as shown in Figure 4-1:
The quickest and easiest way to get data is to access it directly. While this may have seemed like a good idea before, it leads to obvious dependencies between services. The figure above introduces database independence.
Another technique for avoiding data coupling is called the HTTP pull model. Using this model, instead of directly accessing each service’s database, consumers simply need to make a REST HTTP call to each service to access its data. See Figure 4-2.
The advantage of this approach is that services are segmented based on bounded context, but it is too slow to meet complex and large data acquisition requirements.
The third is the batch pull mode. In this mode, an independent report database or data warehouse is created and the data of different service databases is pulled to the new independent database through batch processing, as shown in Figure 4-3.
The problem with this model is that it is still heavily dependent on the database, and if the database of the pull service is updated, the batch data pull process will have to be modified.
The last is the asynchronous event model, which is recommended, as shown in Figure 4-4
Five, sand trap
One of the biggest challenges architects and developers face when adopting microservice architectures is the issue of service granularity. What is the appropriate service granularity for microservices? Service granularity is critical because it affects application performance, robustness, reliability, testability, and setting up the release model.
Sand traps occur when the granularity of services is too small. Microservices being small does not mean that smaller services are better, but how small is small?
One of the main reasons for this trap is that developers often confuse services with classes, and often a class is a service, where it’s easy to fall into the sand trap.
A service should be thought of as a service component, which should have a clear and concise definition of roles and responsibilities and a clear set of operations. It is up to the developer to decide how the service component should be implemented and how many implementation classes the service needs.
As shown in Figure 5-1, service components are implemented through one or more modules (for example, Java classes). A one-to-one relationship between the model and service components makes services too granular and difficult to maintain later, while services implemented through a single class are often too large and have too many responsibilities, making them difficult to maintain and test.
Of course, the granularity of microservices is not determined by the number of classes that the service implements. Some services are simple and can be implemented with only one simple class, while others require more classes. Since the number of classes cannot be used to determine the granularity of a microservice, what criteria is appropriate to measure the granularity of a microservice?
There are three main ways:
- Scope and functionality of a service
- Requirements for database transactions
- Level of service choreography.
5.1 Analyzing service scope and functions
The first way to determine if the level of service granularity is correct is to analyze the scope and functionality of the service. What does the service do? What are its operations?
For example, a customer service operation has the following operations:
- add_customer
- update_customer
- get_customer
- notify_customer
- record_customer_comments
- get_customer_comments
In this example, the first three operations are related, as they are used to manage and maintain customer information, but the last three are not related to CRUD operations. When analyzing the completeness of the service, it becomes clear that the service can be divided into three services: customer information service, customer notification service and customer comment service.
Figure 5-1 shows the evolution from coarse-grained service to fine-grained service.
Sam Newman provides a good practical way to start by dividing services into coarse-grained services and then further into smaller services as you learn more about them.
5.2 Analyzing database transactions
Database transactions are more formally called ACID Transactions (Atomicity, Consistency, Isolation, and Wellness). ACID transactions encapsulate multiple database updates into a single unit of work that either completes as a whole or is rolled back with an error.
Because microservices are distributed, independent applications, it is extremely difficult to maintain ACID transactions between two or more services, So microservices architectures often rely on BASE (Basic Availability, Soft State, and Eventual consistency). However, you still need to use ACID transactions in a specific service. When you need to make tough decisions in ACID vs. BASE transactions, your services may be too finely partitioned.
When you find that final consistency is not available, you usually adjust the service from a fine-grained service to a coarse-grained service, as shown in Figure 5-2.
5.3 Analyzing service Choreography
The third measure is analyzing service choreography. Service choreography refers to communication between services, and often within services.
Calling services remotely takes time and can degrade overall application performance. Furthermore, it affects the robustness and reliability of the service.
If you find that too many services need to be called to complete a logical request, the granularity of services may be too small. The more remote invocations you make for a single business request, the more likely one of them will fail or time out.
If you find that you need to communicate with too many services to complete a single business request, your services may be too fine-grained. In analyzing the service choreography level, you typically move from a fine-grained service to a coarser service, as shown in Figure 5-4.
By consolidating services and merging them to coarser granularity, you can improve the overall performance, robustness, and reliability of applications. You can also remove dependencies between services for better control, testing, and distribution.
Of course, you may argue that calling multiple services can be executed in parallel to improve the response time of the overall application, such as the asynchronous programming method of Reactive architecture. In fact, the key is to balance the advantages and disadvantages to ensure timely response to users and overall system reliability.