stoyle

stoyle

K8s OOMKilled pods

Hey. Been searching the interwebs and the forums, but don’t seem to find an answer to this problem.

Our service runs several pods in a kubernetes cluster. They are a bit memory hungry, each limited to 1,7 GB of k8s memory. It seems I may have to increase this again, but of course this prevents horizontal scaling. Every now and then k8s kills our pods with OOMKilled.

My gut feeling is that our erlang/elixir/phoenix application does not need this much memory, it just does not know about the memory limits. OOMKilled typically happens when a pod tries to hog more memory than is assigned.

Looking at phoenix live dashboard, it at least it shows the app thinks it has the entire node’s memory accessible.

What should we do? Is there a way to tell beam not to use more memory thank 1,7 GB, or is there a setting where beam reads the k8s memory limits? Or is there some other solution?

Most Liked Switch mode

hubertlepicki

hubertlepicki

I had similar issues and would be interested to find a solution, but I didn’t find such flag.

In fact, figuring out which processes were even responsible for allocating memory was difficult on BEAM.

I ended up sampling the state of the system every 30s or so and be warned if we detect a process that exceeds something like 15 MB of memory.

The code I’m using is something like:

defmodule Infra.Bloat.Find do                                                                                                                                            
  @info_keys [                                                                                                                                                           
    :current_function,                                                                                                                                                   
    :initial_call,                                                                                                                                                       
    :status,                                                                                                                                                             
    :message_queue_len,                                                                                                                                                  
    :links,                                                                                                                                                              
    :dictionary,                                                                                                                                                         
    :trap_exit,                                                                                                                                                          
    :error_handler,                                                                                                                                                      
    :priority,                                                                                                                                                           
    :group_leader,                                                                                                                                                       
    :total_heap_size,                                                                                                                                                    
    :heap_size,                                                                                                                                                          
    :stack_size,                                                                                                                                                         
    :reductions,                                                                                                                                                         
    :garbage_collection,                                                                                                                                                 
    :suspending,                                                                                                                                                         
    :memory                                                                                                                                                              
  ]                                                                                                                                                                      
                                                                                                                                                                         
  @fifteen_megs 1024 * 1024 * 15                                                                                                                                         
                                                                                                                                                                         
  def processes do                                                                                                                                                       
    Process.list()                                                                                                                                                       
    |> Enum.map(fn pid ->                                                                                                                                                
      Process.info(pid, @info_keys)                                                                                                                                      
    end)                                                                                                                                                                 
    |> Enum.filter(&(&1[:memory] && &1[:memory] >= @fifteen_megs
  end           
end

I run Infra.Bloat.Find every 30s and log if something was found. Then, I look at the logs and if something showed up I fix the memory leak.

The usual suspects, according to my experience are:

  • long running GenServers that do very little work, preventing GC from kicking in. Suspending these processes when idle does the job here.
  • Absinthe GraphQL resolvers that were written in naive way, basically exploding the returned size of the payload and / or doing a lot of N+1 queries. Data loader / and / or adding pagination to these helps a lot.
  • processing uploaded files, and/or processing JSON. I ended up writing a pretty shitty streaming subset of JSON parser because of the API I’m using tends to return super large arrays of things in JSON and this was crashing the system easily.

With GraphQL resolvers or just a web requests that are crashing your pod, you need to know that BEAM is able to allocate a lot of memory very very fast, so you won’t catch all of such spikes. Some will kill your pod, others won’t but will go unnoticed, so I ended up just running the sampling code constantly on production and making sure nothing new exceeds the arbitrary memory limit I’ve set up. This seems to work really well at scale and I am able to detect memory leak issues before they are the problem.

12
Post #1
dominicletz

dominicletz

Creator of Elixir Desktop

As BEAM is process-oriented you can set a per-process memory limit. By default, no process has a memory limit and they are all allowed to consume as much as they want.

You can change that default though. E.g. to change the limit to a reasonable 10MB if you’re launching your instance from a shell script add the export ELIXIR_ERL_OPTIONS:

export ELIXIR_ERL_OPTIONS="+hmax 10000000"
iex -S mix

Or if you’re using a release put that into your rel/vm.args.eex

+hmax 10000000

In addition to that, it’s possible to change the max process memory on a per-process level. So you can go with a default limit of 10MB per process as above but then increase that for certain “important” processes, or reduce it for less important processes.

Also, you could stay without a global default limit but set a per request limit from an embedded plug. For E.g. adding this to the beginning of your endpoint definition would set all phoenix request memory limits to 1MB:

defmodule BuzzWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :buzz

  # Setting request memory limit
  plug :set_memory_limit
  defp set_memory_limit(conn, _opts) do
    :erlang.process_flag(:max_heap_size, 1_000_000)
    conn
  end

If you’re using liveview this does not affect the liveview processes as they don’t run through these endpoint plugs. In that case, you could do it from the mount callback or similar.

Hope this helps.
Cheers!

sb8244

sb8244

Author of Real-Time Phoenix

A book suggestion on this topic is Erlang in Anger. It’s free and is one of the best resources that I read to understand what things can go wrong and how to diagnose them.

That said, one thing I might try in a high-memory situation is to force GC globally to see if the memory drops. I do this with Process.list() |> Enum.each(& :erlang.garbage_collect/1). If you do see a large memory drop, then you may have a “memory leak”. But it may not be a memory leak like you’re used to where there’s unfreed memory, but rather processes that don’t have the opportunity to GC due to the lifecycle of the BEAM. One thing you can do to give the opportunity to GC more frequently is to set the VM flag of -env ERL_FULLSWEEP_AFTER 20 in your VM.args file.

I set the above flag in every Elixir app I build now. I have not seen any negative side effects from it (can increase CPU usage, but I did not see a noticeable change) and the benefits can be significant in some long-lived processes.

I wrote an old post about how I diagnosed this in one of my services. The post itself is irrelevant because Phoenix Channels hibernate by default now, but the content itself is still relevant.

edit: 1 more question. What does your memory usage look like? There are several types of memory in the BEAM (process, binary, atom, etc) and that can determine the specific issue. I am not familiar with LiveDashboard as I have used observer_cli in my projects, but I imagine that’s one of the main breakdowns it provides.

Last Post!

stoyle

stoyle

This turned into quite a few insights, at least for me. Thanks again for all your help and knowledge.

I don’t think there is an actual solution to this problem. So don’t think I will mark any answers as a solution. But at least I will summarize what we have done.

  • Deployed similar code to hubertlepicki suggestion, to monitor process memory usage over time. Gives us a chance to find processes hogging too much memory.
  • Added the ERL_FULLSWEEP_AFTER from sb8244, to trigger a full gc more often.
  • Considering setting a global max heap size, as suggested by dominicletz, but we will have to get more control before doing so.

A final change we’ve done. We are running our pods in prod now without a k8s limits memory setting, i.e. the pods may hog as much memory as they like. Baseline seems to be between 500-700 MB, which is ok. Every now and then a pod will allocate more than over 1,7 GB, but since the host has a bit of free memory, this should be ok. If a node starts to struggle k8s will kill the pods anyways.

Only been running this for a few hours, but no OOMKilled pods so far at least.

Where Next?

Trending in Questions Top

jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
silverdr
Using Phoenix.LiveView.TagEngine as an EEx.Engine is deprecated! To compile HEEx, use Phoenix.LiveView.TagEngine.compile/2 instead. Sta...
New
saveman71
Hello ! We want new/edit form pages to POST/PUT to their own URL rather than the resources REST defaults (post /things, put /things/:id)...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
michallepicki
I am using Oban and occasionally, shortly after a deployment, a handful of jobs can fail because of dependency on other parts of the syst...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement