Sober Look at Microservices
Microservices limit the number of possible architectural choices.
This article is not attempt to make reader choose or not choose microservices. Intead I’m trying to fill some gaps in the microservices-related information so reader may make informed decision.
It's hard or even impossible to ignore microservices hype. Every day one can see more and more new articles describing how good and advanced microservices, that everyone who does not do microservices should start doing them immediately, which patterns to use and which avoid, and so on and so forth.
This flood of shameless microservices propaganda leaves very little room for objective analysis of advantages and disadvantages of "microservices architecture".
Perhaps, it's worth to start from explanation, why in last sentence "microservices architecture" provided in quotes. The answer is pretty simple — because there is no such thing as "microservices architecture". There also no such thing as "monolithic architecture" either. For many microservices-based applications, it is possible to repackage them into a single deployable artifact just by changing build/packaging options/configuration.
The resulting "monolith" application will have identical architecture, but will not be microservice-based anymore. Hence, microservices/monolith are just a packaging options from the point of view of architecture.
Of course, use of microservices affects architectural decisions, but this does not mean that microservices are architecture. For example, software stack also affects architecture decisions, but nobody tries to call it "architecture".
Curious observation: this is not always possible to do a transition from single to multiple deployable artifacts, or "break the monolith". Not necessarily because "monolith" is poorly designed or implemented (this also happens, but it's another story). The actual reason is that internal architecture might be not service-based. For example, Hexagonal Architecture is hard or impossible to refactor into service-based architecture. Another possible cause — for business reasons, it might be impossible to split domains into services. After all, applications are created to solve business problems, not vice versa.
Actually, if you take a look at all those tons of articles dedicated to transition to microservices, you might notice that the only choice of architecture you have is a specific variant of Service-Oriented Architecture with several microservice-specific patterns applied. In other words, microservices limit the number of possible architectural choices.
It also worth to mention some microservices-related patterns:
- Domain-scoped services
- Single DB per service
- Communication between services via unreliable channels and barely suitable protocols like HTTP (there are other options, but they're very rarely even mentioned, even less discussed)
- Traditional ACID transactions are replaced with Saga pattern
Most of these patterns attempt to solve problems which are brought in by microservices and do not exist in traditional systems. And most of them have their own issues.
In return, microservices proponents promise numerous advantages like separate deployment, easy scaling, failure isolation and fault tolerance, independent development of services, improved testability, etc.
I'm going to analyze these advantages one by one, but it is worth to look at the high level picture of the microservices-based system first.
Microservices and System Complexity
Complexity of each software system depends on two main parts: number of components and number of links between components.
Usual approach is to reduce complexity as much as possible, because it has far fetching consequences of the system and organization which uses it.
This is not the case for microservices: every microservices-based system looks like a traditional application turned inside out — all internal dependencies between services are exposed to the external world. In addition to pre-existing internal complexity, this transition added few more sources of complexity (in no particular order):
- Increased response time (achieving SLA is more complex).
- Each invocation of other service may fail.
- Data scattered across services, complicating access and often causing duplication and/or much more complex retrieval/processing
- Any transaction which crosses service boundary now gets much more complex.
- Infrastructure used to deploy individual services now is an integral part of the system.
- Each service need to be configured.
Usually, when complexity is increased, this is justified by advantages provided by increased complexity. For example, modern car is far more complex than Ford Model T. In return we got better performance, comfort and safety. Microservices bring a lot of additional complexity, but what we get in return?
Separate deployment is an inherent property of the microservices — they just can't be made ready together at once. Until all services are ready, the whole system is non-functional.
Re-deployment of one service also makes the whole system non-functional unless service is put behind load balancer and there are more than one instance of service running.
Usually it is advertised that separate deployment enables possibility to update services independently. This indeed an advantage if traditionally packaged application takes too long to start. But if is not, there is no sensible advantage in the separate deployment.
Separate deployment has its own downsides as well:
- Application of changes which cross service boundaries is much more complicated.
- API changes require special attention and management.
In theory, a microservices-based system can be scaled by adding more instances of the necessary service. In practice, it does mean that each service, which we're going to scale, need to be put behind the load balancer. This, obviously, means more infrastructure configuration and maintenance.
But the complications didn't end here. As mentioned above, one of the microservices patterns is "Single DB per service". If more than one instance of the service need to be launched, there are three possible choices:
- Use independent DB’s per instance
- Use sharding
- Use same DB
The first choice is usually not an option since instances need to be identical from the point of view of external user/service. If they use independent DB's, then there is no simple way to ensure data consistency.
The second option is more interesting. Sharding is a well-known and widely used technique to scale databases. The problem here is that all shards need to be accessible to make sure all data is available.
This means that there is no simple way to replace a single shard or reconfigure shards dynamically. In other words, while this option is possible, it does not allow dynamic scaling as advertised.
Finally, the last choice just shifts the bottleneck from service to DB. Unfortunately, this makes sense only in a narrow number of cases. Usually, a properly designed and implemented service is I/O bound (in particular, DB I/O), so scaling by adding new instances will not improve performance. So, this approach makes sense only if it does some heavy computations or written with popular resource hogs like Spring.
Let's take a look at the microservice scaling in general. It appears that microservices-based systems assume only one way of scaling — by launching more instances, with all relevant operations expenses.
There are numerous other options, which are not available for microservices, for example, scaling by shifting load to other containers, which are not so heavily loaded. None of them available for microservices. As a consequence, scaling options not only limited, but even available resources are used inefficiently.
Failure Isolation and Fault Tolerance
In theory, microservices enable failure isolation — when one service fails, the remaining continue to work.
In reality, a partially working system not always what is actually needed/desired because it is prone to data loss or corruption. Traditional desing is to make application stop when it meets unrecoverable failure. While this might make the system less available, at the same time it makes the system much more resistant to data loss and corruption.
It's also worth to keep in mind that the number of possible failure scenarios is quite limited (out of memory, no disk space, etc.) and barely changes as the system evolves.
So, to prevent data loss/corruption, the microservices-based system should be designed with possible failure scenarios in mind. All of them.
Microservices is a distributed system, so, beside well known traditional failures, there are communication failures which need to be taken into account. Adding a new microservice increases the number of possible failure scenarios. Worse is that, the whole failure handling happens at the business logic level, i.e. developer ought to keep in mind possible failures while writing business logic. This clearly violates the separation of concerns principle, and this issue is inherent to microservices.
Another possible approach is to deploy services into service mesh and launch few instances of the service. This will solve, or, at least, mitigate, many of the mentioned above issues.
Unfortunately, this approach is quite expensive in regard to efforts and operational expenses. Worse is that this approach provides no guarantees of failt tolerance. After all, if some specific load/request caused the failure of one instance, most likely it will cause the failure of other instances as well.
Another important question: should we consider a system able to survive failure of one part (service) fault tolerant? According to common definition, to be considered fault tolerant, the system must be able to continue operation after failure, perhaps with reduced throughput and/or increased response time. According to this definition, microservices-based systems have no inherent fault tolerance — failure of one service makes the system incapable to handle requests.
Independent Services Development
It should be noted, that this property is not technical. It's purely organizational and therefore can be applied to any type of system. Nevertheless, microservices should be credited for much wider acceptance of Domain Driven Design.
Also, microservices leave no room for "shortcuts", which usually possible in traditional design. Overall, this makes design of the whole system much more clean.
But, again, this property is not unique to microservices. It is still possible to use similar approaches to design and implementation of other types of systems.
Reliance on the Infrastructure
As mentioned above, infrastructure is an integral part of any microservices-based system. This fact has several implications:
- Infrastructure complexity. Each service is unique and has its own dependencies and configuration.
- There is no way to launch the system without infrastructure.
- The system is usually tied to a single cloud provider. Change of provider is possible, but quite expensive.
- Operational expenses are significantly higher.
- Attempt to adopt microservices without skilled operations team almost inevitably doomed from the very beginning.
At some point infrastructure complexity grows so much that it is necessary to introduce even more infrastructure managament tools, like service meshes.
Few curious observations:
- Average microservices-dedicated article tries to avoid the fact that in order to transition to microservices, organization must have well established and skilled operations team.
- Microservices require fault-tolerant and reliable infrastructure in order to operate (like Kubernetes or cloud provider), while they are not fault tolerant itself. It feels wrong, that we build unreliable systems on top of reliable ones.
In theory, since each service is relatively small, testing is simpler and easier. Unfrotunately, this claim compares testing of subsystem with whole system test.
Freedom in Software Stack Choice
In theory, each team responsible for particular service, may freely choose software stack for implementation.
In practice most large organizations trying to limit number of choices as much as possible, because supporting diverse stacks is inefficient and expensive in the long run.
Let's Sum Up
Almost every aspect of microservices increases complexity of system design, implementation and operation. The only real benefit — ability to develop services more or less independently, but this benefit is not specific to microservices.
Numerous "success stories" of switching to microservices attribute success to microservices. Unfortunately, they miss the key point — transition to microservices forced them to change and/or cleanup design of original system.
In other words, there are two changes — packaging (monolith/microservices) and design. None of these "success stories" did comparison of new design with different packaging. Why success is attributed to packaging but not the design — I don’t know.
Who Benefits from Microservices Hype?
One who did read this far, perhaps noticed, that two things are mentioned most often — complexity and infrastructure. And infrastructure complexity. Microservices are not just increasing but multiplying demand for infrastructure.
Obviously, companies which provide this infrastructure benefit most. In other words, your organization may or may not benefit from transiton to microservices, but cloud providers will definitely make profit on this transition.
Originally published at Medium