From c6e22356300893cf043fad0592b328f32c8ee8b1 Mon Sep 17 00:00:00 2001 From: schneems Date: Wed, 25 Apr 2018 21:23:46 -0500 Subject: [PATCH] [close #751] Default MALLOC_ARENA_MAX new apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 2 ++ changelogs/v205/malloc.md | 32 ++++++++++++++++++++++++++++++++ lib/language_pack/metadata.rb | 8 +++++++- lib/language_pack/ruby.rb | 23 ++++++++++++++++++----- spec/hatchet/rubies_spec.rb | 4 +++- 5 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 changelogs/v205/malloc.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e645edf7..196124535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/changelogs/v205/malloc.md b/changelogs/v205/malloc.md new file mode 100644 index 000000000..b8cd03e8e --- /dev/null +++ b/changelogs/v205/malloc.md @@ -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 diff --git a/lib/language_pack/metadata.rb b/lib/language_pack/metadata.rb index f6979883c..4cc1d33e1 100644 --- a/lib/language_pack/metadata.rb +++ b/lib/language_pack/metadata.rb @@ -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) @@ -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 diff --git a/lib/language_pack/ruby.rb b/lib/language_pack/ruby.rb index ee72e3c50..8de8513cc 100644 --- a/lib/language_pack/ruby.rb +++ b/lib/language_pack/ruby.rb @@ -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") @@ -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? @@ -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 diff --git a/spec/hatchet/rubies_spec.rb b/spec/hatchet/rubies_spec.rb index 283d97138..0cfa68755 100644 --- a/spec/hatchet/rubies_spec.rb +++ b/spec/hatchet/rubies_spec.rb @@ -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! @@ -93,4 +95,4 @@ expect(app.output).to match("Ruby version change detected") end end -end \ No newline at end of file +end