PragTob

PragTob

You may not need GenServers and supervision trees

Hey everyone,

this has been brewing in my head some time and it came up again while reading Adopting Elixir.

GenServers, supervisors etc. are great technologies that help you solve problems. They’re one of the things that is most special about elixir/Erlang. As a result lots of conference talks, blog posts etc. focus on them and it seems everyone wants to use them.

However, do you need them all the time? At least while using a framework (like Phoenix), chances are you don’t. Of course, until you got a problem that these help you solve.

Building a relatively standard CRUD web application with Phoenix? No need.
Just using channels for a chat like applications in Phoenix? You’re good.

The hidden detail of course is that you are using GenServers and friends without even knowing it - Phoenix runs every request and every channel in their own processes. Ecto uses poolboy for your database connections. It’s already parallelized and you don’t need to take care of it. That’s the beauty of it. What I’m saying is that in the standard situation the eco system takes care of you.

Why am I picking up this topic?
It feels like we talk so much about GenServers etc. that people who come to Elixir feel like they need to use them or they are not “really” using Elixir. I hear people say something to the tune of “We’re still using this like Rails - we should use GenServers” - without any need (granted they mostly don’t know what Phoenix & friends already do under the hood). At worst (as I’ve seen in some questions here) people create a single GenServer that then essentially all traffic needs to go through complicating their code while also adding an unneeded bottleneck. Maybe they just complicate their code, that’s also bad.

To get back to “Adopting Elixir” an example from it:

A new developer team started building their Phoenix applications.
They had always heard GenServers could be treated like microservices but even
tinier. This “wisdom” led them to push all of their database access control to
GenServers .
(…)
performance was abysmal. Under high-enough load, some pages took 3 sec-
onds to render because they built a bottleneck where none existed. They
defeated Ecto connection pools because all access happened through a single
process.
In essence, they made it easy to create global, mutable variables in Elixir. They
essentially crippled the single biggest advantage of functional languages, for
no gain whatsoever.

Which is also what I’ve seen around a bunch of times. The book also provides some guidance as to what to best use GenServers for:

  • Model state accessed by multiple processes.
  • Run multiple tasks concurrently.
  • Gracefully handle clean startup and exit concerns.
  • Communicate between servers

So, what do I want in the end?

Well, I want to discuss with you all about this and hear your opinions!

I think we should make it clearer that you don’t have to use GenServers and that doing so might actually be harmful. My 2 production applications include no single GenServer written by us. They run fine. In general the eco system takes good care of you so you’re using them without realizing it (which is good imo).

I’m not saying you shouldn’t learn about GenServers. You should. But know when to use them and when not to.

Lastly, if you disagree I want you to scream at me and teach me the error in my ways :smiley:

Most Liked

josevalim

josevalim

Creator of Elixir

So you are using a mostly IO bound workload to profile the CPU? Not maxing out the CPU may actually be a good sign. Because you are being vague on the details, I am free to interpret your data like this:

  • C# had to serve less requests because it maxed out the CPU
  • Elixir was able to serve more requests and have spare CPUs

If that’s the case, I will pick the second, thank you.

That’s why when talking about benchmarks, we need numbers and methodology. There are about hundreds of things that could go wrong. Or even when the measurement is right, we take the wrong conclusions. So unless you can provide applications, benchmark tools and methodology, there is nothing to conclude and nothing to discuss.

Can you please provide an actual example? Please let us know your OTP version and OS too. In literally years benchmarking Elixir applications with tools like wrk, I did never bring it down. Even when opening 2 million connections - where we used 40 different client machines to benchmark a single server.

It is not about bad requests bringing down the server but how you semantically react to those. I have literally seen frameworks and libraries rescuing OutOfMemoryError and putting systems in an unmanageable state because of that.

Still, focusing on bad requests is a gross misrepresentation of what fault tolerance means on Elixir. It is also drastically undervalues the benefits of process in designing those systems. Some examples:

  • Ecto being fault-tolerant means safer design around connection pools (and leaking of connections)
  • Phoenix being fault-tolerant means we can easily multiplex multiple channels over the same websocket connection and save on system resources, while also scheduling on CPU and IO bound work
  • Ecto being built on top of processes grants an excellent amount of visibility into the system. You can navigate process trees, inspect the pool memory, state, queue size and more, etc
  • Phoenix being built on top of processes means no stop the world GC and per process garbage collection

And the list could go on and on.

You did not. You made vague statements. “It isn’t fast”. For what? Compared to what? You said it “doesn’t maximise the CPU” but you didn’t provide an example workload. It fails during benchmarking? How? What errors? Under which scenarios?

Yet we see companies using it for data processing with GenStage and Flow. Or for the web with Phoenix. Or for embedded devices with Nerves. I recommend folks to look at videos from conferences such as Empex, ElixirConf, CodeBEAM and others to learn more about the variety of use cases BEAM is deployed to.

cmkarlsson

cmkarlsson

Because this is an elixir forum I’ll come to the rescue of the BEAM and counter some of your arguments :smiley:

If you can’t saturate nearly 100% there is something wrong with the system somewhere. I.e you have a GenServer bottleneck, IO bottlebeck, NIF/BIF bottleneck somewhere. The BEAM overhead is not that much

