Things that I hate in software #1: microservices
In the recent years, since probably 2018 or so, there is a fad in the software industry: the microservice architecture.
This fad led to several headaches for developers, broken promises and no real benefit, except for cloud vendors, that made money for software that made essentially nothing, except sharing messages. But let's dig it deeper.
Note: these are my opinions and I am available to debate and discuss it politely.
What this article is not
This article is not a criticism for a particular technology. I, myself, am using Spring Boot as a daily driver, although it is used for microservices in the Java world. I think that Spring Boot is a great piece of software and many more technologies are good. I focus on the architecture, not the specific library or framework.
This article is not about serverless architecture, although it follows an architecture similar to microservices. They are totally different and I will write an article later.
A brief history
Microservices start from the ashes of Service-Oriented Architecture (SOA) that was a hype since 1999 to 2005 and involves extensive use of SOAP protocol. But they share the same idea: separate the code to enable modularization. SOA however focuses its architecture on services, exposed via SOAP-HTTP while microservices do not focus on a certain technology, but on the modules themselves.
In 2005 the REST concept started to arise, although it is a bit different to the one we know today. JSON was only one of many representations of resources, while today it is the default. Even HTML was one kind of representation. From the end of 2008, with the arrival of super-fast JavaScript engines like V8 in Chrome and the AJAX increased usage, parsing JSON in client code was feasible. This led to the architecture we see today, where the client code is completely separated to the server code, that sparked the creation of pure front-end developers.
In 2011 the concept of Microservices arised slowly, although the fad started only later, my idea is that it happened in 2018 or so, but feel free to correct me.
This concept is followed by the DevOps movement, they share some concepts with totally different outcomes.
Criticism of the monolith
The microservice movement started with a fundamental criticism of the "monolith", i.e. a derogatory term to indicate the typical development that was made before the advent of microservices.
The "monolith" is a single application that has all the inner workings inside, typically in a single process. Clusters can be made, but with the same code running on different machines.
Why monoliths are bad (for microservices advocates):
So microservices takes the deconstruction path to destroy everything you thought was right at the time, from database interaction to communication with the clients.
Architectural style(s)
The term "microservice" is not clear from the start. But essentially this could be defined as an "application that focuses on specific tasks that is part of a larger architecture with many microservices, communicating with them through message passing".
But new questions arise:
So new technologies arise to solve these doubts.
Various interpretations
If you talk to microservices developers and advocates, you probably will find out different code writing styles. Anecdotal recounts tell of people:
We can go on, every people in microservice architecture will tell his/her story about how doing microservice development "the right way".
Synchronous processing
Synchronous processing among microservices is made through message passing. A technology I've seen that is mostly used is gRPC: it is very fast of an HTTP-based technology, but it is slower than (obviously) simple method invocation and lacks transaction management, as in competing technologies like EJB using two-phase commit.
Asynchronous processing
Asynchronous processing is frequent in microservices architecture, because it is hoped that applications use the CPU better than synchronous calls (although blocking threads do not consume CPU, but this is another story). So a message broker is introduced, with producers and consumer. A typical technology used in this field is Apache Kafka, that shares messages with an impressive speed, but again it is slower than simple method invocation, lacks message send-receive transaction management (if it fails on the receive side, no rollback on the send side can be made), is not good for large messages (RabbitMQ is better in this case, but its speed is not).
Separation of data
As I said before, database can be used in the microservice architecture as a shared resources OR as distinct resources and synchronized through messages.
The latter style is what seems more frequent: each microservice use its own database. If it needs to share data with some other microservices:
This leads to several problems regarding atomicity and redundancy.
Modularization to the extreme
In "monoliths" modularization can be made through the use of language constructs. For example, the typical layered structure in the applications:
Along with other constructs (such as interfaces and implementation in Java and similar languages), this can lead to modularization at the code level.
In microservices, these modules are taken to the extreme: depending on developers, it can arrive to the point where each service has an endpoint that can receive a message (either synchronous or asynchronous), even with different processes.
领英推荐
Working: before and after
Obviously this takes to a different way of development and problem solving strategies. Sometimes you feel that everything you have learnt up to entering a microservice-based project was wrong. I felt this way and it felt so wrong, because it is.
Here I focus on differences between the previous style and the microservice style, so everything that was the same (such as unit testing) is not mentioned here.
Coding
This is what you do in your daily life as a coder.
Before: You create your modules, for example (in Java) service interfaces, implementations, group them in packages, expose them in HTTP controllers.
After: You create your interface, the implementation and expose it as a receiver for a message, unless you determine that a service is purely internal, but that depends on developers.
Running the application locally
Before: start the application, or the application server with the application deployed in it.
After: start 5-20 applications at the same time. And a message broker.
Root cause analysis
Before: you see a stack trace for an error, then you determine the real cause by looking at the stack trace itself. With the help of a debugger you can reach to the fix.
After: you see a stack trace. You try to understand if it is a problem of the microservices raising it, or the real cause is from the caller. Or the caller of the caller. Or the caller of the caller of the caller. To do this you have to look at the logs, for which you added correlation IDs to determine a single flow of messages. Oh and your logging level is TRACE.
Debugging running instances
Before: A ticket is created telling that there is a bug. So you start to see the log for possible problems. You try to replicate the problem locally by running on your machine. Increase the level of logging on the production machine and ask the customer to repeat the process. In the end you solve the problem.
After: A ticket is created telling that there is a bug. So you start to see the log collecting software (e.g. Kibana), hoping that you've put the correlation ID everywhere. So you try to understand the flow, correlating the message on the log with the exact line of code. Then you try to recreate the problem with the same data on your machine, running (in debug) again 5-20 applications with a message broker on your local machine. Put several breakpoints in the various applications, hoping for the best. You solve the problem (with much more time spent).
Atomicity (transactions)
Before: If you wrap your service in a transaction, you are sure that, if something fails, no write has been made.
After: For every action you code, you have to code also the counter-action, in case something goes wrong. Introduce the SAGA pattern, that will make you cry.
Suppose that microservice A does something on the DB then call the microservice B to do something else on the DB. You would like that, in case in microservice B something goes wrong, you want to rollback the actions done in microservice A.
So the flow would be:
Welcome to the reinvention of the wheel!
Misconceptions introduced by microservice advocates
Microservices advocates tell a lot of misconceptions about "monoliths" that in theory they want to solve. Not only they are false, but the solutions are even worse than the thing they want to solve. Let's take some examples.
Scalability - Monoliths do not scale well. Go tell this to cluster provider that was there since the existence of the Java EE application servers.
Speed - Monoliths are slow. So you replace method invocation with message passing, hoping that it will speed up. Spoiler alert: it won't.
Modularity - Monoliths are not modular. This essentially depends on developers: common Object Oriented Programming patterns lets you achieve modularity. Do not confuse modularity with separation of processes.
Deployment - Monoliths need to be deployed altogether. This is in fact true, but it is a good thing: DevOps operations are much simpler to code.
(Broken) promises
They told you it is will be better, they told you it's the right thing to do... No it's not. Let's see all the promises not kept.
Separate deployments: true in theory, but in fact most microservices depend on each other. Because messages must be exchanged between microservices, and if you change the send part, you have to change the receiving part. There is no escape.
(Horizontal) Scalability: abused term to tell that you can create infinite instances of a microservices. This is true, but it is far from useful.
Speed: As a side effect of using multiple instances of microservices, you can control speed of the process. This is not true because speed varies depending on workload and you can achieve potentially a better throughput. The problem is that most of the time is spent sending and receiving messages.
Parallelization: Potentially multiple instances of a microservice can treat large workload in parrallel. The problem is that microservices advocates do not do any kind of study for scalability (as in High Performance Computing) with varying workload, so it's simply an assumption.
Separation among teams: microservices are kept so small that interaction between teams is mandatory. The ideal situation in which each microservice team can work independently to the others, is simply a pipe dream.
Faster data treatment: Most of the time, in microservice architecture, NoSQL database are used. Although they are faster, they sacrifice the consistency for speed. Moreover, all the speedup that you obtain is deleted by the message passing inherent slowness.
Separate databases are better: This leads to lack of atomicity and, most importantly, a large amount of redundancy. A thing that any database teacher would yell at.
Can it be saved?
Not everything about the microservice architecture is bad, some principles are created with the best intentions.
In my opinion, the count of microservices should be reduced a lot. Instead of creating microservices for each module you create, focus on the workload, on needed speed, on possibilities of parallelization. Identify the algorithms of your project, see if they can be split and if they benefit from that split.
Much of the High Performance Computing lessons must be learnt by microservices advocates. Study scalability, with varying nodes, leaving the same amount of workload and with varying workload. Study the Amdahl's law (it's easy), remember that you cannot get a speedup more than what is possible.
And probably, most importantly, before following the microservice path, create a monolith first. In the end you will have a functioning project that can be optimized later.
Software Developer
1 年Great article! You described the pain I've suffered in projects with excessive microservices with no evidence that it brought any positive value like maintainability or performance... But the cloud provider was very happy with our costs
Maintainer @ Passepartout
1 年This dogmatic take on SRP (Single Responsibility Principle), which microservices is a byproduct of, has been intoxicating the field for years. No, not every piece of software can be treated as an island, and exaggerating this approach for the sake of adhering to a trend, brings up all the problems that you mention. Good analysis!