Rails サーバーを Unicorn から Puma に切り替える

Rails アプリでは、一般的に使用されている 2 つの一般的な Web サーバーのうち、unicorn はワーカー プロセスでリクエストを処理しますが、puma はプロセス ワーカーとスレッドの両方で処理できます。したがって、unicorn から puma に切り替えると、同時実行性が向上するだけでなく、メモリ使用量も削減できます。さらに、rails 5.0 以降、使用されるデフォルトの Web サーバーは puma です。

Prerequisites

In order to use puma for rails app, we must make sure the code is thread safe. Although the way how rails framework was designed to serve request by instantializing a new controller object per request makes it safe under multiple threads environment, there are still many things to check and update, dependent on specific apps.

  • Check the lib folder
    The lib folder usually contains code used by adhoc scripts or batch jobs that is not used by MVC code, but legacy apps tend to add the lib folder or its subfolders into autoload_paths with eager loading and cache class turned off (in our case), so we need refactor them first to:

    • Move code in lib that is used by MVC code to app/lib
    • Leave only adhoc script related code in the lib folder
  • Check configuration about eager loading and cache classes
    Set the values based on recommendation:

# for production/preprod
config.eager_load = true
config.cache_classes = true

# for development
config.eager_load = false
config.cache_classes = false

# for test
config.eager_load = false
config.cache_classes = true

# For all environments
# Do not change or mess with autoload_paths because it's not necessary
  • Search for @@ about class variables usage
    Search for class variables in code base, and even if they are read only after app finishes loading, most of the time, they should not be used and can be replaced by constants.

  • Search for ||= about memoization usage on class instance variables
    The ||= on class instance variables should be avoided, such as:

class << self
  def config
    @config ||= load_config
  end
end

should be guarded with a mutex block like:

@mutex = Mutex.new

class << self
  def config
    @config ||= @mutex.synchronize do
      @config ||= load_config
    end
  end
end
  • Maybe some others depending on app...

Switch to use puma for default server

  • Update Gemfile
    For legacy app using unicorn, the Gemfile and Gemfile.lock need updated:
gem 'unicorn' # remove this
gem 'puma' # add this

and run bundle i to update Gemfile.lock

  • Prepare environment specific puma configuration
    Create config/puma/{environment}.rb config files to adjust config based on environment, for example:
port        ENV.fetch('PORT') { 3000 }
environment ENV.fetch('RAILS_ENV') { 'development' }
bind "unix://#{shared_path}/tmp/sockets/puma.sock"
environment ENV.fetch('RAILS_ENV') { 'production' }
  • Update deploy scripts to deploy rails app with puma instead of unicorn
    If you use capistrano for deployment, a gem called capistrano3-puma makes it easy to deploy with puma but it can also be problematic, such as the latest version of puma (5.0) is not yet supported as of this writing. In such case, you can write custom script to use tools like systemd to manage the puma services on linux servers.

参考文献

  • https://github.com/rails/rails/issues/33209
  • https://stackoverflow.com/questions/15184338/how-to-know-what-is-not-thread-safe-in-ruby/15184752#15184752
  • https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#thread-safety
  • https://bearmetal.eu/theden/how-do-i-know-whether-my-rails-app-is-thread-safe-or-not/
  • https://guides.rubyonrails.org/v5.1/autoloading_and_reloading_constants.html
  • https://github.com/puma/puma/blob/master/docs/deployment.md
  • https://medium.com/@anilkumarmaurya/when-not-to-use-memoization-in-ruby-on-rails-9d54bce0ae74
  • https://github.com/rails/rails/pull/9789
  • https://blog.arkency.com/3-ways-to-make-your-ruby-object-thread-safe/
  • https://stackoverflow.com/questions/62458471/is-establish-connection-on-worker-boot-still-required-on-rails-6-and-puma
  • https://github.com/puma/puma/issues/1001

rails ruby