2014年1月8日 星期三

Rails 的 i18n fallback 問題

當在 config/application.rb 中指定了 config.i18n.fallbacks 語系順序後,別忘了要 comment 掉 produciton.rb 中的 config.i18n.fallbacks = true
production 下的語系順序才會乖乖實現。

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 特性。如果對這有興趣的人可以參考這篇



2014年1月1日 星期三

vagrant 初體驗與故障排除

1. 30 秒認識 vagrant:

Vagrant 是一個 ruby 寫的工具,它是一個 DSL 讓開發者可以輕易控制 VirtualBox 的 VM 。用它可以輕鬆管理和制作我們理想中的開發環境。
  $ gem install vagrant #安裝 vagrant
  $ vagrant box add ubuntu http://cloud-images.ubuntu.com/vagrant/quantal/current/quantal-server-cloudimg-i386-vagrant-disk1.box #安裝新的 Vagrant Package。這裡的 ubuntu 是一個預先做好的空的 ubuntu 12.10 (intel-based)
  $ vagrant init ubuntu
  $ vagrant up
box 檔可以在 http://www.vagrantbox.es/ 下載


2. 設定:

此時就可以使用 vangrant ssh 連到vm了,預設使用者/密碼為 vangrant/vangrant
接著編輯本地資料夾中的 Vagrantfile,修改 config.vm.network :hostonly, "33.33.33.33" 將ip改成自己想要的ip,這裡以 "33.33.33.33" 為例。修改完後要執行 vangrant reload


3. 故障排除:


無法以 SSH 連接 VM:

[default] Failed to connect to VM!
Failed to connect to VM via SSH. Please verify the VM successfully booted
by looking at the VirtualBox GUI.
當出現上面的錯誤訊息時,可以照 http://vagrant.wikia.com/wiki/Usage 上的步驟來排除,基本上就是將 Vagrantfile 中的 config.vm.boot_mode = :gui 設定打開。再重新 vagrant up,在產生的 GUI 中輸入 sudo dhclient eth0


Guest additions not matchs:

[default] The guest additions on this VM do not match the install version of
VirtualBox! This may cause things such as forwarded ports, shared
folders, and more to not work properly. If any of those things fail on
this machine, please update the guest additions and repackage the
box.


Reference:

http://reality.hk/posts/2011/12/21/vagrant
http://gogojimmy.net/2013/05/26/vagrant-tutorial/