Techracho

RailsサーバUnicornを飼いならす! 運用時の便利技

このエントリーをはてなブックマーク Share
2010.07.28    Ruby, Ruby on Rails, 伊藤   タグ: , , —    tomotaka   

伊藤です。

前回ブログで紹介したRailsサーバUnicornくんを運用し始めて結構時間が経ちました。
サービスを落とさないであるとか、システムの安定性を確保するために、
ちょっとしたユーティリティを作ったり監視ソフトMonitの設定を行ったりしていました。

みなさんのお役に立つかわかりませんが、弊社でUnicornと組み合わせて運用に利用しているツールや設定をブログに掲載してみたいと思います。
もっといいやり方がありましたら、ぜひコメント欄でご紹介頂ければと思います。

ダウンしたら自動的に再起動

これはMonitで行っています。
もちろん同内容の監視ツールGodでも可能だと思いますが、以前設定した経験があって設定が楽そうだったので、Monitでやってみました。(事実楽でした)

check process unicorn with pidfile "/path/to/rails/tmp/pids/unicorn.pid"
  start program = "/home/tomotaka/monitor/unicorn_start" with timeout 10 seconds
  stop program = "/home/tomotaka/monitor/unicorn_stop"
  if 2 restarts within 3 cycles then timeout
  if cpu usage > 95% for 3 cycles then restart

とりあえず, コピペで使う際に変更しなければいけない箇所は

  • check processのあとの監視タスク名(なんでもよし, unicornを1個しか走らせてないならunicornでいいかも?)
  • start program(後述)
  • stop program(後述)

お分かりかと思いますが、Unicorn起動/停止のためのコマンドは自作しました。
startは簡単なシェルスクリプトです。

#!/bin/bash
UNICORN_RAILS_BIN=/usr/local/bin/unicorn_rails
MY_RAILS_ROOT=/path/to/rails
MY_UNICORN_CONFIG=config/unicorn.rb
MY_RAILS_ENV=production

pushd $MY_RAILS_ROOT
$UNICORN_RAILS_BIN -c $MY_UNICORN_CONFIG -E $MY_RAILS_ENV -D
popd

簡単ですね。コピペで使う際は各変数を書き換えればオッケー。しいて言えばMY_UNICORN_CONFIGはRAILS_ROOTからの相対パスであることに注意でしょうか。(ディレクトリを移動してからコマンドを発行してるため) こういう簡単なツールを組み合わせて便利に使えるのがコンピュータのいいところですね。

unicorn_stopはUnicornのマスタープロセスのPIDをしらべて、それに対してQUITシグナルを送ればよいですね。シェルスクリプトでも出来るシンプルな内容ですが、unicornのプロセス制御のためのライブラリを作ったので、それを使ってやってます。(ライブラリについては後述)

require File.expand_path("./unicorn_manager.rb", File.dirname(__FILE__))

rails_root = "/path/to/rails"
pids = UnicornManager.get_unicorn_pids(rails_root)

puts "Sending signal to unicorn master [pid=#{pids[:master]}]"
Process.kill :QUIT, pids[:master]
puts "OK"

unicorn_manager.rbというのがライブラリですね。UnicornManager.get_unicorn_pidsというメソッドでUnicornのPID情報をハッシュ形式で返してくれるので、その情報をもとにProcess.killでシグナルを送ってます。unicorn_manager.rbについては、次の自動再起動の項で触れます。unicorn_manager.rbを同じディレクトリにおいて、rails_root変数を書き換えれば動作するはずです。

とりあえず、ライブラリunicorn_manager.rbと、上記のstart/stopコマンド2点があれば

  • unicornが突然死したら自動的に起動
  • unicornがCPU食い過ぎてたら自動的に再起動
  • unicornがメモリ食い過ぎてたら自動的に再起動

などのタスクがMonitだけで完了します。
その他の複雑な条件も設定できる懐の深さがMonitにはありますので、ぜひ一度Monitのドキュメントに目を通してみてください。

これでとってもハッピーになれそうな感じですが、現時点のMonitにはstop programとstart programしかなく、”再起動”も”停止” => “起動”で実現される点が気になりました。というのもUnicornを使用するメリットのひとつはダウンタイムを作らずに子プロセスを新しく生まれ変わらせることができる点です。そのため、メモリを食べ過ぎて太っちゃった子プロセスにQUITシグナルを送るプログラムを先ほどunicorn_manager.rbというプログラムを作成してcronで利用しています。

メモリ使用量の多い子プロセスを定期的にrespawn

というわけで, fat-memory-process-killer.rbです。物騒な名前ですね。

#!/usr/local/bin/ruby
require File.expand_path("./unicorn_manager.rb", File.dirname(__FILE__))
require "logger"

logfile = "/path/to/logdir/fat-memory-process-killer.log"
loglevel = Logger::DEBUG # Logger::INFO if this script get enough stable
rails_root = "/path/to/rails"
threshhold = 40 # MB

# ----- end of config -----

puts "fat-memory-process-killer started at #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}"

logger = Logger.new(logfile)
logger.level = loglevel
logger.info "----- Start -----"
memory = UnicornManager::get_memories(rails_root)
logger.debug "** rails_root=#{rails_root}"
logger.debug "** threshhold=#{threshhold}MB"
logger.info "Got PIDs: m=#{memory[:master].keys} c=#{memory[:children].keys.join(",")}"
mpid = memory[:master].keys[0]
mmem = memory[:master][mpid]
logger.debug sprintf("** master-memory: % 7d => %4.2fMB", mpid, mmem.to_f/(1024*1024))

