適当おじさんの適当ブログ

技術のことやゲーム開発のことやゲームのことなど自由に雑多に書き連ねます

Rails + Puma + Nginx on CentOS 7 の 環境構築メモ

はじめに

Ruby on Rails製のWebアプリケーションを本番環境であるCentOS 7にデプロイしたので、整理も兼ねてそのときのアプリケーションサーバ、Webサーバの設定についてまとめました。この記事ではRubyの環境構築については一切触れません。 環境構築時のバージョンは、以下のようになっています。

gem バージョン
rails 5.1.4
puma 3.11.3

また、対象のWebアプリケーションを developmentモードで起動できる状態になっていることを前提とします。

目指す構成

  • ユーザのリクエストをWebサーバのNginxで受け取り、アプリケーションサーバのPumaに流す構成にします。
  • Nginx と Puma間の情報のやりとりは、UNIX Socketを利用します。
  • Railsアプリケーションは本番環境用の設定で起動させます。

Pumaの特徴

Puma はアプリケーションサーバの1つです。Rails 5からデフォルトのアプリケーションサーバとなりました。そのほかには、 Unicorn や Passenger といったものがあります。

github.com

Pumaは単一のプロセスでも起動できますが、複数の子プロセスを動作させることもできます。子プロセスは worker と呼ばれます。複数のworkerを起動させた場合、リクエストはworkerごとに処理されます。つまり、workerが複数起動していれば、そのぶんだけ並行してリクエストを受けられます。workerごとにスレッドを作成できるため、受け取ったリクエストは複数のスレッドで並行に処理されます。プロセス数が少ないと、ビジーなプロセスが生まれる可能性が高くなります。デフォルトでは、単一プロセス・マルチスレッドで起動します。

Pumaの起動方法

Pumaは、pumapumactlrails s のいずれかのコマンドで起動できます。rails s コマンドを実行すると内部でPumaが起動されます。以下に、違いをいくつか列挙します。

  • puma は、pumactl よりも多くのオプションを実行時に指定できます。ただし、それらの項目はすべて設定ファイルで指定可能です。
  • puma と rails s は、-e オプションで実行環境を指定できますが、pumactl は実行時に環境を指定できません。
  • puma は、環境変数RACK_ENV で実行環境を指定できます。
  • rails s は、環境変数RAILS_ENV で実行環境を指定できます。
  • pumactlは、pumactl stoppumatl restart などでバックグラウンドのプロセスを安全に停止したり、再起動できます。puma と rails s の場合は、killなどで直接プロセスを削除するしかありません。

すべてのコマンドで設定ファイルをオプションに指定できます。pumactlは、オプション・環境変数のいずれでも実行環境を指定できません。pumactlで実行環境を指定するには、 environtment 'production' と書かれた設定ファイルを読み込むしかありません。

Pumaの設定ファイルの読み込み方

いずれのコマンドもデフォルトは、 config/puma.rb を読み込みます。config/puma.rbrails new などでアプリケーションを作成した際に自動生成されています。

rails spuma 実行時は、config/puma/<environment_name>.rb という環境名のファイルがあれば、Pumaはそちらを読み込みます。つまり、rails s -e production を実行すれば、自動的に、config/puma/production.rb が読み込まれます。もちろん設定ファイルを直接指定することもできます。

pumactl 実行時は、-F オプションで直接設定ファイルを読み取る必要があります。

Pumaの設定

設定方針

  • 開発環境、本番環境で異なる設定ファイルを用意する
  • 開発環境は3000番ポートにバインドする(デフォルトの設定)
  • 本番環境はUNIX Socketにバインドし、負荷を考慮した設定にする
  • コマンド実行時のオプションは最低限とし、設定項目は可能な限り設定ファイルに記載する
  • 実行環境やDBのパスワードは、環境変数に設定する

環境変数から実行環境を読み取りたいので、今回は rails s でサーバを起動させます。開発環境ではこれまで通り気軽にサーバを起動させたいので、デフォルト設定用の config/puma.rb はそのまま置いておきます。そして、本番環境用に config/puma/production.rb を作成します。この記事では、この production.rb を作成していきます。

設定項目1: UNIX Socketにバインド

bind でUNIX Socketにバインドします。各ファイルは、Railsアプリケーションのルートディレクトリの tmp ディレクトリ以下に作成しています。

tmp_path = "#{File.expand_path("../../..", __FILE__)}/tmp"
bind "unix://#{tmp_path}/sockets/puma.sock"
pidfile "#{tmp_path}/pids/puma.pid"

pidfile は実行したプロセスを格納しておくpidファイルです。UNIX Socketにバインドするのであればこの設定は不要ですが、後述するプロセス監視のためにこちらのファイルを作成しておきます。

設定項目2: スレッド数とworker数

プロセス数とスレッド数は重要な設定項目です。worker でworker(プロセス)数を、thread でスレッド数をそれぞれ指定できます。スレッド数を設定する際は、workerごとにスレッドが作成されることを考慮しましょう。スレッド数の最大値は、スレッドの数 * workerの数 となります。workerの数は、CPUのコア数を超えないようにする必要があります。

threads 3, 3 # 最小数, 最大数
workers 2 # 起動数
preload_app!

preload_app! を指定することで、子プロセスを作成する際にCopy on Writeと呼ばれる手法が使われるようになります。Copy on Write は、子プロセスを生成する際のメモリ最適化手法の1つです。workerの起動=子プロセスの作成なので、workerの設定をした場合はあわせて preload_app! のオプションもつけておきましょう。

設定項目3: デーモンとして起動

アプリケーションを長時間バックグラウンドで常駐させるために、デーモンとして起動する設定をします。

