はじめに
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 といったものがあります。
Pumaは単一のプロセスでも起動できますが、複数の子プロセスを動作させることもできます。子プロセスは worker と呼ばれます。複数のworkerを起動させた場合、リクエストはworkerごとに処理されます。つまり、workerが複数起動していれば、そのぶんだけ並行してリクエストを受けられます。workerごとにスレッドを作成できるため、受け取ったリクエストは複数のスレッドで並行に処理されます。プロセス数が少ないと、ビジーなプロセスが生まれる可能性が高くなります。デフォルトでは、単一プロセス・マルチスレッドで起動します。
Pumaの起動方法
Pumaは、puma
、pumactl
、rails s
のいずれかのコマンドで起動できます。rails s
コマンドを実行すると内部でPumaが起動されます。以下に、違いをいくつか列挙します。
- puma は、pumactl よりも多くのオプションを実行時に指定できます。ただし、それらの項目はすべて設定ファイルで指定可能です。
- puma と rails s は、
-e
オプションで実行環境を指定できますが、pumactl
は実行時に環境を指定できません。 - puma は、環境変数
RACK_ENV
で実行環境を指定できます。 - rails s は、環境変数
RAILS_ENV
で実行環境を指定できます。 - pumactlは、
pumactl stop
やpumatl restart
などでバックグラウンドのプロセスを安全に停止したり、再起動できます。puma と rails s の場合は、kill
などで直接プロセスを削除するしかありません。
すべてのコマンドで設定ファイルをオプションに指定できます。pumactlは、オプション・環境変数のいずれでも実行環境を指定できません。pumactlで実行環境を指定するには、 environtment 'production'
と書かれた設定ファイルを読み込むしかありません。
Pumaの設定ファイルの読み込み方
いずれのコマンドもデフォルトは、 config/puma.rb を読み込みます。config/puma.rb は rails new
などでアプリケーションを作成した際に自動生成されています。
rails s
と puma
実行時は、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を用いれば、それが簡単に実現できます。
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サーバです。同時に多くのコネクションを処理できるように、イベントベースのコネクション処理機構が搭載されています。
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
になりました。
http://www.firewalld.org/documentation/man-pages/firewall-cmd.htmlwww.firewalld.org
firewalld
に各種設定をして、Nginxが待ち構えている80番ポートに接続できるようにします。firewall-cmd
コマンドを使って80番ポート、つまり、http://
での接続を許可します。
# firewall-cmd --permanent --zone public --add-service http # firewall-cmd --get-active-zones