2014年1月3日 星期五

Unicorn + Nginx 配置及設定

最近部屬公司的新產品 - flyTutor 的時候,嘗試了不同的架構,從以往的 passenger-plugin on Nginx 換成了Unicorn 搭配 Nginx 的「反向代理」架構

What's "Reverse Proxy Model" ?

在提起 Unicorn 及 Nginx 的配置之前,我們得先了解一下「反向代理機制」,下面這張圖應該可以幫助你了解:

(轉自: ROR實戰聖經)


在用戶端向 server 作請求的時候,不是直接與 app server 連結 (中間那三個),而是先連到 web server ,通常是 Apache 或 Nginx,再由 web server 指派 request 給 app server、或直接 serve static assets。
所以在這樣的架構下你可以想像你的系統需要常駐著以下的服務:
  1. web server
  2. 數個 app server
  3. DB server

這樣的架構是與常見的 Apache/Nginx + Passenger 架構有所不同的,需要作一點觀念上的調適。下面我們就來介紹一下如何做相關的配置。


Nginx 配置

以下是預設的 Nginx 設定檔 - nginx.conf:
user deployer; # 定義操作 nginx 的使用者
worker_processes 1; # 定義 worker 的數量

error_log  /var/log/nginx/error.log;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {

  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  access_log    /var/log/nginx/access.log;

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;

  keepalive_timeout  65;

  gzip  on;
  gzip_http_version 1.0;
  gzip_comp_level 2;
  gzip_proxied any;
  gzip_vary off;
  gzip_types text/plain text/css application/x-javascript text/xml application/xml application/rss+xml application/atom+xml text/javascript application/javascript application/json text/mathml;
  gzip_min_length  1000;
  gzip_disable     "MSIE [1-6]\.";

  server_names_hash_bucket_size 64;
  types_hash_max_size 2048;
  types_hash_bucket_size 64;

  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;

}

上面這段預設的設定檔有兩個地方需要注意:
第一行的使用者身分必須為負責 deploy 的用戶,否則常常會出現 403 權限問題,因為未經設定的話 nginx 會沒有權限存取 deployer 的檔案,如 js, css。
第二行的 worker 數量建議別設定超過 cpu 數目,多了也沒好處。

再來就是重頭戲了,我們要開始加入 app server 的設定,這段設定需要加在 http 的括號內:
  upstream unicorn {
    server 127.0.0.1:8080 fail_timeout=0;
  }

  server {
    listen 80 default deferred;
    server_name flytutor.com;
    root /var/www/flytutor/public;

    location ^~ /assets/ {
      gzip_static on;
      expires max;
      add_header Cache-Control public;
    }

    location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;

      if (!-f $request_filename) {
        proxy_pass http://unicorn;
      }

    }

    error_page 500 502 503 504 /500.html;
    client_max_body_size 4G;
    keepalive_timeout 10;
  }


upstream:

首先我們先來講解一下第一部分的 upstream。首先,upstream 後的「unicorn」並非一定的,端看你希望怎麼命名,但是要注意的是 proxy_pass 也需作對應的設定。再來,upstream 內的 server 可以不只一個,如果你有數個 app server 在不同的 host 的話,你也可以做如下的設定:
upstream flytutor.com {
  server 192.168.0.111:8080 weight=3;
  server 192.168.0.222:8080 weight=2;
  server 192.168.0.333:8080 weight=3;
}
其中 weight 代表的是「被配發 request 的權重」,當 weight 越高的時候被分配到的機率越大,可以做 loading balance。

而除了走 TCP/IP 外,如果是在 web server 和 app server 都在同一個主機的情況下,你也可以使用 socket 來做設定,基本上效能會比 TCP/IP 好一點點(因為少了 header):
  upstream unicorn {
    server unix:/tmp/unicorn.sock fail_timeout=0;
  }

server:

接下來我們要介紹一下 server 的設定,基本上這邊指的 server 就是指 app server,一台 nginx server 可以依 server 設定派發 request ,所以可以做 vhost 的應用,例如判斷 request 的不同網域 (設定檔中的 "server_name")、或是同網域但是不同 port (設定檔中的 "listen"),來派發到不同的服務。
proxy_pass 很重要,許多網友都死在這個地方,切記要將他設置成 upstream 的命名,例如 "http://<upstream_name>"。
你可以想像當一個 request 經過 location(path), server_name(domain), listen(port) 重重篩選後,你卻不指派 proxy_pass 的目的地給他,讓這個 request 就此隱沒在數據流中,成了「digital phantom」,你於心何忍?


Unicorn 配置

unicorn 的配置通常是放在 rails project 中的 config 資料夾,長得像這樣:
app_root = "/var/www"
app_name = "flytutor"
listen "127.0.0.1:10101" #, :backlog => 2048 #這邊要跟nginx虛擬主機檔中upstream內定義的務必一樣
worker_processes 2 #看情況開
preload_app false
timeout 30
 
module Rails
  class <<self
    def root
      File.expand_path(__FILE__).split('/')[0..-3].join('/')
    end
  end
end

_working_directory = File.join(app_root, app_name)
working_directory _working_directory
logs_path = "#{_working_directory}/log"
pid "#{_working_directory}/tmp/pids/unicorn.pid"
stderr_path "#{logs_path}/unicorn.stderr.log"
stdout_path "#{logs_path}/unicorn.stdout.log"
 
GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true
 
before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
  
  old_pid = "#{Rails.root}/tmp/pids/unicorn.pid.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      puts "Send 'QUIT' signal to unicorn error!"
    end
  end
end
 
after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

這邊要注意的地方有兩個:

  1. listen 的對象必須與 nginx.conf 中 upstream 設定的一樣,unicorn 與 nginx 就是靠這行設定連起來的。還有,如果是走 TCP/IP 的設定,盡量不要使用 1024 以下的 port 以免 deployer 沒有權限啟動 unicorn 導致失敗(你總不能老用 root 權限作 deploy 吧?)。
  2. unicorn 是採用 master-slave 機制,啟動 unicorn 後叫出  process 列表你就會了解的。而這裡的 worker_processes 的數目顧名思義就是指定 worker 的數目。開多少個才好?基本上沒有一定的標準答案,還是要依你的使用情況及環境做測試後才知道,如果懶得想的話就直接設定為「CPU 數目 + 1」吧。


操作 unicorn

許多網友在網路上搜尋到的啟動法是以 unicorn_rails 來啟動:/usr/bin/unicorn_rails -c config/unicorn.rb -E $RAILS_ENV -D。但現在 unicorn 官方已經不建議使用這種作法而是直接改用 unicorn 來啟動/重啟 unicorn

Hot restart with zero down time

許多人之所以選擇 unicorn 或許就是因為他的 hot restart 特性。如果對這有興趣的人可以參考這篇