daemonize
stdout_redirect "#{tmp_path}/logs/puma.stdout.log", "#{tmp_path}/logs/puma.stderr.log", true

バックグラウンドでプロセスを起動するにあたって、標準出力と標準エラー出力をファイルに出力するようにしています。

設定項目4: workerを定期的に再起動させる

ケアを一切せずにRailsアプリケーションを放置していると、プロセスのメモリ使用量が徐々に増加していきます。メモリ使用量が多くなりすぎると、サーバ全体の処理が非常に遅くなる可能性があります。それを避けるために、定期的にworkerをkillして新しいworkerを立ち上げます。PumaWorkerKiller というgemを用いれば、それが簡単に実現できます。

github.com

PumaWorkerKillerでは、以下の2つの設定ができます。具体的な設定は次の「Pumaの設定まとめ」を参照してください。

  • 時間 を設定して、定期的にプロセスを再起動させる
  • メモリ使用量の閾値 を設定して、それを超えた場合に再起動させる

Pumaの設定まとめ

# config/puma/production.rb
environment "production"

# UNIX Socketへのバインド
tmp_path = "#{File.expand_path("../../..", __FILE__)}/tmp"
bind "unix://#{tmp_path}/sockets/puma.sock"

# スレッド数とWorker数の指定
threads 3, 3
workers 2
preload_app!

# デーモン化の設定
daemonize
pidfile "#{tmp_path}/pids/puma.pid"
stdout_redirect "#{tmp_path}/logs/puma.stdout.log", "#{tmp_path}/logs/puma.stderr.log", true

# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart

# puma_worker_killerの設定
before_fork do
  PumaWorkerKiller.config do |config|
    # 閾値を超えた場合にkillする
    config.ram           = 1024 # mb
    config.frequency     = 5 * 60 # per 5minute
    config.percent_usage = 0.9 # 90%
    # 閾値を超えたかどうかに関わらず定期的にkillする
    config.rolling_restart_frequency = 24 * 3600 # per 1day 
    # workerをkillしたことをログに残す
    config.reaper_status_logs = true
  end
  PumaWorkerKiller.start
  ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
end
on_worker_boot do
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

この状態で以下のコマンドを実行すれば、Pumaがバックグラウンドで起動することが確認できます。

$ rails s
$ ps aux | grep rails

psコマンドを実行すれば、ワーカープロセスが複数起動していることが確認できるかと思います。エラーが発生する場合には、stdout_redirect に指定した各ファイルをチェックすると良いです。続いてNginxの設定に移ります。

Nginxの導入

NginxはWebサーバです。同時に多くのコネクションを処理できるように、イベントベースのコネクション処理機構が搭載されています。

www.nginx.com

CentOS 7 には、以下のコマンドでインストールできます。

# yum install -y nginx

この時点ではNginxは起動していません。マシン再起動後にNginxが自動的に起動するように、Nginxをサービスとして登録します。CentOS 7からサービス関連のコマンドは、chkconfig から systemctl に変わりました。

# systemctl enable nginx

コマンド実行後にマシンを再起動すると、Nginxが起動していることが確認できるかと思います。

UNIX Socketへリクエストを流す設定

Nginxをリバースプロキシとして動作させます。以下は、そのための最低限の設定です。

# /etc/nginx/conf.d/rails.conf
upstream <rails-app> {
    server unix:///<path-to-railsapp-root>/tmp/sockets/puma.sock;
}
server {
    listen       80;
    server_name  <server-name>;
    location / {
        proxy_pass http://<rails-app>;
    }
}

これで、NginxからRailsアプリケーションまでソケットを介してリクエストが転送されるようになります。 <> で囲われた部分には、任意の値を設定してください。

クライアント情報をRailsアプリケーションに転送する

Nginxをリバースプロキシとして動作させると、HTTPヘッダにあるクライアントの情報が、プロキシサーバの情報で上書きされてしまいます。Railsアプリケーションにクライアントの情報を中継したい場合は、以下のようにproxy_set_headerディレクティブを追加すると良いです。

location / {
    proxy_redirect off;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
}

1行目の proxy_redirect off は、レスポンスのLocationヘッダがNginxによって書き換えられないようにするための設定です。proxy_set_headerディレクティブは、proxy_set_header <HTTPヘッダ名> <値> で設定できます。$で始まる変数はNginxで定義されているものです。各変数の値は以下のようになっています。

変数名
$remote_addr Nginxが認識するクライアントのIPアドレス
$proxy_add_x_forwarded_for リクエストのX-Forwarded-Forの値に、$remote_addrを足したもの
$http_host ユーザが入力したURLのHost部分

Nginxの設定まとめ

いくつかのオプションを付け加えて、最終的に以下のような設定としました。

upstream <rails-app> {
    server unix:///path-to-railsapp-root/tmp/sockets/puma.sock;
}

server {
    listen       80;
    server_name  <server-name>;

    location / {
        proxy_pass http://<rails-app>;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_redirect off;
        proxy_connect_timeout 30;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root /path-to-railsapp-root/public;
    }
}

error_pageディレクティブと locationディレクティブで、50x系のエラーは public/50x.html を返すようにしています。40x系のエラーはアプリケーションでハンドリングできるため、Nginxでは何も設定していません。これで、NginxからRailsアプリケーションまで繋がりました。最後に外部から接続できように、ファイアウォールの設定をします。

ファイアウォールの設定

CentOS 7からファイアウォールの設定が iptables から firewalld になりました。

www.firewalld.org

firewalld に各種設定をして、Nginxが待ち構えている80番ポートに接続できるようにします。firewall-cmd コマンドを使って80番ポート、つまり、http:// での接続を許可します。

# firewall-cmd --permanent --zone public --add-service http
# firewall-cmd --get-active-zones