Browse Source

Rails: Auto-tune unicorn and puma (#2850)

Nate Berkopec 8 years ago
parent
commit
f93a1bd4fa

+ 41 - 0
frameworks/Ruby/rails/config/auto_tune.rb

@@ -0,0 +1,41 @@
+#!/usr/bin/env ruby
+# Instantiate about one process per X MiB of available memory, scaling up to as
+# close to MAX_THREADS as possible while observing an upper bound based on the
+# number of virtual/logical CPUs. If there are fewer processes than
+# MAX_THREADS, add threads per process to reach MAX_THREADS.
+require 'etc'
+
+KB_PER_WORKER = 128 * 1_024 # average of peak PSS of single-threaded processes (watch smem -k)
+MIN_WORKERS = 2
+MAX_WORKERS_PER_VCPU = 1.25 # virtual/logical
+MIN_THREADS_PER_WORKER = 1
+MAX_THREADS = Integer(ENV['MAX_CONCURRENCY'] || 256)
+
+def meminfo(arg)
+  File.open('/proc/meminfo') do |f|
+    f.each_line do |line|
+      key, value = line.split(/:\s+/)
+      return value.split(/\s+/).first.to_i if key == arg
+    end
+  end
+
+  fail "Unable to find `#{arg}' in /proc/meminfo!"
+end
+
+def auto_tune
+  avail_mem = meminfo('MemAvailable') * 0.8 - MAX_THREADS * 1_024
+
+  workers = [
+    [(1.0 * avail_mem / KB_PER_WORKER).floor, MIN_WORKERS].max,
+    (Etc.nprocessors * MAX_WORKERS_PER_VCPU).ceil
+  ].min
+
+  threads_per_worker = [
+    workers < MAX_THREADS ? (1.0 * MAX_THREADS / workers).ceil : -Float::INFINITY,
+    MIN_THREADS_PER_WORKER
+  ].max
+
+  [workers, threads_per_worker]
+end
+
+p auto_tune if $0 == __FILE__

+ 7 - 0
frameworks/Ruby/rails/config/mri_puma.rb

@@ -0,0 +1,7 @@
+require_relative 'auto_tune'
+
+# FWBM only... use the puma_auto_tune gem in production!
+num_workers, num_threads = auto_tune
+
+workers num_workers
+threads num_threads, num_threads

+ 53 - 47
frameworks/Ruby/rails/config/unicorn.rb

@@ -1,53 +1,59 @@
-worker_processes 8
+require_relative 'auto_tune'
+
+# FWBM only...
+num_workers, = auto_tune
+worker_processes num_workers
+
 listen "/tmp/.sock", :backlog => 256
 
 preload_app true
+
 GC.respond_to?(:copy_on_write_friendly=) and
   GC.copy_on_write_friendly = true
 
-  before_fork do |server, worker|
-    # the following is highly recomended for Rails + "preload_app true"
-    # as there's no need for the master process to hold a connection
-    defined?(ActiveRecord::Base) and
-      ActiveRecord::Base.connection.disconnect!
-
-    # The following is only recommended for memory/DB-constrained
-    # installations.  It is not needed if your system can house
-    # twice as many worker_processes as you have configured.
-    #
-    # # This allows a new master process to incrementally
-    # # phase out the old master process with SIGTTOU to avoid a
-    # # thundering herd (especially in the "preload_app false" case)
-    # # when doing a transparent upgrade.  The last worker spawned
-    # # will then kill off the old master process with a SIGQUIT.
-    # old_pid = "#{server.config[:pid]}.oldbin"
-    # if old_pid != server.pid
-    #   begin
-    #     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
-    #     Process.kill(sig, File.read(old_pid).to_i)
-    #   rescue Errno::ENOENT, Errno::ESRCH
-    #   end
-    # end
-    #
-    # Throttle the master from forking too quickly by sleeping.  Due
-    # to the implementation of standard Unix signal handlers, this
-    # helps (but does not completely) prevent identical, repeated signals
-    # from being lost when the receiving process is busy.
-    # sleep 1
-  end
-
-  after_fork do |server, worker|
-    # per-process listener ports for debugging/admin/migrations
-    # addr = "127.0.0.1:#{9293 + worker.nr}"
-    # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
-
-    # the following is *required* for Rails + "preload_app true",
-    defined?(ActiveRecord::Base) and
-      ActiveRecord::Base.establish_connection
-
-    # if preload_app is true, then you may also want to check and
-    # restart any other shared sockets/descriptors such as Memcached,
-    # and Redis.  TokyoCabinet file handles are safe to reuse
-    # between any number of forked children (assuming your kernel
-    # correctly implements pread()/pwrite() system calls)
-  end
+before_fork do |server, worker|
+  # the following is highly recomended for Rails + "preload_app true"
+  # as there's no need for the master process to hold a connection
+  defined?(ActiveRecord::Base) and
+    ActiveRecord::Base.connection.disconnect!
+
+  # The following is only recommended for memory/DB-constrained
+  # installations.  It is not needed if your system can house
+  # twice as many worker_processes as you have configured.
+  #
+  # # This allows a new master process to incrementally
+  # # phase out the old master process with SIGTTOU to avoid a
+  # # thundering herd (especially in the "preload_app false" case)
+  # # when doing a transparent upgrade.  The last worker spawned
+  # # will then kill off the old master process with a SIGQUIT.
+  # old_pid = "#{server.config[:pid]}.oldbin"
+  # if old_pid != server.pid
+  #   begin
+  #     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
+  #     Process.kill(sig, File.read(old_pid).to_i)
+  #   rescue Errno::ENOENT, Errno::ESRCH
+  #   end
+  # end
+  #
+  # Throttle the master from forking too quickly by sleeping.  Due
+  # to the implementation of standard Unix signal handlers, this
+  # helps (but does not completely) prevent identical, repeated signals
+  # from being lost when the receiving process is busy.
+  # sleep 1
+end
+
+after_fork do |server, worker|
+  # per-process listener ports for debugging/admin/migrations
+  # addr = "127.0.0.1:#{9293 + worker.nr}"
+  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
+
+  # the following is *required* for Rails + "preload_app true",
+  defined?(ActiveRecord::Base) and
+    ActiveRecord::Base.establish_connection
+
+  # if preload_app is true, then you may also want to check and
+  # restart any other shared sockets/descriptors such as Memcached,
+  # and Redis.  TokyoCabinet file handles are safe to reuse
+  # between any number of forked children (assuming your kernel
+  # correctly implements pread()/pwrite() system calls)
+end

+ 1 - 1
frameworks/Ruby/rails/run_mri_puma.sh

@@ -4,4 +4,4 @@ fw_depends mysql rvm ruby-2.4
 
 rvm ruby-$MRI_VERSION do bundle install --jobs=4 --gemfile=$TROOT/Gemfile --path=$IROOT/rails/bundle
 
-DB_HOST=${DBHOST} rvm ruby-$MRI_VERSION do bundle exec puma -t 8:32 -w 8 --preload -b tcp://0.0.0.0:8080 -e production &
+DB_HOST=${DBHOST} rvm ruby-$MRI_VERSION do bundle exec puma -C config/mri_puma.rb -b tcp://0.0.0.0:8080 -e production &