mtrudel

mtrudel

Creator of Bandit

What do you think could be the best way to garbage collect in Bandit?

Bandit author here.

We’re currently working up a solution to the oft-reported issue whereby Bandit’s memory consumption increases over time when HTTP/1 connections are kept open via keepalive (an issue exacerbated by load balancers, who will reuse a single connection for a LONG time). Because Bandit uses the same process that is handling the TCP connection to run the Plug stack for each subsequent HTTP request, the end result is that a single process may end up handling any number of HTTP requests, and memory usage balloons as a result.

Thanks to the tireless efforts of @ianko, we’ve managed to isolate a couple of approaches, which we’re discussing here. The gist of it is that we have two solutions which are roughly equivalent in terms of efficacy:

  1. Explicitly call :erlang.garbage_collect() between every separate HTTP request on a single connection.
  2. Set fullsweep_after: 0 on the handler process in order to make every minor sweep be a full sweep of the old heap.

@ianko has provided pretty exhaustive evidence that these two approaches solve the problem and both have a negligible effect on performance. However, if you look towards the end of the discussion, you’ll see something that surprised me; the fullsweep_after approach actually ended up using significantly more CPU to accomplish the same outcome.

So, my questions to any VM wizards in the audience are these:

  1. Do you have any guidance about preferring to use explicit GC calls vs tuning fullsweep_after and letting the VM figure it out? Explicitly trying to do the VM’s job for it seems heavy handed, but the evidence seems to suggest it’s the more performant option in this case. Advice welcome.

  2. Any advice about how to tune either approach? On the explicit GC side, I’ve defined a config option to specify we should only GC every ‘n’ requests, but this again feels like a pretty coarse way to do the VM’s job. On the fullsweep_after side, we could use values other than 0, but picking and choosing values for this feels like stabbing in the dark.

I’d love any advice y’all are able to provide on this.

Most Liked

garazdawi

garazdawi

Erlang Core Team

To me, doing a manual garbage collect seems like a good idea for this type of scenario. It will be much easier for the application code to know when it is a good time to do a GC then it is for the system. Another solution would be to spawn a process per request (as mentioned before in this thread), but that has other tradeoffs in performance and memory usage.

I’m not surprised that fullsweep_after is more expensive as it removes the old generation of the heap, which means that any long lived data will be copied in each GC, while if you do it manually, that data will only be copied when the manual GC is done.

Speaking of the old heap, maybe it would make sense for you to only trigger a minor gc? If you call :erlang.garbage_collect(self(), [{:type, :minor}]) it will only collect the young generation, and maybe that is enough? Or you could try to couple that with setting fullsweep_after to some low value that is not 0. Very hard to know what will be effective as it depends a lot on what the process is doing.

mtrudel

mtrudel

Creator of Bandit

Bandit 1.3.0 just went out with the explicit GC fix mentioned here (and a default of GC’ing every 5 requests). I’m not committing to any stable public interface into this yet; the relevant config option to tune this is marked ‘experimental’ as I reserve the right to change how we accomplish this based on feedback (I’m planning on revisiting this somewhere in the second half of 2024).

Feedback / real world experience with this change is welcome!

dimitarvp

dimitarvp

I am against pokes in 99% of the cases but this time I think it’s justified if we summon @rvirding, @garazdawi and @bjorng. Sincere apologies to them if I am mistaken.

If you can’t make this problem go away I’d actually think about doing a double fan-out i.e. have these processes spawn other, much shorter-lived processes, each of which represents a single request, whereas the spawning processes represent connections – and you said they are prone to be long-lived due to keep-alive policies which, ahem, spawned this problem in the first place.

You can also minimize latency there by keeping a pool of pre-spawned several sub-processes for each connection process, and expand that pool in conditions of heavy load.

BTW when you said in OP that both your suggested approaches only introduce minimal latency, how much % we’re talking? Also what absolute numbers? I’ve looked at the graphs in the GitHub thread but I can’t intuit much from them (i.e. in some of them the latency looks like +20% more, but in most it looks like there’s no difference?).

Though I’ll agree with some of the commenters in the GitHub thread that even if explicit GC works it still feels like a hack / workaround. :confused: But this is the real world, we have to do compromises. As a guy involved in a greenfield project where I chose Bandit over Cowboy I wouldn’t be against the explicit GC as a final solution if nothing else turns up.

sorentwo

sorentwo

Oban Core Team

Have you considered using Process.hibernate rather than triggering an explicit sweep?

Bump the “GC after” config to a less frequent value, say 50-100, and trigger a hibernate. That will still cause a GC and minimize memory if the connection goes unused briefly.

A similar approach has worked well for pubsub heavy processes that accumulated binary garbage in Oban.

derek-zhou

derek-zhou

How about just quit after serving a preset number of requests? like a thousand. The reverse proxy or load balancer will reconnect and give birth to a new process. Calling :erlang.garbage_collect() feels like tuning to a particular behavior of the VM. On the other hand, quitting after serving its term has been used since forever, Apache still does this.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
Kagamiiiii
Student & New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement