Introduction: Microservices are hot now. The popular comparison in the industry are all so-called Monolithic applications, while a large number of systems were distributed systems more than ten years ago. Then, what is the difference between microservices as a new concept and the original distributed system, or SOA (service-oriented architecture)?
On the core concepts of microservices architecture
Microservices architecture is different from SOA
Let’s look at the similarities first
1. Registry is needed to realize the dynamic service registration discovery mechanism;
2. Transaction consistency under distributed mode should be considered. Under CAP principle, two-stage commit cannot guarantee performance, and transaction compensation mechanism should be considered.
3. Synchronous call or asynchronous message delivery, how to ensure message reliability? SOA integrates all messages by the ESB;
4. A unified Gateway is required to aggregate and arrange interfaces, realize a unified authentication mechanism, and provide RESTful interfaces for external applications.
5. Similarly, we should pay attention to how to locate system problems under redistribution and how to do log tracking, just as we have done signaling tracking for more than ten years in the field of telecommunications.
So what’s the difference?
Is it continuous integration, continuous deployment? For CI, CD (continuous integration, continuous deployment), which itself is intertwined with Agile and DevOps, I think it’s more in the realm of software engineering than microservices technology per se;
Is there a difference between using different communication protocols? The benchmark communication protocol for microservices is RESTful, while traditional SOA is generally SOAP. However, lightweight RPC frameworks such as Dubbo, Thrift and gRPC are widely used. In Spring Cloud there is also the RPC-like behavior of Feign framework turning standard RESTful apis into code. These communication protocols should not be the core difference between microservices architecture and SOA;
Is it popular container-based framework or virtual machine based? Docker and virtual machine or physical machine are both ways of architecture implementation, not the core difference;
The essence of microservices architecture is sharding
There is a big difference in service segmentation. SOA originally emerged as an “integration” technology. Many technical solutions encapsulate the original internal services of the enterprise into an independent process, so that new business development can reuse these services, which are likely to be very large particles such as supply chain and CRM. The “micro” of micro service shows that he is exquisite in segmentation and does not compromise. There are countless examples that show that if your shards are wrong, you can have more trouble with Monolithic than with the “low coupling, no-impact upgrade, high reliability” benefits that microservices promise.
Microservices that do not split storage are pseudo-services:
In practice, we often see a kind of architecture, the back-end storage is all and in a database, just split the front end of the business logic to different service process, in essence, as a Monolithic, just change the module between the in-process calls to call between process, this kind of segmentation is not desirable, in violation of the distributed first principles, Module coupling is not resolved, but performance suffers.
First Principle of Distributed Design – “Don’t distribute your objects”
The word “Micro” for microservices is not as small as possible, but rather we need a smaller and more appropriate granularity than coarse-grained services like SOA, and the Micro is not infinitely small.
If we combine two-way (synchronous) communication with small/micro services and follow principles such as “1 class =1 service”, then we are effectively back in the 1990s with Corba, J2EE, and distributed objects. Unfortunately, the new generation of developers, who have no experience with distributed objects and therefore do not realize how bad this idea is, are trying to repeat history, only this time using new technologies such as HTTP instead of RMI or IIOP.
Microservices and Domain Driven Design
A simple library management system certainly does not require a microservice architecture. Since the micro-service architecture is adopted, the problem space must be relatively large, such as the whole e-commerce and CRM.
How to disassemble the service?
What method is used to dismantle the service? The industry is popular 1 class =1 service, 1 method =1 service, 2 Pizza teams, can rewrite in 2 weeks and other methods, but these lack the implementation basis. We have to look at some software design approaches where the problem space where object-oriented and design patterns apply is a module, whereas the idea of functional programming works more at the micro level of code.
Eric Evans’s domain-Driven Design book is a great guide to microservices architecture, which presents a technique that can break down a large problem space into relationships and behaviors between domains and entities. For now, this is the most reasonable solution to the problem of separation. Through the concept of Bounded Context (hereinafter referred to as BC), we can encapsulate the implementation details, so that ALL BC can realize SRP (single responsibility) principle. Each microservice is a physical mapping of BC in the real world, and the microservices that conform to BC idea are independently and loosely coupled to each other.
Microservices architecture is a good thing, forcing people to focus on the rationality of software design. If Monolithic domain analysis, object-oriented design is not done properly, switching microservices will multiply the problem.
With electricity in the two fields, for example, order and the goods in accordance with the DDD apart, they should be two independent bounded context, but the order must be contain goods, if rushed down to two BC, query, call relations are coupled together, even with the troublesome problem of distributed transaction, how the association apart? BC theory holds that in different BCS, even if it is a term, its focus is different. In commodity BC, the focus is on attributes, specifications, details, etc. (In fact, commodity BC has price, inventory, promotion, etc., so it is unreasonable to treat it as a single BC. In order to simplify the example, We first consider commodity BC as basic commodity information), while in order BC, we pay more attention to commodity inventory and price. Therefore, in the actual coding design, the order service will often focus on the commodity name, price and other attributes redundancy in the order, this design freed from the strong association with commodity BC, two BC can provide services independently, independent data storage
summary
Micro service architecture must first concern is not RPC/ServiceDiscovery/Circuit Breaker, these concepts are not Eureka/Docker/SpringCloud/Zipkin the technical framework, but the boundary of the service, responsibility, Partitioning errors can lead to numerous intercalls and distributed transactions between services, in which microservices can be troublesome rather than convenient.
DDD brings us a reasonable means of division, but DDD concept is numerous, obscure and difficult to understand, how to grasp the focus, reasonable application of microservices architecture?
In my opinion, the following architectural ideas are the most important:
1. Hyperemia model
2. Event driven
Microservice and hyperemia models
We talked about the relationship between microservices DDD, many people still think it is unreal, DDD so complex theory, aggregation root, value object, event source, how should we start?
In fact, DDD and object-oriented design, design patterns and other theories are inextricably linked, if you are not familiar with OOA, OOD, DDD is not good to use. However, these OO theories often feel useless because most Java programmers start their careers by learning the classic J2EE layering theory (Action, Service, Dao), where there is little opportunity to use so-called “behavioral” design patterns. The core reason for this is that J2EE’s classic layered approach to development is the “anaemic model.”
Martin Fowler, in his book Enterprise Application Architecture Patterns, identifies two development styles, “transaction scripts” and “domain models,” which correspond to the “anaemic model” and the “congested model.”
Transaction script development pattern
The core of transaction script is process. It can be considered that most business processing is a piece of SQL. Transaction script organizes a single SQL into a section of business logic, and uses transactions to ensure ACID of logic during logic execution. The most typical example is stored procedures. Of course, we often use transaction scripts in the Service layer in the normal J2EE classic hierarchical architecture.
With this development approach, objects are only used to transfer data between layers, which is the “anaemic model” of objects, with only data fields and Get/Set methods, and no logic in the object.
Let’s take an inventory deduction scenario for example:
The business scenario
Let’s start with the business scenario, an order reduction inventory (lock inventory), which is very simple. Determine whether the inventory is sufficient, deduct the marketable inventory, increase the inventory occupied by orders, and then record an inventory change log (as evidence).
Design of anemia model
First, design an inventory table Stock with the following fields:
Design a Stock object (Getter and Setter omitted) :
The Service entrance
Design a StockService that writes logic to its lock method with inputs (spuId, skuId, num).
Implementation pseudocode
If you can do a better job of combining update and SELECT count, you can use one statement to complete the spin and solve the concurrency problem (expert).
summary
Here I recommend an architecture learning exchange group. Exchange learning group number: 705127209 inside will share some senior architects recorded video video: Spring, MyBatis, Netty source analysis, high concurrency, high performance, distributed, microservice architecture principle, JVM performance optimization, distributed architecture and so on to become the architect of the necessary knowledge system. I can also get free learning resources, which I benefit a lot from now.
Have you noticed that the Stock object does not appear at all in the operation process of order placing and inventory reduction, which is a very important core logic in this business field? It is all database operation SQL. The so-called business logic is composed of multiple SQL. Stock is just a CRUD data object, there is no logic to it.
The “anaemic model” defined by Martin Fowler is an anti-pattern. It is no problem to develop simple small systems using transactional scripting. The business logic is complicated, and the business logic and various states are scattered in a large number of functions.
Although we use Java as an object-oriented language for development, it is the same as a procedural language, so in many cases it is better to use database stored procedures instead of Java write logic (ps: Spring Boot is not microservices).
Development patterns for domain models
Domain models encapsulate data and behavior and map to real-world business objects. Each class has a clear division of responsibility that allows logic to be distributed to the appropriate objects. Such objects are known as “hyperemic models”.
In practice, we need to clarify the concept that the domain model is stateful and represents something that actually exists. Continuing with the above example, we design the Stock object to represent the actual inventory of an item, and add the methods of business logic to this object.
To do this, you must restore Inventory from Repository using the primary key load, and then execute the corresponding lock(num) method to change the Inventory state. The object is then persisted to the store via the Repository save method.
Application provides the interface to integrate the operations described above:
The most important aspect of the domain model development approach is that it encapsulates the business logic by placing the details of state changes caused by deductions into the Inventory object.
The lock method of the Application object can be contrasted with the lock method of the StockService of the transaction script method. The StockService knows all the details and changes when the inventory is zero (for example, it can be deducted). The Application approach doesn’t need to change; it just evaluates inside the Inventory object. The code is in the right place, the computation is at the right level, and everything makes sense. This kind of design can make full use of all kinds of OOD, OOP theory to realize the business logic is very beautiful.
Disadvantages of the hyperemic model
From the above example, the load in Repository to the execution of the business method to the save back can take some time, but if multiple threads request the Inventory lock at the same time, it can lead to inconsistent state. The trouble is that concurrency against the Inventory is not only difficult to handle, but also common.
The anemia model completely relies on the support of the database for concurrency, which can be simplified a lot, but the congestion model has to be realized by itself. No matter by locking objects in memory or using the remote locking mechanism of Redis, it is more complex and less reliable than the anemia model, which is the challenge brought by the congestion model. A better approach is to eliminate concurrency through an event-driven architecture.
Relationship between domain model and microservices
The implementation of the domain model was mentioned above, but how does it relate to microservices? In practice, this Inventory is the aggregate root of a bounded context, and we can think of an aggregate root as a microserver process.
However, the problem comes again, the Inventory of an Inventory must be associated with the commodity information, and it is not enough to rely on the commodity ID of the redundant point in the Inventory, and the state of goods on and off shelves and so on are required by the business logic. Isn’t that the introduction of heavy objects like commodity Sku into this micro-service? Two heavy objects in one service? Such micro services can not be removed, or must rely on commodity banks? !
Microservices and event driven
We took a domain-driven approach to development, used the congestion model, enjoyed its benefits, but had to face its drawbacks. This disadvantage is amplified by distributed microservices architectures.
Transaction consistency
The problem of transaction consistency was not a huge problem under Monolithic, but was fatal under microservices. Let’s review the so-called ACID principle:
1. Atomicity — Atomicity, where changing data states either happens together or fails together
2. Consistency – Consistency: The data status is consistent
3. Isolation-isolation lines, which do not affect each other even if there are concurrent transactions
4. They are persistent, irrevocable once a transaction is committed
In the case of singleton services and relational databases, it is easy to achieve ACID by database properties. But once you split the aggregate root-microservices architecture according to DDD, their database is already separated, and you have to deal independently with distributed transactions to satisfy ACID in your own code.
For distributed transactions, people usually think of the old JTA standard, 2PC two-stage commit. I remember being in the Dubbo group and being asked almost every week when Dubbo supported distributed transactions. In fact, according to the CAP principle in distributed system, when P(partition tolerance) occurs, the forced pursuit of C (consistency) will lead to (A) decreased availability and throughput. At this time, we generally use final consistency to ensure the AP capability of our system. Of course, this does not mean abandoning C, but CAP can guarantee data consistency in general. In the case of partitioning, we can guarantee data consistency through final consistency.
For example, an order is placed in an e-commerce service and inventory is frozen. Need to confirm whether the order is clinched according to the inventory situation.
Suppose you have adopted a distributed system, where the order module and the inventory module are two services, each with its own store (relational database).
In one database, one transaction can handle two table changes, but in microservices, this is not possible.
In the DDD philosophy, a single transaction can only change the state within one aggregate, and if state consistency is required between multiple aggregates, then final consistency is required. The order and inventory are clearly aggregations of two different bounded contexts, where the ultimate consistency requires an event-driven architecture.
Event-driven ultimate consistency
The event-driven architecture synchronizes state between domain objects through asynchronous messages. Some messages can also be published to multiple services at the same time. After a message causes synchronization for one service, another message may be sent, and events may spread out. Strictly event-driven, there is no synchronous invocation.
Example: After the Order service adds an Order, the status of the Order is “enabled”, and an Order Created event is issued to the message queue:
After receiving the Order Created event, the Inventory service subtracted the saleable Inventory from one of the SKUs in the Inventory table, increased the Order occupied Inventory, and then sent an Inventory Locked event to the message queue:
The Order service receives an Inventory Locked event that changes the status of the order to Confirmed.
Someone asked, what if the inventory is low and the lock is unsuccessful? Simply, the inventory service sends a Lock Fail event, and when the order service receives it, it sets the order to “canceled.”
Good news! We can do without locks!
One of the great advantages of event-driven is that concurrency is eliminated and all requests are queued in, which helps us to implement a congestion model without having to manage locks in memory ourselves. Locking is cancelled, queue processing is efficient, and event-driven can be used in high-concurrency scenarios such as panic buying.
Yes, the user experience has changed.
With this event-driven, the user experience may be changed. For example, when there is no inventory in the original synchronization architecture, it will immediately tell you that you cannot place an order if conditions are not met, and no orders will be generated. But with the change to the event mechanism, the order is generated immediately, and it is likely that the system will notify you later that the order has been canceled. Just like buying “Xiaomi phone”, hundreds of thousands of people are waiting in line for a long time to tell you that they are out of stock, come back tomorrow. To obtain results immediately, you can use a lock such as CountDownLatch on BFF (Backend For Frontend) to convert Backend asynchrony to front-end synchronization. BFF consumption is high.
I can’t. The product manager won’t accept it.
The product manager said that the user’s experience must be that there is no inventory and no orders will be generated. This scheme will constantly generate cancelled orders. What should he do? Skip the order with cancel status when querying the order list, perhaps with an extra view. I’m not an idealist, solving the current problem is at the forefront of my mind, we designed microservices originally to solve the business concurrency. But now we are facing the problem of user experience, so the architectural design also needs to compromise :(but at least after the analysis, I know where I compromise, why I compromise, and there may be changes in the future.
Join query for multiple domains and multiple tables
Personally, I think the aggregate root mode is particularly suitable for modifying the state, but it is really inconvenient for searching data, such as screening a batch of qualified orders, and the aggregate root object itself cannot undertake the batch query task, because it is not its responsibility. You have to rely on facilities such as Domain services.
When a method is inconvenient to place on an entity or value object, using a domain service is the best solution. Make sure the domain service is stateless.
Our query tasks are often complex, such as querying a list of items and asking us to sort them by their sales in the previous month; Sort by return rate and so on. However, after microservices and DDD, our storage model has been removed, and the above queries are all related to the data of orders, users and goods. How to do? Now we’re going to introduce the idea of a view. For example, the following operation, query user name order, directly call two services in memory join efficiency is undoubtedly very low, coupled with some filter conditions, paging, can not do. Therefore, we broadcast the events, and a separate view service receives these events and forms a Materialized view. These data have been joined and processed, and placed in a separate query library, waiting for query. This is a typical space-for-time processing mode.
After analysis, most of our query tasks can be placed in a separate query library, which can be either the ReadOnly library of a relational database or the NoSQL database, except for simple query based on primary key Find or List without much association. We actually used ElasticSearch as a special query view in our project, and it worked pretty well.
Bounded Context and data coupling
In addition to the problem of multi-domain join, we often encounter some scenarios in business. For example, commodity information in e-commerce is basic information and belongs to a separate BC, while other BCS, whether marketing service, price service, shopping cart service or order service, all need to reference this commodity information. But the required product information is only a small part of the whole, marketing services need the id and name of the product, off and off the shelf state; The order service requires an item ID, name, catalog, price, and so on. This is a small subset of the product center that defines an item (item ID, name, specification, specification value, details, and so on). This illustrates the same term in different bounding contexts, but with different concepts. This problem is mapped to our implementation. Every time we directly query the commodity module in the order and marketing module, it is definitely not appropriate, because:
The commodity center needs to accommodate the data required by each service and provide different interfaces
Concurrency is bound to be large
The coupling between services is serious, and the impact of an outage and upgrade is large
This last one, in particular, severely limits our ability to take advantage of the microservices offer of “loose coupling, where each service can be upgraded frequently on its own without affecting other modules.” This requires us to decouple the coupling through an event-driven approach, with some data being appropriately redundant to different BCS. Such coupling is sometimes achieved by embedding Value objects into entities, which is redundant when entities are generated. For example, when orders are generated, commodity information is redundant. Sometimes it is through the way of additional Value Object list, the marketing center redundant part of the related commodity list data, and at any time pay attention to monitor the upper and lower status of the commodity, synchronous replacement of the commodity list in the context of this limit.
The following is an analysis of the ordering scenario. In the e-commerce system, we can think that members and commodities are the basic data of all businesses, and their changes should be released to all fields through broadcasting, and each field retains the information it needs.
Ensure final consistency
The ultimate consistency success depends on many factors
1. Depending on the reliability of message delivery, system A may change its status and the message may be lost when it is sent to system B, causing the status of SYSTEM AB to be inconsistent.
2. Depending on the reliability of the service, if system A changes its status but hangs up before sending A message, the status will be inconsistent.
I remember that JMS in the JavaEE specification has requirements for dealing with these two kinds of problems. One is that JMS ensures the delivery reliability of messages through various acknowledgement messages (Client Acknowledge, etc.), and the other is that JMS delivery operations can be added to the database transaction – that is, no message is sent. It will cause a rollback of the database. There are very few JMS compliant MQS out there, especially since consistency requires reduced performance, and high-throughput MQ is now relegating the problem to our own applications. So here are a few common ways to improve the final consistency.
Using local transactions
Or an example of credit deduction for the order above:
Order service starts local transaction, first add order;
The Order Created Event is then inserted into a special Event table, and the transaction is committed;
There is a separate timed task thread that periodically scans the Event table and dumps what needs to be sent to MQ, setting events to “sent”.
The advantages of the scheme are:
If the Event is not inserted successfully, the order will not be created. Setting the event as sent after the thread scans also ensures that the message will not be missed (the goal is to prefer reposting to missing, since event processing is designed to be idempotent).
The disadvantage is that:
Event publishing needs to be handled separately in business logic, which is tedious and easy to forget. Event sending is lagging; Periodic scanning consumes a lot of performance and may cause high water levels in the database. We can use MySQL Binlog tracking (Ali’s Canal) or Oracle’s GoldenGate technology to get change notifications from the Event table of the database, so as to avoid scanning through scheduled tasks.
However, tools that use these database logs can be tied to a specific database implementation (or even a specific version), so be careful when making decisions.
Use Event Sourcing
Event traceability is a special idea for us. Instead of persisting the Entity object, it only records the initial state and events of each change, and restores the latest state of the Entity object in memory according to the events. The specific implementation is similar to the implementation of Redolog in the database. It’s just that he puts this mechanism in the application layer.
Although event sourcing has many claimed advantages, it should be introduced with great care. First, it may not be suitable for most business scenarios, and efficiency is indeed a big problem when there are many changes. Other query problems are also troubling.
We are only using Event Souring and AxonFramework tentatively for a few businesses, but because of the complexity of implementation, we will have to wait for some time to summarize the details, perhaps an additional article will be needed to describe them in detail.