Skip to content

Commit

Permalink
[close #751] Default MALLOC_ARENA_MAX new apps
Browse files Browse the repository at this point in the history
This PR will set MALLOC_ARENA_MAX=2 by default for new Ruby apps

While we currently have [documentation on tuning the memory behavior of glibc by setting this environment variable](https://devcenter.heroku.com/articles/tuning-glibc-memory-behavior) the default does not produce good results for Ruby applications that are using threads:

- https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/
- https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html

In general most Ruby applications are memory bound and by decreasing the memory footprint of the application we can enable scaling out via more workers. Less memory might also mean a cheaper to run application, as it consumes fewer resources.

Setting this value is not entirely free. It does come with a performance trade off. For more information, see how we originally benchmarked this setting:

- https://devcenter.heroku.com/articles/testing-cedar-14-memory-use

If a customer’s application is not memory bound and would prefer slightly faster execution over the decreased memory use, they can set their MALLOC_ARENA_MAX to a higher value. The default as specified by the [linux man page](http://man7.org/linux/man-pages/man3/mallopt.3.html) is 8 times the number of cores on the system. Or they can use the 3rd party [jemalloc buildpack](https://elements.heroku.com/buildpacks/mojodna/heroku-buildpack-jemalloc).

Our documentation will be updated to reflect this change once the PR is merged and deployed.
  • Loading branch information
schneems committed Sep 12, 2019
1 parent 6d41fb1 commit c6e2235
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## v205 (unreleased)

* Default `MALLOC_ARENA_MAX=2` for new applications (https://github.com/heroku/heroku-buildpack-ruby/pull/752)

## v204 (9/12/2019)

* Default Ruby version for new apps is now 2.5.6 (https://github.com/heroku/heroku-buildpack-ruby/pull/919)
Expand Down
32 changes: 32 additions & 0 deletions changelogs/v205/malloc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Default `MALLOC_ARENA_MAX=2` for new Ruby applications

The environment variable `MALLOC_ARENA_MAX` will now default to `2` for Ruby applications. This environment variable was
previously unset. This change will only affect new applications on the platform, to update an existing application please
run:

```
$ heroku config:set MALLOC_ARENA_MAX=2
```

The goal of setting this value is to decrease memory usage for the majority of Ruby applications that are using threads
such as apps that use Sidekiq or Puma. To understand more about the relationship between this value and memory please see
the following external resources:

- https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html
- https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/

We also maintain our own [documentation on tuning the memory behavior of glibc by setting this environment variable](https://devcenter.heroku.com/articles/tuning-glibc-memory-behavior).

If a your application is not memory bound and would prefer slightly faster execution over the decreased memory use,
you can set their `MALLOC_ARENA_MAX` to a higher value. The default as specified by the [linux man page](http://man7.org/linux/man-pages/man3/mallopt.3.html)
is 8 times the number of cores on the system.

## Jemalloc

Another popular alternative memory allocator is jemalloc. At this time Heroku does not maintain a supported version of this memory allocator,
but you can use it in your application with a 3rd party [jemalloc buildpack](https://elements.heroku.com/buildpacks/mojodna/heroku-buildpack-jemalloc).

If you are using jemalloc, setting `MALLOC_ARENA_MAX` will have no impact on memory or performance. For more information on
how jemalloc interacts with Ruby applications see this external post:

- https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html#fix-2-use-jemalloc
8 changes: 7 additions & 1 deletion lib/language_pack/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def initialize(cache)

def read(key)
full_key = "#{FOLDER}/#{key}"
File.read(full_key) if exists?(key)
File.read(full_key).chomp if exists?(key)
end

def exists?(key)
Expand All @@ -27,6 +27,12 @@ def write(key, value, isave = true)
full_key = "#{FOLDER}/#{key}"
File.open(full_key, 'w') {|f| f.puts value }
save if isave

return true
end

def touch(key)
write(key, "true")
end

def save
Expand Down
23 changes: 18 additions & 5 deletions lib/language_pack/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,22 @@ def config_detect

private

def default_malloc_arena_max?
return true if @metadata.exists?("default_malloc_arena_max")
return @metadata.touch("default_malloc_arena_max") if new_app?

return false
end

def stack_not_14_not_16?
case stack
when "cedar-14", "heroku-16"
return false
else
return true
end
end

def warn_bundler_upgrade
old_bundler_version = @metadata.read("bundler_version").chomp if @metadata.exists?("bundler_version")

Expand Down Expand Up @@ -352,6 +368,7 @@ def setup_profiled
set_env_override "GEM_PATH", "$HOME/#{slug_vendor_base}:$GEM_PATH"
set_env_override "PATH", profiled_path.join(":")

set_env_default "MALLOC_ARENA_MAX", "2" if default_malloc_arena_max?
add_to_profiled set_default_web_concurrency if env("SENSIBLE_DEFAULTS")

if ruby_version.jruby?
Expand Down Expand Up @@ -568,11 +585,7 @@ def load_default_cache
# install libyaml into the LP to be referenced for psych compilation
# @param [String] tmpdir to store the libyaml files
def install_libyaml(dir)
case stack
when "cedar-14", "heroku-16"
else
return
end
return false if stack_not_14_not_16?

instrument 'ruby.install_libyaml' do
FileUtils.mkdir_p dir
Expand Down
4 changes: 3 additions & 1 deletion spec/hatchet/rubies_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
app = Hatchet::Runner.new("default_ruby", stack: DEFAULT_STACK)
app.setup!
app.deploy do |app|
expect(app.run('echo "MALLOC_ARENA_MAX_is=$MALLOC_ARENA_MAX"')).to match("MALLOC_ARENA_MAX_is=2")

run!(%Q{echo "ruby '2.5.1'" >> Gemfile})
run!("git add -A; git commit -m update-ruby")
app.push!
Expand All @@ -93,4 +95,4 @@
expect(app.output).to match("Ruby version change detected")
end
end
end
end

0 comments on commit c6e2235

Please sign in to comment.