「Railsを本番環境で動かす際に、UnicornとNginxを使用している人が多いけど、一体なんでだろう?」
そう疑問に思ったので、Rails×Unicorn×Nginxについて調べてまとめました。
Railsとアプリケーションサーバー
本題に入るまえに、Railsとアプリケーションサーバーについて解説します。
通常、$rails sでアプリケーションサーバーの「rack」が起動し、Railsが動き出します。
rackとは、rubyのコードで書かれたWebフレームワークとWebサーバーを繋ぐもので、シンプルに言えばRailsを動かすアプリケーションサーバです。
ただし、高負荷に弱いというデメリットがあるそうで、その問題を解決するための方法がRails×Unicorn×Nginxになります。
Rails×Nginx×Unicornの仕組み
まずNginxとUnicornについて簡単に解説します。
- Nginxは、高負荷に強いWebサーバ。
- Unicornは、Rack専用のWebサーバ。
Webサーバーがダブっていますが、これで合っています。
なぜなら、NginxはRackに繋ぐことができないので、専用サーバーであるUnicornなどで中継させる必要があるからです。
また、Nginxのバッファリングにより、1つづつしか処理を捌けないUnicornに、通信状態のわるいクライアントからの処理が送られなくなっています。
バッファリングとは
通信速度の減速や中断などに備え、記憶領域にデータを保存する機能。
Unicornについて
Unicornは、プロセス(工程)がmaster1つと複数のworkerに分かれていますが、ソースコードを読み込むのはmasterだけなので起動が早いです。
処理の流れをまとめると以下のようになります。
- Nginxからリクエストが届く
- unicornが起動する
- masterプロセス生成
- ソースコードの読込&worker作成
- Nginxから流れてきた処理を捌いていく
上記のように、Unicornは負荷分散をおこない、処理をRack(Rails)に送ります。
最終的にRack側から返答が返ってきたら、それをクライアントを返して終了です。
Unicornの導入方法
まずはUnicornをインストールするために、gemfileに以下のコードを追加。
gem 'unicorn'
続いてUnicorn設定ファイルを作成するので、configディレクトリ下にunicorn.rbファイルを作成。
※参考にさせて頂いたサイトのコードをみてメモっているだけです。詳細はこちら
のサイトが分かりやすかったです。
$worker = 2
$timeout = 30
$app_dir = "/app"
$listen = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dir
$pid = File.expand_path 'tmp/pids/unicorn.pid', $app_dir
$std_log = File.expand_path 'log/unicorn.log', $app_dir
worker_processes $worker #ワーカーの最大同時起動数
working_directory $app_dir #自分のアプリケーションまでのpath
stderr_path $std_log #ログ出力ファイル
stdout_path $std_log #エラーログ出力ファイル
timeout $timeout #ワーカーの処理開始から終了までの最大時間
listen $listen #Nginxとの接続用ポート番号
pid $pid #プロセスIDの保存ファイル
preload_app true #Unicornの再起動をダウンタイムなしで行うかどうか
#フォーク開始前の処理
before_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.pid
begin
Process.kill "QUIT", File.read(old_pid).to_i
rescue Errno::ENOENT, Errno::ESRCH
end
end
end
#フォーク終了後の処理
after_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end
設定 | 説明 |
---|---|
worker_processes | ワーカーの最大同時起動数 |
timeout | ワーカーの処理開始から終了までの最大時間 |
working_directory | 起動コマンドが実行されるディレクトリ |
pid | プロセスIDの保存ファイル |
listen | Nginxとの接続用ポート番号 |
stdout_path | ログ出力ファイル |
stderr_path | エラーログ出力ファイル |
preload_app | Unicornの再起動をダウンタイムなしで行うかどうか |
変数を定義して、各項目に値をあてているだけです。設定内容に関しては、上記の表を参照してください。 |
フォーク前後の処理内容はすこし複雑なので、ザックリ解説します。
フォークとは
IT分野では、現在のプロセスを複製して、新しいプロセスとして動かすことを意味する。
今回の場合は、マスタープロセスがワーカープロセスを複製することを指します。
フォーク前の処理(before_fork)
フォーク前には、unicornとActiveRecordとの接続を切ります。
defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
また、マスタープロセスを再起動する処理も必要。
理由は、preload_appをtrueにしてダウンタイムなしでUnicornを再起動するようにしているので、再起動を行い古いプロセスを止めたいからです。
old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.pid
begin
Process.kill "QUIT", File.read(old_pid).to_i
rescue Errno::ENOENT, Errno::ESRCH
end
end
フォーク後の処理(after_fork)
フォーク後には、再度unicornとActiveRecordを接続します。
after_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end
Unicornの起動/停止/再起動コマンド
内容 | コマンド |
---|---|
Unicorn起動 | $ bundle exec unicorn -c config/unicorn.rb [-E $RAILS_ENV] [-D] |
Unicorn停止 | $ kill -QUIT "cat /path/to/unicorn.pid" |
Unicorn再起動(通常) | $ kill -HUP "cat /path/to/unicorn.pid" |
Unicorn再起動(Capistranoなどの使用時) | $ kill -USR2 "cat /path/to/unicorn.pid" |
Capistranoを使用している場合の再起動には、-USR2オプションを指定する必要があるようです。
もしアプリのデプロイ時などに、再起動をおこなっても内容が反映されない場合は、停止→起動コマンドを実行してみて下さい。
Nginxについて
先述したように、Nginxは高負荷につよいWebサーバーで、以下のような特徴があります。
- 複数のアクセスに1つの対応。
- 急激なアクセス数UPに強い。
- 処理速度を維持しやすい。
今回、RailsとNginxを併用する理由は高負荷につよいサーバーが欲しいからなので、まさにピッタリのWebサーバーだと思います。
その他のWebサーバー
メジャーなWebサーバーにApacheなども存在しますが、アクセスの急上昇によわくダウンしやすいといった特徴があるようです。
Nginxの導入方法
今回はRailsのディレクトリ内にNginxディレクトリを作成し、その中にDockerfileとNginx.confファイルを作成しました。
(Dockerfile)
FROM nginx:stable
RUN rm -f /etc/nginx/conf.d/*
COPY nginx.conf /etc/nginx/conf.d/myapp.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
(Nginx.conf)
upstream unicorn {
server unix:/app/tmp/sockets/.unicorn.sock;
}
server {
listen 80;
server_name localhost;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
root /app/public;
error_page 404 /404.html;
error_page 505 502 503 504 /500.html;
try_files $uri/index.html $uri @unicorn;
location @unicorn {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://unicorn;
}
}
それぞれのコードの意味。
upstream unicornについて
Unicornソケットの設定を行います。
簡単にいえば、NginxとUnicornで通信するために必要な設定です。
※Unicorn側でもNginxと通信するために、listen = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dirでソケットを指定しています。
serverについて
上から順に、listenではポート番号を設定しますが、Webサーバーには80を指定するのが基本です。
今回はローカルで起動させたので、server_nameをlocalhostに設定し、access_logとeroor_logでログ出力設定を行なっています。
rootにpublicディレクトリを指定することで、コンパイル済みのJavaScriptファイルやCSS、画像などをRailsへ送信することができるようになります。
try_filesとは?
try_filesを使えば、指定のファイルorディレクトリが存在しなかった場合に、unicornへリダイレクトするように設定可能です。
今回の場合は、$uri/index.htmlに対する静的コンテンツ(コンパイル済のJavascpirt等)が存在すれば、そのファイルを返すように設定しています。
docker-composeの設定
Rails×Unicorn×Nginxのコンテナを一括管理するために、docker-compose.ymlを作成します。
#(一部省略しています)
version: '3'
services:
db:
image: mysql:5.7
ports:
- 3306:3306
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ✳︎✳︎✳︎✳︎✳︎✳︎✳︎✳︎
app:
build:
context: .
dockerfile: Dockerfile
command: bundle exec rails s -p 3000 -b 0.0.0.0 --environment=production #Railsを本番環境で開始する
tty: true
stdin_open: true
ports:
- 3000:3000
volumes:
- .:/app:cached
- tmp-data:/app/tmp/sockets
- ./public:/app/public
environment:
APP_DATABASE_HOST: db
APP_DATABASE_USERNAME: ✳︎✳︎✳︎✳︎
APP_DATABASE_PASSWORD: ✳︎✳︎✳︎✳︎✳︎✳︎✳︎✳︎
depends_on:
- db
stdin_open: true
tty: true
nginx:
build:
context: ./nginx
dockerfile: Dockerfile
ports:
- 80:80
restart: always
volumes:
- tmp-data:/app/tmp/sockets
- ./public:/app/public
depends_on:
- app
volumes:
db_data:
tmp-data:
あくまで僕がproduction環境でRails動かす場合のコードなので、細かいところは変更して下さい。
Rails×Unicorn×Nginxを動作させるための最終準備
Rails×Unicorn×Nginxをproduction環境で動作させるために、以下の流れでコマンドを実行します。
- $docker-compose build
- $docker-compose up
- $docker-compose run app bin/rails db:create RAILS_ENV=production
- $ docker-compose run app bin/rails db:migrate RAILS_ENV=production
- $docker compose run app bundle exec unicorn -p 3000 -c /app/config/unicorn.rb
①②はDockerイメージをビルドして、コンテナを起動させるためのコマンドです。
③④で、production環境用のDBの作成を行なっています。
⑤は、Unicornを起動させるコマンドですが、docker-compose内のcommandで実行していいと思います。※自分もあとで書きかえる予定です。
【注意】webpackerを使用している場合
最後に、自分がRails×Unicorn×Nginxを設定するのに10時間ほどハマってしまった落とし穴があるので紹介します。
ちなみにwebpackerを使用してJavaScriptを扱っていない人には関係のない話です。
✳︎✳︎✳︎.jsファイルの取得が404エラーになる
ここまでの流れで、無事にNginxとUnicornを繋ぐことが出来たのですが、webpackerで使っていたjsファイルが取得できる、画面が真っ白になるエラーが発生しました。
いくつかの要素が重なって起きたエラーだったので、原因をまとめました。
- 静的ファイルを用意していなかった。
- webpack-dev-serverを起動していた。
通常、development環境では、webpackerがjsなどのコードをコンパイルしてくれています。
しかし、production環境では、コンパイルの時間をかけたくないので、jsファイルなどは事前にコンパイルしてpublicディレクトリに入れておかないといけません。
※Railsではproduction環境でwebpackerによるコンパイルは行われない設定になっています。
jsファイルなどをコンパイルする方法
docker-compose run app rails assets:precompile
上記のコードを実行すれば、publicディレクトリの配下にコンパイルされたJavaScriptのコードなどが出力されます。
同時にmanifest.jsonというファイルも生成されますが、そのなかで定義されたファイルが、production環境で出力されます。
{
"entrypoints": {
"hello_vue": {
"js": [
"/packs/js/✳︎✳︎✳︎-✳︎✳︎✳︎.js"
],
"js.map": [
"/packs/js/✳︎✳︎✳︎-✳︎✳︎✳︎.js.map"
]
}
},
"hello_vue.js": "/packs/js/✳︎✳︎✳︎-✳︎✳︎✳︎.js",
"hello_vue.js.map": "/packs/js/✳︎✳︎✳︎-✳︎✳︎✳︎.map"
}
webpack-dev-server
エラーを繰り返しながら、原因を探っているうちに気がついたのですが、production環境でwebpack-dev-serverは起動してはいけません。
なぜならば、上記のmanifest.jsonのファイル名を勝手に変更されてしまうからです。
webpack-dev-serverが存在しないファイルを指定
自分の場合、webpack-dev-serverがpublic/packs/js配下に存在しないファイルを指定していたので、ファイルのGETに対して404エラーが出ていました。
"GET /packs/js/hello_vue-16116754674fd726dd84.js HTTP/1.1" 404
そもそもproduction環境でwebpack-dev-serverは不要なので、起動しないのが無難ですね。