# kill fat child
memory[:children].each do |pid, mem_size|
  logger.debug sprintf("** child-memory:  % 7d => %4.2fMB", pid, mem_size.to_f/(1024*1024))
  if 1024*1024*threshhold < mem_size then
    logger.info "Sending QUIT signal to child(pid:#{pid}) memsize=#{mem_size} > threshhold(#{1024*1024*threshhold})"
    Process.kill :QUIT, pid
  end
end

logger.info "Finish"

デーモンにすると面倒なことが多いので、5分間隔でcronで実行しています。コピペで利用するには、 end of configより上の部分を書き換えて、unicorn_manager.rbを同じディレクトリに置けば動作するはずです。このプログラムの働きぶりを観察するためにloggerでログに結果を出力していますが、興味がなければlogger関係のコードは削ってしまってもいいかもしれません。

いちおうこんな感じで報告されます。使用メモリが指定した40MBを超えたプロセスが3ついたので、QUITされてるようです。

I, [2010-07-27T11:00:02.734795 #1161]  INFO -- : ----- Start -----
D, [2010-07-27T11:00:02.994152 #1161] DEBUG -- : ** rails_root=/var/www/music-fly.net/webservice2
D, [2010-07-27T11:00:02.994369 #1161] DEBUG -- : ** threshhold=40MB
I, [2010-07-27T11:00:02.994444 #1161]  INFO -- : Got PIDs: m=13550 c=31905,748,30108,980,32380,30389,29861,981,745,31898,30105,746,31679,30106,747,417
D, [2010-07-27T11:00:02.994639 #1161] DEBUG -- : ** master-memory:   13550 => 30.49MB
D, [2010-07-27T11:00:02.994680 #1161] DEBUG -- : ** child-memory:    31905 => 35.09MB
D, [2010-07-27T11:00:02.994720 #1161] DEBUG -- : ** child-memory:      748 => 33.98MB
D, [2010-07-27T11:00:02.994758 #1161] DEBUG -- : ** child-memory:    30108 => 34.14MB
D, [2010-07-27T11:00:02.994795 #1161] DEBUG -- : ** child-memory:      980 => 46.07MB
I, [2010-07-27T11:00:02.994830 #1161]  INFO -- : Sending QUIT signal to child(pid:980) memsize=48304128 > threshhold(41943040)
D, [2010-07-27T11:00:02.994897 #1161] DEBUG -- : ** child-memory:    32380 => 31.07MB
D, [2010-07-27T11:00:02.994936 #1161] DEBUG -- : ** child-memory:    30389 => 36.44MB
D, [2010-07-27T11:00:02.994973 #1161] DEBUG -- : ** child-memory:    29861 => 45.95MB
I, [2010-07-27T11:00:02.995007 #1161]  INFO -- : Sending QUIT signal to child(pid:29861) memsize=48177152 > threshhold(41943040)
D, [2010-07-27T11:00:02.995049 #1161] DEBUG -- : ** child-memory:      981 => 31.05MB
D, [2010-07-27T11:00:02.995086 #1161] DEBUG -- : ** child-memory:      745 => 31.05MB
D, [2010-07-27T11:00:02.995123 #1161] DEBUG -- : ** child-memory:    31898 => 34.82MB
D, [2010-07-27T11:00:02.995160 #1161] DEBUG -- : ** child-memory:    30105 => 34.49MB
D, [2010-07-27T11:00:02.995197 #1161] DEBUG -- : ** child-memory:      746 => 45.10MB
I, [2010-07-27T11:00:02.995231 #1161]  INFO -- : Sending QUIT signal to child(pid:746) memsize=47288320 > threshhold(41943040)
D, [2010-07-27T11:00:02.995272 #1161] DEBUG -- : ** child-memory:    31679 => 31.50MB
D, [2010-07-27T11:00:03.057719 #1161] DEBUG -- : ** child-memory:    30106 => 36.51MB
D, [2010-07-27T11:00:03.057796 #1161] DEBUG -- : ** child-memory:      747 => 31.52MB
D, [2010-07-27T11:00:03.057835 #1161] DEBUG -- : ** child-memory:      417 => 31.05MB
I, [2010-07-27T11:00:03.057998 #1161]  INFO -- : Finish

ダウンタイムなしでコードをリロードするコマンド

Railsのproduction環境では、新しいコードを動作中のサーバに反映するにはいったんサーバを再起動しないといけません。Unicornもダウンタイムこそ無いものの、マスタープロセスのpidを調べて, USR2シグナルを送るという作業が必要になります。UNIX界で長年暮らしてらっしゃる方はそんなもんpsやawk組み合わせたシェルスクリプトで一発だろ、って感じかと思いますが、僕は軟弱者なのでメモリ食い過ぎプロセスを殺すためにつくったライブラリを利用して、Rubyで作りました。unicorn_reloadコマンドです。

#!/usr/local/bin/ruby

require File.expand_path("./unicorn_manager.rb", File.dirname(__FILE__))

rails_root = "/path/to/rails"
pids = UnicornManager.get_unicorn_pids(rails_root)

puts "Sending signal to unicorn master [pid=#{pids[:master]}]"
Process.kill :USR2, pids[:master]
puts "OK"

unicorn_stopのところで紹介したプログラムとシグナルの名前しか変わってないですね…これはひどい。

unicorn_manager.rb

unicorn_manager.rbのコードも貼付けてやろうかと思いましたが、ちょっと長いのでダウンロードリンクだけにしておきます。

その代わりといっては何ですが、unicorn_manager.rbの機能を紹介しておきます。

  1. Unicornのpidリストを得る: UnicornManager.get_unicorn_pids(”/path/to/rails”) => {:master => 123, :children => [124,125,126,127...] }
  2. Unicornに設定を再読込みさせる(マスターにHUPシグナルを送る): UnicornManager.reload_config(rails_root)
  3. Unicornにプログラムを再読み込みさせる(マスターにUSR2シグナルを送る): UnicornManager.reload_code(rails_root)
  4. Unicornの子プロセスを全て再起動させる(各子プロセスに順番にQUITシグナルを送る): UnicornManager.restart_all_child(rails_root)
  5. Unicornのメモリ使用量をプロセスごとに得る: UnicornManager.get_memories(rails_root) => {:master => {123 => 1000000}, :children => {124 => 1000000, :125 => 1000000, …}}

動作要件:

  • /path/to/railsがRAILS_ROOTとして与えられたとき、/tmp/pids/unicorn.pidにunicornのpidがあること
  • psの出力フォーマットがLinux互換であること

書いてて気づきました。unicorn_stopとunicorn_reloadのコマンドは専用のメソッドがあるじゃないか… もっと短くできますね。

まとめ

Unicornが落ちないような仕組みをMonitで構築した。Unicornの子プロセスがメモリ食い過ぎたらrespawn(日本語でいい表現を思いつかない、再起動とはちょっと違うような…)する仕組みをcronとオリジナルスクリプトで実現した。開発の際に作ったライブラリを使ってコードでプロイ時にも楽できるスクリプトとかも作った。

それではみなさん快適なRailsライフを!

Unicornシリーズ前の記事: 次世代RailsサーバーUnicornを使ってみた

次世代RailsサーバーUnicornを使ってみた

このエントリーをはてなブックマーク Share
2010.07.09    Ruby, Ruby on Rails, 伊藤   タグ: , , , —    tomotaka   

2010.07.20追記: prefixを指定した運用も可能でした。ご指摘頂きありがとうございます。
2010.07.28追記: 関連記事「RailsサーバUnicornを飼いならす! 運用時の便利技」へのリンクを張りました。

伊藤です。

Railsサーバはたくさんあってややこしいですね!
最近さらにUnicornというものが頭角を表してきたようで、Twittergithubも使っているようなので使ってみましたので、特徴や使い方などレポートしてみたいと思います。
このブログの他にもEngine Yardのブログ記事「Everything You Need to Know About Unicorn」やgithubの記事「Unicorn!」が非常に参考になると思いますので、あわせてご覧ください。
(そもそもUnicornは用途をRailsに限定しない汎用のRackアプリケーションサーバです。タイトルは煽り気味ですね。すいません。)

ざっくりと、Unicornのアーキテクチャとそれにまつわるメリットデメリットをリスト形式で。

  • thinやmongrelみたいなマルチプロセスによるclusterではなく、forkを使ったmaster-slave
    • マルチプロセスモデルよりメモリ効率がいいかも?(copy-on-write)
    • ふくれあがったメモリ食い過ぎプロセスを殺しても、サービスにダウンタイムが発生しない
      • Monitとかでふくれあがったプロセスに対してQUITを送ると、そいつは処理中のリクエストを処理したら死ぬ
        • 親がそいつの代わりをすぐrespawnする
    • デプロイが早い
    • デプロイ時のダウンタイムがない
  • apache => app-server-clusterという風なpush requestではなくapache <= unicornというpullリクエスト
    • slaveプロセスが共通のソケットを通じてリクエストを受け取る
    • ひまなプロセスが処理を開始する(ソケットからリクエストを取り出す)
    • 処理中のもっさりアプリケーションサーバにあたることがない
      • もっさりサーバは処理がおわってないのでリクエストを取りにいくことがないから

それでもって、以下が実際に使ってみた印象です。

  • CPUあんまり食わない
  • メモリあんまり食わない
  • 確かにデプロイ(起動/再起動)早い。
  • たまに重いアクションを叩かれるような場合ではそのリクエストを処理しているworkerにあたることがなくなるので、全体のスループットを向上できる?
  • prefixを指定した運用ができない prefixを指定して運用可能です:unicorn_railsの–pathオプションで指定できます。

RailsアプリをMongrelやThinのクラスターで運用するとメモリをたくさんお食べになられるのが、結構悩みのタネだと思います。
スモールスタートのプロジェクトではフロントのロードバランサ、アプリケーションサーバ、DBサーバも全て1台でやるのが経済的理由からあたり前ですので、ThinやMongrelがメモリを食いまくるからといってサーバをもう1台追加しなくてはならないようでは積極的に使いにくいですよね。
しかしこのメモリ食いまくる現象にも理由はあり、Engine Yardのブログ記事「That’s Not a Memory Leak, It’s Bloat」でもActiveRecordのインスタンスが大量生成され、Rubyがそれを解放しないからふくれあがってしまうという主な理由が説明されていました。
もちろん優れたハッカーが多数いるRailsコミュニティでは対策も当然あり、Monitやそれに似たRubyベースのモニタリング(&再起動)ツールGodなどを駆使してCPUを使いすぎていたり、メモリを使いすぎているインスタンスに対して自動的に再起動をかけるのが一般的です。

しかしこのモデルでは以下のような一般的な構成で、サービスの安定性を追求した際に問題があります。

ロードバランサとThinクラスタ

ロードバランサは重み付けなしの設定を行うと、リクエストごとに、バックエンドのアプリケーションサーバに対して順番にリクエストを行います。ここで3番目(右はじ)のサーバに対して「レスポンスを返すまで時間がかかる地雷アクション」へのリクエストが来たとします。

クラスタの一部が重くなってる状態

このまま6回目のアクセスがくると、6回目のリクエストには3回目のリクエストが終わってないので処理が返せません。
こ6回目のアクセスがくる前に、左側のthinインスタンスと真ん中のthinインスタンスが4回目のリクエストと5回目のリクエストの処理を終えていれば、処理を肩代わりしてほしいところですが、リクエストの振り分けはapacheが上流で行っているので、難しいという状況です。

これと同じことがプロセスの再起動時にもおこります。ここで同様に右端のインスタンスが激重アクションによりメモリ使用量が爆増し、監視しているMonitなりGodなりが再起動をかけているとします。Railsのスタックのロードには1〜2秒程度時間がかかるので、アクセス数の多いサイトだったらこの間にリクエストをapacheから振られる可能性はゼロではありません。

再起動中のインスタンスがある場合

この例では右端のインスタンスが再起動を開始した1秒以内に3つのアクセスがくると、3つ目が準備が終わっていない3つめのインスタンスに来てしまいます。ここでは直感的には1つめの左端のインスタンスが、リクエストが終わり次第処理してほしい気がします。

ここでUnicornのロードバランシングのアーキテクチャを図にして見ました。

Unicornのアーキテクチャ

Unicornでは、MasterとSlave(図ではchildと書いています)がおり、Masterは起動するとSlaveをforkして生産します。mongrelをたくさん立ち上げるのと同じイメージですね。そしてApacheからのロードバランシングでは、Apache(ロードバランサ)が接続する先はMaster1つで、Unicornの内部でMasterとSlaveがリクエストをやりとりし、Slaveが処理した結果をMaster経由でApache(ロードバランサ)に返します。このやりとりというのがミソで、ひまになったSlaveがMasterに対して「次に処理するリクエストをくれ」という風にPULLしにくるのです。Masterは一番最初にpullしてきたSlaveに処理を行わせるだけでよいですね。こうすると、なにがうれしいかというとさっきのような例で「重い処理をしている」「再起動中」といったプロセスがリクエストの処理を担当することが原理的に起こりえません。このような状態では処理すべきリクエストをmasterに対してpullすることができないからですね。

また、forkを使うことによってOSのcopy-on-write機能により実際に使う実メモリ使用量が減る、Railsのコードをロードしなおさなくてよいなどのメリットがあるようです。

また、プログラムを更新する際にダウンタイムを作らない仕組みをうまく作っているのも面白いです。MongrelやThinクラスターをproductionモードで動かしている場合はクラスタ全体のの再起動が必要で、1〜2秒 * インスタンス数という時間が必要です。慌てて何度もやっているとサービスに影響が出てしまいますし、そもそもアプリケーションサーバがたくさんあるような場合では時間がかかってめんどくさいですね。Unicornではこの辺もエレガントに解決できており、UnicornのマスタープロセスにUSR2シグナルを送ると、もうひとつmasterプロセスを作って引き継ぎを開始します。新しいmasterプロセスは古いmasterプロセスから、上流のロードバランサと通信しているポートの引き継ぎを行います。古いmasterプロセスはUSR2シグナルを送っただけでは死なないのですが、後半に載せてあるgithubに公開されている設定スクリプトのように古いmasterに対してQUITシグナルを送る処理を自動化することも可能です。USR2シグナルを使って新しいmasterを起動するときはだいたい古いmasterはいらないと思われるので、before_forkでQUITシグナルを送ってしまうのがよいでしょう。

$ sudo kill -USR2 (masterのpid)

だけですみます。楽ですね!しかも今までthinインスタンス10個でクライアントの処理が完了するのを待っているのを含めて10秒以上かかっていたのがものの2〜3秒でできてしまいました。

という感じで、非常にいい感じのUnicornくんですが、日本語ではまだあまり情報がないので以下にハウツーとして実際に使えるコマンドや設定などを列挙していきたいと思います。

インストール

$ sudo gem install unicorn

※Windowsではまともに動かないっぽいです

起動

# productionモード(-E), デーモン化する(-D), 詳細をconfigファイルで指定
$ cd RAILS_ROOT
$ cd unicorn_rails -c config/unicorn-config.rb -E production -D

-Dオプションでデーモン化を指定しない場合フォアグラウンドプロセスとして動き、production.logに記録される内容も標準出力として出力されます。フォアグラウンドプロセスとして動いている場合、Ctrl+Cでunicorn masterと全てのunicorn workersの動作を停止させることができます。

設定ファイル(ここではconfig/unicorn-config.rbとして指定されている)の書き方については後述。

停止

そもそもUnicornではダウンタイムなしで新しいコードをproductionモードでデプロイできるので、停止の必要はめったにありませんので注意。
Unicornにおいては設定最読み込みとどうようにhogehoge stopというようなコマンドはなく、INT(TERM)/QUITシグナルを送ることにより、停止させます。QUITシグナルはgraceful shutdownで、全てのworkerがリクエストの処理を終えるのを待ちます。INTまたはTERMシグナルを送った場合は、すぐにworkerプロセスを全て皆殺しにします。(quick shutdown)

# unicornのmasterプロセスのIDを特定する
$ sudo pgrep -f 'unicorn_rails master'
12345

# graceful shutdown: 現在処理中の全てのリクエストの処理が終わるのをまってからシャットダウン
$ sudo kill -QUIT 12345

# quick shutdown: 現在処理中の全てのリクエストの処理を中断し、シャットダウン
$ sudo kill -INT 12345

設定再読み込み

Unicornにおいては設定を再読み込みさせるにはhogehoge reloadというようなコマンドはなく、HUPシグナルを送ることによって実現される。シグナルを送る先はunicornのmasterプロセス。masterのプロセスID(pid)を特定するには以下のようにする。
preload_app(デフォルトfalse)をtrueにしていると、プログラムコードはリロードされません。(逆にいえばpreload_appがfalseのままならmasterにHUPを送るとリロードされる)

# unicornのmasterプロセスのIDを特定する
$ sudo pgrep -f 'unicorn_rails master'
12345

# masterプロセスにHUPシグナルを送る
$ sudo kill -HUP 12345

プログラムのデプロイ

手順は公式のSIGNALのページに詳しく書いてあります。

# unicornのmasterプロセスのIDを特定する
$ sudo pgrep -f 'unicorn_rails master'
12345

# masterプロセスにUSR2シグナルを送る
$ sudo kill -USR 12345

再起動

先にも述べましたが、そもそもUnicornではダウンタイムなしで新しいコードをproductionモードでデプロイできるので、停止の必要はめったにありません。
それでも再起動するなら、停止→起動ですね。

設定ファイルの書き方

まずは、公式で参考として配布されている2つを見てみてフィーリングをつかむのがよさそうです:

githubのブログ記事で公開されているものを改変して、うちで使っているものも公開しちゃいます。

$default_env = "production" # デフォルトRailsEnv
$unicorn_user = "bps" # slaveの実行ユーザ
$unicorn_group = "bps" # slaveの実行グループ

$dev_processes = 4 # dev環境用子プロセス数
$prod_processes = 16 # 本番環境用子プロセス数

$timeout = 75 # タイムアウト秒数。タイムアウトしたslaveは再起動される

# String => UNIX domain socket / FixNum => TCP socket
#$listen = "/home/bps/tmp/unicorn.sock"
$listen = 5000

# ---- end of config ----

# Main Config for Unicorn
rails_env = ENV['RAILS_ENV'] || $default_env
worker_processes (rails_env == 'production' ? $prod_processes : $dev_processes)
preload_app true
timeout $timeout
listen $listen, :backlog => 2048

# For RubyEnterpriseEdition: http://www.rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
if GC.respond_to?(:copy_on_write_friendly=)
  GC.copy_on_write_friendly = true
end

# workerをフォークする前の処理
before_fork do |server, worker|
  ##
  # When sent a USR2, Unicorn will suffix its pidfile with .oldbin and
  # immediately start loading up a new version of itself (loaded with a new
  # version of our app). When this new Unicorn is completely loaded
  # it will begin spawning workers. The first worker spawned will check to
  # see if an .oldbin pidfile exists. If so, this means we've just booted up
  # a new Unicorn and need to tell the old one that it can now die. To do so
  # we send it a QUIT.
  #
  # Using this method we get 0 downtime deploys.

  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
      # someone else did our job for us
    end
  end
end

# workerをフォークしたあとの処理
after_fork do |server, worker|
  ##
  # Unicorn master loads the app then forks off workers - because of the way
  # Unix forking works, we need to make sure we aren't using any of the parent's
  # sockets, e.g. db connection

  ActiveRecord::Base.establish_connection
  #CHIMNEY.client.connect_to_server
  # Redis and Memcached would go here but their connections are established
  # on demand, so the master never opens a socket

  ##
  # Unicorn master is started as root, which is fine, but let's
  # drop the workers to git:git

  begin
    uid, gid = Process.euid, Process.egid
    user, group = $unicorn_user, $unicorn_group
    target_uid = Etc.getpwnam(user).uid
    target_gid = Etc.getgrnam(group).gid
    worker.tmp.chown(target_uid, target_gid)
    if uid != target_uid || gid != target_gid
      Process.initgroups(user, target_gid)
      Process::GID.change_privilege(target_gid)
      Process::UID.change_privilege(target_uid)
    end
  rescue => e
    if RAILS_ENV == 'development'
      STDERR.puts "couldn't change user, oh well"
    else
      raise e
    end
  end
end

nginxをロードバランサにした場合の設定例

upstream backend-unicorn {
        server 192.168.xxx.xxx:3000;
}

server {
        listen   80;
        server_name hogehoge.bpsinc.jp;

        access_log  /var/log/nginx/hogehoge.bpsinc.jp.access.log;

        location / {
                proxy_pass_header Server;
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-Host $host;
                proxy_set_header X-Forwarded-Server $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                #proxy_set_header X-Geo $geo;
                proxy_read_timeout 75; # unicorn設定ファイルのtimeoutも忘れずに
                proxy_pass http://backend-unicorn; # upstreamで定義したバックエンド

                # 通常と違うポートでフロントサーバ(ロードバランサ)を動かしているときはこれが必要
                #proxy_redirect http://hogehoge.bpsinc.jp/ http://hogehoge.bpsinc.jp:8080/;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
                root   /var/www/nginx-default;
        }
}

参考リンク

どうでしたでしょうか?
BPSでもまだUnicornはノウハウが十分に蓄積されていませんが、積極的に使っていってノウハウをためていこうと思います。

運用時のTipsについて、続編を書きましたので合わせてごらんください:RailsサーバUnicornを飼いならす! 運用時の便利技

※7/13追記: USR2シグナルをmasterプロセスに送ったときの挙動の記述について、一部間違いがあったので修正。古いmasterは自動では死にませんね… あとnginxでの設定例を追加してみました。

Rubyでマルチスレッドプログラミング

このエントリーをはてなブックマーク Share
2010.07.05    Ruby, 伊藤   タグ: , —    tomotaka   

伊藤です。

RubyはWeb界隈でよく使われてるスクリプト言語の中では比較的簡単に割と本格的なマルチスレッドプログラムを書くことができます。
うまく使うとI/O待ちで遅くなっているが、必ずしも順番に行う必要のないプログラムの実行時間を短縮したりすることができます。(たくさんのURLにアクセスしてレスポンスを取得してくるクロウラーなど)
しかし現在最も広く使われているRuby1.8ではスレッドの実装はいわゆるグリーンスレッドという実装で、OSによるスレッドを使えないため、マルチコアの恩恵を受けることができないので、大量のデータをマルチコアで処理する目的などで使っても効果が薄い(どころかまったくない)可能性が高いです。
Ruby1.9ではRubyでスレッドの動きを制御しているものの、ネイティブスレッドベースでの動作となっているため、マルチコアの恩恵を受けることができます。

それではさっそく、配列の各要素を並列処理できるルーチンを貼付けてみます。
これは一番シンプルな実装で、配列に含まれる要素分だけスレッドを新規に立ち上げて、ガンガン処理させるというものです。

def multi_process(ary)
  threads = []
  ary.each_with_index do |e, i|
    threads << Thread.start(e, i){|tle, tli| yield(tle, tli) }
  end
  threads.each{|t| t.join }
end

# --- sample
a = [1, 2, 3]
multi_process(a) do |item, index|
  sleep 0.1*rand(10)
  puts "[#{index}]" + (item * 2).to_s
end

しかし、この実装では配列の個数の数だけスレッドが走ってしまうので、1つずつの要素に対する処理が重くて個数が多い場合、大量の要素を処理する場合Rubyが固まったようになってしまうでしょう。それにスレッドの切り替えや、実行チェックなどに多くのCPUタイムを取られてしまって実際の処理が思ったより進まないと思われます。この場合マルチスレッドプログラムにおけるProducer-Consumerパターンを適用して、Consumer(Worker)スレッドの数を限定することで同時にN個のスレッドしか動作させないことを保証することで並列性と処理効率のバランスを図ることができそうです。

Producer-Consumerパターンについて軽く解説すると、実際に処理を行うConsumer(Worker)スレッドをN個走らせて、これらに与えるタスクはブロッキングキューを使ってProducerスレッドから提供されます。ブロッキングキューは同期処理されたキューで、同時にアクセスされてもデータ構造が壊れることなく、正確に1つエンキュー(キューに追加)したり正確に1つデキュー(キューから取り出し)することができます。バグのないブロッキングキューを実装するのは思い出したり調べたりしないといけなくて意外と面倒だったりするのですが、rubyでは標準ライブラリthreadをrequireすることでQueueやSizedQueueといったクラスで利用することができます。SizedQueueではキューの最大の長さを指定することができ、キューが指定された大きさの状態でエンキューしようとすると他のスレッドからデキューされるまでエンキューメソッドはブロックします。

require "thread"

def multi_process2(ary, concurrency = 10, qsize = nil)
  q = (qsize) ? SizedQueue.new(qsize) : Queue.new

  producer = Thread.start(q, concurrency){|p_q, p_c|
    ary.each_with_index do |item, index|
      q.enq [ item, index, true]
    end

    p_c.times{ q.enq [nil, nil, false] }
  }

  workers = []
  concurrency.times do
    workers << Thread.start(q){ |w_q|
      task, index, flag = w_q.deq
      while flag
        yield task, index
        task, index, flag = w_q.deq
      end
    }
  end

  producer.join
  workers.each{|w| w.join }
end

# --- sample
a = [1, 2, 3]
multi_process2(a) do |item, index|
  sleep 0.1*rand(10)
  puts "[#{index}] #{item*2}"
end

こんなかんじになりました。
やはり多少コードは増えてしまいましたね。
multi_process2メソッドの第2引数では並列度としてConsumerスレッドの数を指定できるようにし、第3引数ではキューの長さを指定できるようにしました。キューの長さは明示的に指定しない場合無限(Queue.new)にするようになっています。

こんな感じで、案外簡単にマルチスレッド動作するプログラムが書けてしまいます。
ここで最後に、マルチスレッド初心者の方向けではありますが、基本的な落とし穴をご紹介しておきます。

以下の1〜10000の数をマルチスレッドで全て合計するプログラムを作ってみました。
数学的には1..nの合計はn(n+1)/2で求められることがわかっていますので、50005000が答えになるはずです。
しかし何回かこのプログラムを走らせると答えがおかしくなることがあります。

a = (1..10000).to_a
total = 0
multi_process2(a) do |item, index|
  total += item
end
puts total

これはtotalを読み書きしているdo〜endの部分で、total += itemという1文の命令が、実際には
1. totalを読み出す
2. (1)で読み出したtotalの値にitemの値を足す
3. (2)で計算した値をtotalに代入する
という分割されたフェーズで実行されることが原因です。
これが各スレッドで、まとまった固まりとして実行されず、互い違いに実行されるとおかしくなります。
これを防ぐには、一定の文の固まりを互い違いに実行させない仕組みが必要です。
RubyではMutexクラスのオブジェクトを使って、排他制御を行うことでこれを実現できます。

a = (1..10000).to_a
total = 0
lock = Mutex.new
multi_process2(a) do |item, index|
  lock.synchronize{ total += item }
end
puts total

これで答えがおかしくなることはなくなりました。
マルチスレッドから変数を操作する際には排他制御に気を配りましょう!
しかし、このスレッドのように並行実行しようとしている全てのコードが排他制御されているようでは本質的にマルチスレッドで実行する意味がまったくなくなってしまうので注意ですね。

true/false一覧君

このエントリーをはてなブックマーク Share
2010.06.12    C#, C++, PHP, Python, Ruby, java, javascript, プログラミング言語, 馬場   タグ: —    baba   

当然ながら、言語ごとに真偽判定(true/false)の基準が違います。

特にスクリプト系の言語では、明示的に型を指定しないことが多いので、たまに問題になります。

たまたま手元にあった環境で、どんな値が真に判定されるのか、というのをまとめてみました。
新しい言語に突撃するときのメモ代わりにでもして頂ければ幸いです。

検証コードイメージ

a = 0
if a
  print 'true'
else
  print 'false'
end

truefalse

凡例:
× は、コンパイルエラー
ー は、言語仕様にその値が存在しない

備考:
*”" というのは、ナル文字の値を指す(\0なので、実体はゼロという整数値)
array[] というのは、各言語での空の配列を指す
※1 C言語にfalseは存在しない
※2 配列のアドレスを指すため、真と評価される
※3 noticeが発生するが、設定や@で抑制可能

こうしてみると、やはりPHPは変態です。’0′ をfalseと判定する言語はあまりないですね。Webに特化しているのがよくわかります。$_GETなどは文字列で渡ってくるので、確かに手抜きに便利仕様です。

空配列をfalseと判定するのが、PythonとPHPというなんだかおもしろい組み合わせになりました。
良く忘れるけど、rubyはゼロという数字をtrueに判定するので注意が必要ですね。

結局、ミスのしようが無い Java / C# は安全で好きです。
また、ゼロは false、ほかは true と割り切ったC言語も、シンプルで好きです。

Railsでdevelopmentモードでもエラー情報が出ない

このエントリーをはてなブックマーク Share
2010.06.08    Ruby, Ruby on Rails, 馬場   タグ: , —    baba   

Railsではエラーが発生した際、developmentモードではstacktraceなどが詳細に表示され、productionモードでは詳細が全部隠れて表示されます。

プロダクションモード

プロダクションモード

デベロップメントモード

デベロップメントモード

しかし、developmentモードで動作しているはずなのに、詳細なエラーが出ないことがあります。

DBへの接続でエラーになった際は、productionと同じ画面が出たことがありました。

他にありがちなのは、エラー詳細画面をrenderする際のエラーです。

今回は、エラーログに、以下のようなエラーが出ていました。

ActionView::TemplateError (wrong number of arguments (1 for 2)) in C:/ruby/lib/ruby/gems/1.8/gems/actionpack-2.3.5/lib/action_controller/templates/rescues/_request_and_response.erb:

-e:2:in `load’
-e:2

Rendered rescues/_trace (42.0ms)
/!\ FAILSAFE /!\ Fri May 28 11:45:52 +0900 2010
Status: 500 Internal Server Error

エラー画面は
/lib/action_controller/templates/rescues/_request_and_response.erb:
ですが、このテンプレートの中では、debugなどの関数を使っています。

今回の原因は、自前で「引数2個の」debug関数を作っていたことでした。
上記エラーからそれが読み取れます。

関数の名前を変えたら、ちゃんとした画面が出ました。
当然、直ってもエラー画面なんですけどね・・・

ありがちな名前を付けないように気をつけようというお話です。
フレームワークが変な名前を予約しないで欲しい・・・ typeとか。

RubyのBase64.encode64とキャッシュ

このエントリーをはてなブックマーク Share
2010.06.07    Ruby, 馬場   タグ: , —    baba   

rubyでBASE64エンコードするだけなら、

require 'base64'
Base64.encode64 'hogehoge'

で良いのですが、なぜか改行を付けてくれる親切設計です。

BASE64してファイル名にすること等も多いと思うので、

Base64.encode64('hogehoge').chomp.sub('/', '_').sub('+', '-')

のようにまとめておくと良さそうですね。

ということで、手抜きのキャッシュモジュールです。
色々穴だらけですが。

module Datacache

  #キャッシュから取得する
  def cache_read(key)
    begin
      filename = get_filename key
      if File.exists? filename
        fp = File.open(filename)
        return Marshal.load(fp)
      end
    rescue
    ensure
      fp.close if fp
    end

    return nil
  end

  #キャッシュに書き込む
  def cache_write(key, value)
    begin
      filename = get_filename key
      fp = File.open(filename, 'w')
      Marshal.dump(value, fp)
    rescue
    ensure
      fp.close if fp
    end
  end

  #キー名からファイル名を返す
  def get_filename(key)
    file = Base64.encode64(key).chomp.sub('/', '_').sub('+', '-')
    return ::RAILS_ROOT + '/tmp/datacache/' + file
  end

end

Rails image_pathの動作が違う

このエントリーをはてなブックマーク Share
2010.06.05    Ruby, Ruby on Rails, 馬場   タグ: —    baba   

Railsで画像のパスを取得したいときは、image_pathを使います。

<%= image_tag 'test.png' %>
<%= image_path 'test.png' %>

これらのヘルパーメソッド、Viewの中で使う分には問題なく動くのですが、Controller内で使うと、prefixが付かない問題が発生します。

たとえば、http://example.com/myapp/ をアプリのルートにしているとき、

View内で使用: /myapp/images/test.png
Controller内で使用:/images/test.png

このように戻り値が違います。

完全にバグとしか言えないのですが、とりあえず修正しないことには仕方ないので、

prefix = ActionController::Base.relative_url_root
path = image_path 'test.png'
return prefix + path

みたいな処理を行うことにしました。ControllerとView両方で呼び出すメソッドでは、判定式も必要になりそうです。

Railsはprefix周りで問題が多いですね。
サーバに明示的な引数まで指定してこれですか・・・と

何も指定しなくても勝手にやってくれるCakePHPは、実はすごく優秀な子だったんだと見直すばかりです。

windowsでacts_as_paranoidをインストール

このエントリーをはてなブックマーク Share
2010.06.04    Ruby, Ruby on Rails, 馬場      baba   

Railsの必須プラグインの一つ、論理削除を実現するacts_as_paranoidですが、Windowsだと上手くインストールできないことがあります。

gitをちゃんと設定すればいけるはずですが、ダウンロードした方が早いです。

http://github.com/technoweenie/acts_as_paranoid の上部から、「download」ができます。

ZIPを解凍して、以下のようなフォルダ構成になるように、vendors/plugin にぶち込めばOKです。

  • project
    • app
    • config
    • ….
    • vendor
      • plugins
        • acts_as_paranoid
          • lib
          • test
          • init.rb

置いた後は、サーバを再起動しましょう。

すごく便利なのは良いんですが、
・標準で入れて欲しい
・名前が微妙
・サーバ再起動めんどくさい
といった点が気になります・・・

RailsのActiveRecordで日付処理

このエントリーをはてなブックマーク Share
2010.05.26    Ruby, Ruby on Rails, 馬場   タグ: —    baba   

RailsでDBにdatetime型のカラムがある場合、特に意識しなければ、DBにはUTC時刻が保存されます。

ActiveRecordでデータを取得すると、ActiveSupport::TimeWithZone 型のオブジェクトが取得できます。
http://www.51773.com/tools/api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html

to_sやinspectをすればそれっぽい文字列になってくれますが、日本人なら、ちゃんとフォーマットして欲しいですよね。

to_sにフォーマットを渡せるようになっているので、

obj.date.to_s(:db)

と指定すると、2010-05-01 10:00:00 のような形式で取得できます。
ただ、これだとUTC時刻のままになるので、

obj.date.localtime.to_s(:db)

のようにやると良さそうです。

また、日本語の形式などに変換したいときは、

Time::DATE_FORMATS[:jp] = "%Y年%m月%d日 %H時i分s秒"

のように指定すれば良いみたいです。

http://japan.zdnet.com/blog/yoshimi/2008/04/22/entry_27016455/

RailsのHash.from_xmlに注意

このエントリーをはてなブックマーク Share
2010.05.25    Ruby, Ruby on Rails, 馬場   タグ: —    baba   

RailsのActiveSupportは大変便利で、生Ruby使うときも

irb -r rubygems -r active_support

をデフォルトにしたくなります。

# 個人的には .blank? が一番便利だと思います。

ところで、Hash.from_xmlを使うとお手軽にXMLをパースできますが、若干癖があるので注意が必要です。
・子要素も属性も同じように扱われる
・同じ名前の要素が複数あると自動で配列になる
・typeという名前の属性は、無視されることがある
・ハイフンはアンダースコアに置換される

たとえば、user-listの中にuserが複数ある場合、

{"user_list" => {"user" => ["yamada", "tanaka"]}}

のように変換されるため、扱いやすいのですが、userがたまたま1件だと、

{"user_list" => {"user" => "yamada"}}

のようになり、userが配列と期待しているプログラムは動かなくなります。
特に、検索系のXMLを使う際は注意が必要です。

また、属性は子要素のように使えますが、属性が1つだけだったり、子要素が文字列の場合、扱いが変わったり消えてしまうことがあるので、注意しましょう。

以下に例を挙げます。

<a><b size="123">456</b></a>
=> {"a" => {"b" => "456"}}

<a><b size="123" /></a>
=> {"a" => {"b" => "123"}}

<a><b type="123" /></a>
=> {"a" => {"b" => nil}}

<a><b size="123"><c>456</c></b></a>
=> {"a" => {"b" => {"size" => "123", "c" => "456"}}}

<a><b type="123"><c>456</c></b></a>
=> {"a" => {"b" => {"c" => "456", "type" => "123"}}}

複雑なXMLをパースする際は、ちゃんとnokogiriなどのライブラリを使うと良さそうですね。

古い投稿 »

COPYRIGHT [C] 2009 BEYOND PERSPECTIVE SOLUTIONS LTD. ALL RIGHTS RESERVED.