The order of magnitude can be correct, but then we should be talking CPU expensive tasks which have not been correctly off-loaded to a port/NIF or some micro-benchmarking. From my experience working in go, java and erlang I get pretty comparable numbers on real world applications.

Yes, erlang is slightly slower than the other two, but we are talking 10-20% (sometimes up to 50%) here but not order of magnitudes. And I’ve had bottlenecks in the other languages too making them not being able to utilize 100% CPU something especially go should be good at.

If you are stress-testing and overloading the SUT this is my experience too in the first iterations. When stress-testing there is always some component that can’t handle it and in the BEAM this may lead to rapid restarts of the supervision trees and crash of the runtime. Java seems to stay up longer but in practice is not doing much useful work at those loads. For the BEAM you can usually find these places and put up guards around it to make sure the traffic is dropped (for example) before reaching those parts. Any system or runtime will have these problems when overloaded for periods of time.

On the other hand, in practice, if you put nearly 95% load on the system, what I see is that the BEAM gives you much more consistent latency, especially compared to java.

I agree if you look at the basic web system, the BEAMs fault tolerance doesn’t give you much advantage. This is because HTTP is stateless, whereas BEAM is designed for stateful applications.

However a system is more than that. Database servers, message queues, notification servers, statistics collecting, communication with other external systems. anything that requires some sort of state and the BEAM is so much easier to work with, and if one of those parts crashes it doesn’t affect anything else in the system. Especially now when web-sockets and stateful connections are becoming more prevalent BEAM languages has a big advantage. It makes it much easier to isolate and write robust components in erlang/elixir (which perhaps is your point)

For the thread in general. I came to erlang from java and python and I also could not initially see the advantages or how to work with the BEAM to make the most out of it. I used processes and gen_servers and similar just for the sake of it usually with bad and results and awkward code. I think my problem was that I looked at things the wrong way. I had this amazing tool in the BEAM and I was trying to apply it everywhere. Therefore I think the original poster is correct. You may not need GenServers and supervision trees and you should not try to force the BEAM tooling onto a problem just for the sake of it.

Instead you should get as much information, read as much material, and practice to write systems in OTP as much as possible. Then you will see where it is needed and how it can be applied. I’ve also noticed in the elixir community a much larger willingness to use external libraries than in for example erlang (perhaps because there aren’t many libraries there :wink: ). These external libraries make use of OTP in the best way and all you need to do is glue these components together. You get all the benefits of BEAM without doing things yourself. The risk is, if you don’t understand the tooling you don’t know what trade-offs you are making, you don’t know if a 3rd party library is well designed and many times the 3rd party library is not needed at all. We learn all the time and as you progress it will be easier to see these things.

sasajuric

sasajuric

Author of Elixir In Action

I pretty much agree with what you wrote. That said, I think that GenServer/supervision trees are the pieces people should learn about, because in my experience they are great solutions in many cases, and I’ve yet to see a production which didn’t need a GenServer nor some form of supervision tree fairly early on in the game.

With a lot of hand waving, I’d say that GenServers are OTPs built-in building block for building responsive services, Tasks are the same for non-responsive ones, and supervision tree is the built-in service manager like systemd or upstart. In the past 10+ years of my backend side experience, I’ve worked on small to medium systems, and all of them needed all of these technical approaches.

So I guess my point is that while OTP abstractions can be misused (and they frequently are), they are also very useful, and in my experience very frequently needed. I’ve tried to provide some examples of both functional and concurrent design in my To spawn or not to spawn? article. In particular, in that fairly simple example I already use a couple of GenServers and Supervisors to separate the runtime activities, and I don’t think it’s overengineered.

But I ultimately agree with you that with the ecosystem evolving, there’s less need to write GenServers ourselves, since many common cases can be covered by 3rd party libraries, such as Phoenix, Ecto and others.

Where Next?

Popular in Discussions Top

blackode
Elixir Upgrading is so Simple in Ubuntu and It worked for me Ubuntu 16.04 git clone https://github.com/elixir-lang/elixir.git cd elixir...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
cvkmohan
The upcoming Phoenix 1.6 release looks very interesting. Became a habit to watch the commits - and - what they are bringing in. phx.gen...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
jer
I’ve been using umbrellas for a while, and generally started off (on greenfield projects at least) by isolating subapps based on clearly ...
New
ben-pr-p
In general I’ve been sticking to this community style guide GitHub - christopheradams/elixir_style_guide: A community driven style guide ...
New
tomekowal
Hey guys! I want to create a toy project that shows a chart of temperature over time and updates every 5 seconds. I feel LiveView is per...
New
wmnnd
The Go vs Elixir thread got me thinking: Would it be too hard to implement a simple mechanism for creating Go-style static app binaries f...
New
scouten
I’m looking for a host for the server part of a small (personal) side project that I’m working on. It’s currently written in Node.js and ...
New
Owens
Hello all, I am developing a new mobile app with Flutter frontend and Phoenix backend. The mobile app has real-time task management and c...
New

Other popular topics Top

WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36432 110
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54250 245
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement