> there is no need to put a network in the middle to do the linker's job.
Absolutely. More people need to understand the binding hierarchy:
- "early binding": function A calls function B. A specific implementation is selected at compile+link time.
- "late binding": function A calls function B. The available implementations are linked in at build time, but the specific implementation is selected at runtime.
From this point on, code can select implementations that did not even exist at the time function A was written:
- early binding + dynamic linking: a specific function name is selected at compile+link time, and the runtime linker picks an implementation in a fairly deterministic manner
- late binding + dynamic linking: an implementation is selected at runtime in an extremely flexible way, but still within the same process
- (D)COM/CORBA: an implementation of an object is found .. somewhere. This may be in a different thread, process, or system. The system provides transparent marshalling.
- microservices: a function call involves marshalling an HTTP request to a piece of software potentially written by a different team and hosted in a datacenter somewhere on a different software lifecycle.
At each stage, your ability to predict and control what happens at the time of making a function call goes down. Beyond in-process functions, your ability to get proper backtraces and breakpoint code is impaired.
I think a trap that many software engineers fall into is interpreting "loose coupling" as "build things as far down the binding hierarchy as possible".
You see this under various guises in the web world - "event-driven", "microservices", "dependency injection", "module pattern". It's a very easy thing to see the appeal of, and seems to check a lot of "good architecture" boxes. There are a lot of upsides too - scaling, encapsulation, testability, modular updates.
Unfortunately, it also incurs a very high and non-obvious cost - that it's much more difficult to properly trace events through the system. Reasoning through any of these decoupled patterns frequently takes specialized constructs - additional debugging views, logging, or special instances with known state.
It is for this reason that I argue that lower-hierarchy bindings should be viewed with skepticism - if you _cannot_ manage to solve a problem with tight coupling, then resort to a looser coupling. Introduce a loose coupling when there is measurable downside to maintaining a tighter coupling. Even then, choose the next step down the heirarchy (i.e. a new file, class, or module rather than a new service or pubsub system).
Here, as everywhere, it is a tradeoff about how understandable versus flexible you build a system. I think it is very easy to lean towards flexibility to the detriment of progress.
This isn't really true though. It's not like you're suddenly adding consensus problems or something, you don't need to reinvent RAFT every time you add a new service.
Microservices put function calls behind a network call. This adds uncertainty to the call - but you could argue this is a good thing.
In the actor model, as implemented in Erlang, actors are implemented almost as isolated processes over a network. You can't accidentally share memory, you can't bind state through a function call - you have to send a message, and await a response.
And yet this model has led to extremely reliable systems, despite being extremely similar to service oriented architecture in many ways.
Why? Because putting things behind a network can, counter intuitively, lead to more resilient systems.
It's still a distributed problem if it's going over a network, even if you don't need consensus specifically.
I don't think you would get the same benefits as Erlang unless you either actually write in Erlang or replicate the whole fault tolerant culture and ecosystem that Erlang has created to deal with the fact that it is designed around unreliable networks. And while I haven't worked with multi-node BEAM, I bet single-node is still more reliable than multi-node. Removing a source of errors is still less errors.
If your argument is that we should in fact run everything on BEAM or equivalently powerful platforms, I'm all on board. My current project is on Elixir/Phoenix.
> replicate the whole fault tolerant culture and ecosystem
I think the idea is to move the general SaaS industry from the local monolith optimum to the better global distributed optimum that Erlang currently inhabits. Or rather, beyond the Erlang optimum insofar as we want the benefits of the Erlang operation model without restricting ourselves to the Erlang developer/package ecosystem. So yeah, the broader "micro service" culture hasn't yet caught up to Erlang because industry-wide culture changes don't happen over night, especially considering the constraints involved (compatibility with existing software ecosystems). This doesn't mean that the current state of the art of microservices is right for every application or even most applications, but it doesn't mean that they're fundamentally unworkable either.
At the language level, I think you probably need at least the lightweight, crashable processes. I don't think you can just casually just bring the rest of the industry forward to that standard without changing the language.
> putting things behind a network can, counter intuitively, lead to more resilient systems
Erlang without a network and distribution is going to be more resilient than Erlang with a network.
If you're talking about the challenges of distributed computing impacting the design of Erlang, then I agree. Erlang has a wonderful design for certain use cases. I'm not sure Erlang can replace all uses of microservices, however, because from what I understand and recall, Erlang is a fully connected network. The communication overhead of Erlang will be much greater than that of a microservice architecture that has a more deliberate design.
Playing a bit of devil's advocate, erlang is more reliable with more nodes by its design because it is expecting hardware to fail, and when that hardware does fail it migrates the functionality that was running on it to another node. With only a single node then you have a single point of failure, which is the antithesis of erlangs design.
> Erlang without a network and distribution is going to be more resilient than Erlang with a network.
I doubt that this is supposed to be true. Erlang is based on the idea of isolated processes and transactions - it's fundamental to Armstrong's thesis, which means being on a network shouldn't change how your code is built or designed.
Maybe it ends up being true, because you add more actual failures (but not more failure cases). That's fine. In Erlang that's the case. I wouldn't call that resiliency though, the resiliency is the same - uptime, sure, could be lower.
What about in other languages that don't model systems this way? Where mutable state can be shared? Where exceptions can crop up, and you don't know how to roll back state because it's been mutated?
In a system where you have a network boundary, and if you follow Microservice architecture you're given patterns to deal with many others.
It's not a silver bullet. Just splitting code across a network boundary won't magically make it better. But isolating state is a powerful way to improve resiliency if you leverage it properly (, which is what Microservice architecture intends).
You could also use immutable values and all sorts of other things to help get that isolation of state. There's lots of ways to write resilient software.
Distributed systems is more than just consensus protocols.
At minimum, you need to start dealing with things like service discovery and accounting for all of the edge cases where one part of your system is up while another part is down, or how to deal with all of the transitional states without losing work.
> Why? Because putting things behind a network can, counter intuitively, lead to more resilient systems
If you're creating resiliency to a set of problems that you've created by going to a distributed system, it's not necessarily a net win.
> At minimum, you need to start dealing with things like service discovery and accounting for all of the edge cases where one part of your system is up while another part is down, or how to deal with all of the transitional states without losing work.
I really don't get this phobia. You already have to deal with that everywhere, don't you? I mean, you run a database. You run a web app calling your backend. You run mobile clients calling your backend. You call with services. The distributed system is more often than not already there. Why are we fooling ourselves into believing that just because you choose to bundle everything in a mega-executable that you're not running a distributed system?
If anything,explicitly acknowledging that you already run a distributed system frames the problem ina way that you are forced to face failure modes you opt to ignore.
> You can't accidentally share memory, you can't bind state through a function call - you have to send a message, and await a response.
These are not strictly a "program on network vs program not on network" issue. "Accidentally sharing memory" can be solved by language design. It is correct that a system that is supposedly designed as inter-actor communication is not ideal when designed and written in a "function call-like" manner, but erlang only partially solves this phenomena by enforcing a type of architecture, which locks away the mentioned bad practice.
> ...despite being extremely similar to service oriented architecture in many ways. Why? Because putting things behind a network can, counter intuitively, lead to more resilient systems.
This is a strange logic. Resilient system is usually achieved by putting a service/module/functionality/whatever in a network of replications, meanwhile service oriented architecture talks about loosening the coupling between different computers that acts differently.
I do agree on microservice != turn function calls into distributed computing problems.
It MAY happen to systems eagerly designed with microservice architecture without a proper prior architectural validations, but it is not always the case.
You're probably right that Armstrong would be too humble to say it, but increased engineering effort is the only conceivable mechanism for unreliable networks to produce more reliable systems. I can't even tell what you think is going on.
In his initial thesis on Erlang, and in any talk I've seen him give, or anything I've seen him write, he attributes reliability to the model, not to BEAM.
You didn’t continue far enough down the hierarchy to find the places where microservices actually work. Honestly if you’re just calling a remote endpoint whose URL is embedded in your codebase and with the hard coded assumption that it will follow a certain contract you’re not moving down the binding hierarchy at all - you ‘re somewhere around ‘dynamic library loading’ in terms of indirection.
It gets much more interesting when you don’t call functions at all - you post messages. You have no idea which systems are going to handle them or where... and at that point, microservices are freeing.
All this focus on function call binding just seems so... small, compared to what distributed microservice architectures are actually for.
Absolutely. More people need to understand the binding hierarchy:
- "early binding": function A calls function B. A specific implementation is selected at compile+link time.
- "late binding": function A calls function B. The available implementations are linked in at build time, but the specific implementation is selected at runtime.
From this point on, code can select implementations that did not even exist at the time function A was written:
- early binding + dynamic linking: a specific function name is selected at compile+link time, and the runtime linker picks an implementation in a fairly deterministic manner
- late binding + dynamic linking: an implementation is selected at runtime in an extremely flexible way, but still within the same process
- (D)COM/CORBA: an implementation of an object is found .. somewhere. This may be in a different thread, process, or system. The system provides transparent marshalling.
- microservices: a function call involves marshalling an HTTP request to a piece of software potentially written by a different team and hosted in a datacenter somewhere on a different software lifecycle.
At each stage, your ability to predict and control what happens at the time of making a function call goes down. Beyond in-process functions, your ability to get proper backtraces and breakpoint code is impaired.