今の設定ファイル
require 'yaml'
def local_conf
conf = YAML.load_file('path_to_settings.local.yml')
conf['puma']
end
directory 'path_to_app_directory'
workers local_conf['workers']
threads_count = local_conf['threads']
threads threads_count, threads_count
phased_restart = local_conf['phased_restart']
preload_app! unless phased_restart
prune_bundler if phased_restart
rackup DefaultRackup
port 3000
worker_timeout local_conf['worker_timeout']
pidfile File.expand_path('tmp/pids/puma.pid')
state_path File.expand_path('tmp/pids/puma.state')
stdout_redirect File.expand_path('log/puma_access.log'), File.expand_path('log/puma_error.log'), true
before_fork do
ActiveRecord::Base.connection_pool.disconnect! unless phased_restart
end
on_worker_boot do
unless phased_restart
ActiveSupport.on_load(:active_record) do
ActiveRecord::Base.establish_connection
end
end
end
plugin :tmp_restart
5つの設定ポイント
- zero downtimeを実現するための clustered mode設定
- database connection poolings
- logrotate設定
- capistrano-pumaの設定
- prune_bundlerの設定
zero downtimeを実現するための clustered mode
pumaにおいて、zero downtime、 zero hanging requestを実現するためには、
clustered modeを使う必要がある。clustered modeはworker数を指定するだけで起動する。
# clustered modeを使わない場合、workersは0とする
workers 2
threads_count = 5
threads threads_count, threads_count
workerの数はCPUのコア数と一致させる。
なお、2 worker 5 threadでがっつり負荷をかけたら、2GB以上のメモリを消費した。
少ないメモリで zero downtimeを実現したい場合、unicornの方が適しているかも知れない。
(もちろん、実行しているアプリケーションによって異なる)
database connection pools
ActiveRecordのconnectionはworker毎に設定されている。
そのためdatabase connection poolsの値には、1 workerごとの thread数を設定する。
rails デフォルトでは、ENV['RAILS_MAX_THREADS']に値を設定することがデフォルトとなっている。
logrotate設定について
※ RailsのLoggerにて、rotate & 削除をしている人は気にする必要が無いです.
unicornと違い、Pumaはsignalを送っても puma自体のLogのRotateしか行いません。
そのため、chrono_loggerを使うなり、copytruncateを利用するなり、
今までとは異なる方法で Logrotateを行う必要があります。
capistrano-pumaの設定について
puma_preload_appをtrueにすると、phased-restartが出来ない。
ので、cluster modeのときは、puma_preload_appをfalseにしないといけない。
#lib/capistrano/tasks/puma.rake
task :smart_restart do
if !puma_preload_app? && puma_workers.to_i > 1
invoke 'puma:phased-restart'
else
invoke 'puma:restart'
end
end
Prune Bundlerの設定
ここ多分いちばん大切。
prune bundler は cluster processが読みに行く GEM_FILEを指定するもの。
これをしないと、Gemfile を書き換えても読み込んでくれなくなるし、正しく current directoryに
存在する Gemfile を見てくれず、過去のGemfileが参照できなくなったタイミングで puma のプロセスが落ちてしまう。
# launcher.rb
def prune_bundler
return unless defined?(Bundler)
puma = Bundler.rubygems.loaded_specs("puma")
dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
unless puma_lib_dir
log "! Unable to prune Bundler environment, continuing"
return
end
deps = puma.runtime_dependencies.map do |d|
spec = Bundler.rubygems.loaded_specs(d.name)
"#{d.name}:#{spec.version.to_s}"
end
log '* Pruning Bundler environment'
home = ENV['GEM_HOME']
Bundler.with_clean_env do
ENV['GEM_HOME'] = home
ENV['PUMA_BUNDLER_PRUNED'] = '1'
wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
Kernel.exec(*args)
end
end
処理はこんな感じ。
Bundler.with_clean_env は BUNDLE_ の環境変数を消して yieldを実行するので、
新しいBundlerを読み込もうとしていることが分かる。
なお、prune_bundlerを使いたいときは、preload_app をfalseにして、
workersを2以上にする必要がある。
# launcher.rb
def prune_bundler?
@options[:prune_bundler] && clustered? && !@options[:preload_app]
end
def clustered?
(@options[:workers] || 0) > 0
end
# 確認方法 (by capistrano)
cap staging puma:start
DEBUG [xxxx] * Pruning Bundler environment
DEBUG [xxxx] [xxxx] Puma starting in cluster mode...
参考資料:
https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#database-connections
https://devcenter.heroku.com/articles/concurrency-and-database-connections
http://qiita.com/ma2ge/items/1fba78a08e8d3b82b3c2#logger--logrotate
https://github.com/puma/puma/blob/master/DEPLOYMENT.md#restarting