I agree that microservices shift complexity, but I'm not sure that this inherently results in an overall increase in complexity. For example, using highly automated ops tools like autoscaling and load balancing can enable smooth handling of problems that can be challenging to handle in code.
Here at Google, one of our most popular microservices frameworks enables microservices to be assembled into servers. If one microservice calls another one in the same assembly, it won't touch the network, and is quite optimized. There's no reason that microservices have to all run in separate binaries, but when it's useful it can be done easily enough without having to change your code.
I find my self doing the opposite. I’ll often create a Cloud Task that will call the same server that created it, just to make sure that another container will get spun up to handle the load if needed. Using Cloud Run this way quite happily for a mix of OLAP and OTAP workloads.
Yes, it shifts communication to over the network, but this comes with the benefit of making system boundaries much clearer. It's been my experience that this can allow different parts to evolve separately and make the choices best for their own needs, which can sometimes result in a paradoxical reduction of complexity as one all-in-one system turns into a series of simpler tools. Suddenly the part of the system that does background ETL work and the part of the system that serves up the admin panel can use different tools.
I will agree that networks are inherently complicated, but your system or systems probably use networks anyway. Using them internally often forces you to grapple with the complexity you were already facing. Plus, it's been my experience that networks are easier for most to reason about than shared memory and mutexes.
Let's not forget providing an easy set of dials to turn to scale a given part of the system. That's a point of complexity, but it can be an immensely useful one. Complexity is not always the enemy - a power drill is more complex than a manual screwdriver and this complexity is pretty useful.
Microservices probably do increase the overall amount of complexity in a sense, but the idea is to trade a little bit of complexity in order to decouple teams. i.e., each team can own its service from soup to nuts without having to coordinate with a bunch of other teams. To put it differently, there’s a small increase in technical complexity in exchange for a significant decrease in organizational complexity. That said, if you go out of your way to retain that organizational complexity (e.g., by dividing your services in a way that doesn’t resemble your org chart) then you’re going to have a bad time with microservices. Similarly, if you don’t have organizational complexity (e.g., you only have one or two teams) then microservices are probably the wrong way to go.
Examining behavior in the limit (a la physics) is interesting. Imagine a system where every function call goes out to a separate process. Suddenly you have a lot of processes waiting around to be called. Debugging requires special tools. Profiling becomes a nightmare. Even determining if the system is fully running becomes difficult. What have you gained?
Most comments about this assumes a poorly designed monolith and a well designed set of microservices. A microservices architecture can be a rats nest too.
I guess what it means is that even if you can build a well modularized system, it will only stay well modularized if you use a network call to enforce it. Well, at least for most companies.
Conceptually, there's nothing keeping you from designing your codebase to work as both microservices or direct calls. I've certainly done it before - each service defined a Java interface, and codegen could hook that up as either a direct call or to route over some kind of layer.
It’s been mentioned elsewhere in this thread, but the lack of microservices doesn’t imply a global shared state. There’s a big difference between a large service that has well isolated modules, and a large service where all the state is contained in one struct, for example. I feel this issue is pitched as a false dichotomy.
If I'm in Java, JavaScript, or Python and there is a code fault, the system provides me a stack trace of the call structure that lead to the error. If I catch the error I can output more related data as I deem necessary. This comes effectively, out of the box.
How do I do a similar stack trace in microservices to understand the path that led to this state? I've used microservices at a couple of companies and their methods were effectively, look at the request id and trace it through this mass of log files for each microservice we are running. It was terrible and could take hours to compile the same information.
What tooling exists to solve this problem with microservices? Genuinely want to know.
> If I'm in Java, JavaScript, or Python and there is a code fault, the system provides me a stack trace of the call structure that lead to the error. If I catch the error I can output more related data as I deem necessary. This comes effectively, out of the box.
Sure, if you are not writing a program using event-driven async style. Every monolith I've worked with has been in async style pretty much.
With global state in the monolith, this can become quite difficult to reason about. By contrast, with microservices, you can analyze the service as performing a small function with a single input and output without global state dependencies. This can be easier to debug.
It seems to me that you are comparing apples and oranges. If your code has no relevant state or coupling, a pure function. Sure, you can look at it in isolation. You could do the same in a monolith.
I've worked on plenty of monoliths with async style code and have found the stack traces to be plenty helpful. It's never been an issue. At least in JavaScript you retain all relevant scoped data and can dump it all if you like.
Debugging gets much more interesting when there are no obvious such errors, yet error conditions are present.
I'm still not seeing any equivalent microservice tooling.
Also, security. Each component has an identity and it’s own set of permissions so the custom emoji widget doesn’t have access to the payments system or the health data.
This would be true in a monolith as well unless configured otherwise. Functions don’t just start talking to each other; you have to explicitly connect them via a function invocation. I’m not sure how an architecture would change that.
It’s pretty straightforward. In a monolith everything is in the same memory space, so the payment system credentials and routines are available to the whole monolith. In a microservice architecture, one service can’t access the memory or routines of other services.
One function cannot access the memory space of another function any less explicitly than one process can access the memory of another process. Either way, you are just picking different contract enforcement mechanisms. The only way this would be helpful is if your whole goal is to make something (breaking the contract) as hard to do as possible. And in that case, I really have to ask, who are your teammates that you trust them so little as to literally enforce separation this way? No healthy organization should have to resort to this kind of civil war.
Minor nit: functions as a service are almost exactly this - and they (from a technical perspective) don’t wait to be called, they pop into existence just in time.
Arguably though it’s not special tools is just different tools. You usually can’t run a debugger live in production but with tools like distributed tracing and service meshes you get really close
Monolith can Microservices are so often presented as “x is better than y”, but it should be “which is more applicable for the team size, product and operational concerns”.
Monoliths are a great choice for certain team sizes and applications, want stricter isolation and blast-radius between different teams and products and need to scale different things differently? Micro services are probably a better choice.
The operations needed to keep a large monolith buildable when a lot of different teams work on it are also highly complex, and increase in complexity relative to the number of contributors to the codebase. It becomes a massive coordination problem at scale.
Microservices decouple teams, so they can get their work done without stepping on each other's toes.