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のログファイルをMessagePackで超高速解析!

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

こんにちは、伊藤です。

最近Railsがガンガン出力するproduction.logを解析してどのアクションがリクエスト多いのか、DBがボトルネックになってるアクションはないか、などを調べているんですが、production.logがどんどん肥大化して、解析ツールの開発イテレーションを回す際の効率が悪くなってきました。

また似たようなツールをやたらめったらコピペで作ってきたため、開発効率も悪くなってきたので、ここでAPIを整理しつつ、あわよくばログをなめる部分を高速化できないものかと考えました。

シンプルに正規表現をつかって変化する箇所を切り出していたのですが、これではかなり重いのは自明ですし、がんばって文字列をパースするパーサを作ってもrubyではあまりスピードは出なそう、でもrubyで書きたいし….

ということで、一度パースしたデータを読込みに効率よさそうな形式に変換することで、次以降の解析を高速化しようと考えました。RubyのMarshalモジュールのdump/loadを使うのもよさそうでしたが、個人的にMessagePackというライブラリが気になっていたので、試してみました。特にウリ文句もread時のデシリアライズ性能がよいとのことでしたので、結果的にはマッチする用途だったのかなと思います。

んで今回つくったrails_log.rbというライブラリなのですが、以下のようにして使うことを想定してます。

require "rails_log"

# 普通のログファイルを生で解析
rlog = RailsLog.new("/hogege/log/production.log")
rlog.each do |row|
 # rowを使っていろいろ数えたりする
end

# binファイルに変換
rlog = RailsLog.new("...")
rlog.convert_to_bin

# binファイルを変換(はやい)
rlog = RailsLog.new("...")
rlog.bin_each do |row|
 # rowを使っていろいろ数えたりする
end

2つのイテレータeachとbin_eachが解析用のメソッドで、ブロック引数(この例ではともにrowとしている)はHashオブジェクトで、以下のようなキーと値を持ってます。

  • :controller_action – HogeController#action_nameのような文字列
  • :format – “html”とか”json”(レスポンスの出力フォーマット)
  • :ip – リクエストしてきたクライアントのIPアドレス(文字列)
  • :datetime – リクエストされたアクセス時刻(Timeオブジェクト)
  • :http_method – GETとかPOSTとか
  • :params – paramsの中身(Hash)
  • :time_total – アクション処理にかかった総時間(単位msec, FixNum)
  • :time_view – View処理にかかった時間
  • :time_db – DB処理にかかった時間
  • :http_response – “200 OK”とかそういう文字列
  • :request_uri -
    “http://mogera.bpsinc.jp/fuga/hoge/?p1=value1&p2=valuevalue22″のような文字列

出力フォーマットがjsonなリクエストと、そうでないリクエストの数を数えるには以下のようにします。

rlog = RailsLog.new("production.log")
c_json, c_other = 0, 0
rlog.each do |row|
 if row[:format] == "json" then
   c_json += 1
 else
   c_other += 1
 end
end
# 数字をつかってなにかする

ただこれでは遅いというのが、当初の開発動機ですので、MessagePackを応用した形式に変換してから解析してみましょう。

# まず変換(これには多少時間がかかる)
raw_log = RailsLog.new("production.log")
raw_log.convert_to_bin("production.log.bin")

# 解析
bin_log = RailsLog.new("production.log.bin")
c_json, c_other = 0, 0
bin_log.bin_each do |row|
  if row[:format] == "json" then
    c_json += 1
  else
    c_other += 1
  end
end

# 以降はbin_eachですばやく解析できる

注意

キーのリストにある以外の値は全て葬りさられますので、元のファイルはバイナリファイルを作成しても手元に残しておくことをおすすめします。(ロガーオブジェクトで出力したデバッグメッセージなど)

パフォーマンス

実験環境: Core2Duo E8500, Mem2GB, VM(KVM), Ubuntu Linux 32bit, ruby1.8.7 enterprise
ログファイル: 約1.4GB, 44万5000リクエストのログ

  • binファイルに変換: だいたい150秒ぐらいかかりました。変換後のバイナリデータの容量は約185MBになりました。
  • eachによる生ログの解析(変換前): eachを全部回すのにだいたい40秒程度
  • bin_eachによる変換されたログの解析: bin_eachを全部回すのにだいたい5秒程度 => 8倍程度の高速化
  • Java版によるログの解析: (環境が違うので参考まで: MacOSX10.6 CPU C2D 2.66GHz, Mem4GB)約2.3秒 => 15倍以上の高速化

常用していきたいruby版でも8倍ほど早くなりました。

ダウンロード

  • rails_log.rb – ログ解析フレームワークとMessagePackを用いたバイナリ形式への変換機能の提供
  • rails_log-java.zip – ログ解析フレームワーク(バイナリのみ)の提供

まとまらないまとめ

ログ解析なんてどうせ数日に1回とか、1日1回なので遅くてもいいや、という話もありますが、解析ツールを開発するときに実際のデータで素早く解析できる、eachとbin_eachを入れ替えるだけでアルゴリズムを触らず生のログファイル/変換済みのバイナリ形式ファイルと対応を切り替えることができるので割と気に入ってます。
またJava版も勢いで作ってみたものの、やはりかなり早かったので自己満足してしまいました。
MessagePackプロジェクトの皆さんにも感謝です!

参考リンク

プログラムはご自由にお使い頂いて結構ですので、よければ皆さんもお試しください。
# バグってたらごめんなさい…

次世代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での設定例を追加してみました。

RailsでTokyoTyrantを使ってみたらフリーズした

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

※6/25にサンプルコードを修正しました
※7/1にサンプルコードを修正しました&補足を入れました

伊藤です。

最近社内のRailsを使ったプロジェクトで、外部サービスからのレスポンスを使う処理で、
いちいち外部サービスにリクエストを行って取得していてはとても時間がかかるので、
シンプルなキャッシュシステムを構築することになりました。

最初は馬場が記事にしてくれているようにファイルベースのものを用いていましたが(実際はもうちょっと複雑なものを使っていました)、
ファイル数(エントリ)が数万単位で増えた際、パフォーマンスが指数関数的に悪くなってしまったので、キーと値の格納にTokyoTyrantを利用することにしました。
TokyoTyrantの使い方はシステムエンジニアブログにも書いてみました(TokyoTyrantをRubyで使ってみた)ので、TokyoTyrantそのものについてはこちらも見てみてください。

Railsから、利用するためにlib/util/data_cache.rbといったかんじで、以下のようなモジュールを定義してみました。
任意のオブジェクトをデータ構造を保ったまま保存できるようにMarshalクラスによりシリアライズ/デシリアライズを行っています。
コントローラからはincludeして使うイメージです。

require "tokyotyrant"
module DataCache
  def cache_read(key)
    init_tt if !defined?(@@tokyotyrant)
    v = @@tokyotyrant.get(key)
    return v ? Marshal.load(v) : nil
  end

  def cache_write(key, value)
    init_tt if !defined?(@@tokyotyrant)
    @@tokyotyrant.put key, Marshal.dump(value)
  end

  private

  def init_tt
    @@tokyotyrant = TokyoTyrant::RDB.new
    @@tokyotyrant.open("localhost", 1978)
  end
end

これをいろんなコントローラから使っていると、自分のデスクトップから開発している分には正常に動作するのですが、
多人数が同時でアクセスしたりする環境で使うとフリーズします。

どうやらgetやputを行っているときに、並行してこれらのTokyoTyrantとの通信が発生するメソッドを呼び出すと
通信の内容がごちゃごちゃになり、TTから正常にレスポンスを取得することができなくなっていたものと思われました。
マルチスレッド動作のサポートが各方面から望まれていたRailsですが、意外にもこのようなところでも弊害が発生しました。

対処としては、以下のようにRuby標準のthreadライブラリにあるMutexを使って、
TokyoTyrantへのアクセスを行う部分をクリティカルセクションとすることで、解決しました。

require "thread"
require "tokyotyrant"
module DataCache
  MTX = Mutex.new
  def cache_read(key)
    init_tt if !defined?(@@tokyotyrant)
    v = nil
    MTX.synchronize do
      v = @@tokyotyrant.get(key)
    end
    return v ? Marshal.load(v) : nil
  end

  def cache_write(key, value)
    init_tt if !defined?(@@tokyotyrant)
    MTX.synchronize do
      @@tokyotyrant.put key, Marshal.dump(value)
    end
  end

  private

  def init_tt
    MTX.synchronize do
      return if defined?(@@tokyotyrant)
      @@tokyotyrant = TokyoTyrant::RDB.new
      @@tokyotyrant.open("localhost", 1978)
    end
  end
end

このクリティカルセクションに限らずRailsではまだ1プロセスで大量のリクエストをさばけるほどマルチスレッド処理のスループットが
出ませんので、本番環境の実運用では複数プロセスを上げて動かすというのが一般的、というのがまだしばらく続きそうですね。

問題意識を煽るようなタイトルをつけてしまいましたが、コントローラ部分の実装がスレッドセーフかどうかを保証するのはユーザの責任であるというRailsの仕様と、get/putなどの呼び出しはスレッドセーフではないというTokyoTyrantクライアントライブラリの仕様の問題ですね。

TokyoTyrant自体はクリティカルセクションで保護してもサクサク動作しているので、今後もキャッシュ機構が必要になったら積極的に使っていこうと思います。

7/1追記:
今回実装例で挙げているモジュールでは、1つのアプリケーションプロセスで最大で1つのTokyoTyrantコネクションしか使わないため、大量にTokyoTyrantにアクセスを行う場合TokyoTyrantにいちいちつなぎ直すか、複数のコネクションをプーリングするなどの戦略が必要です。調べたところTokyoTyrantのセッション確率のコストはそんなに高くないのでgetやputを少ない回数発行するような利用の場合いちいち接続する設計にするとシンプルで、バグが少なくてよいと思います。また、アプリケーションサーバとTokyoTyrantサーバを一つのマシンで動かしていると、TTへのリクエストの並列度が増えてくるとだんだんレスポンスが悪くなるようです。(アプリサーバとTTサーバに分けて実行すると数倍の速度が出る)

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とか。

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などのライブラリを使うと良さそうですね。

Rails 2.3では相対パスでの運用にバグあり?

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

ごぶさたです、伊藤です。

最近では社内のプロジェクトにもポツポツとRailsを採用するプロジェクトが増えてきてRubyistとしては嬉しい限りです。
しかし、最近Rails2.3のアプリケーションをLinuxサーバ上にApache2.2+Mongrel(Cluster)という構成で配置しようとしたところ、
思わぬトラブルに巻き込まれましたので報告です。

今回配置しようとしたrailsアプリケーションは、いろんなコンポーネントが連携するシステムの一部で、ドメイン直下ではなく
http://hogeramogera/honyarara というような感じでURLのドメイン直下ではなく、相対パスが与えられた上での運用ということになっていました。
Mongrel Clusterでは、このような場合mongrel_cluster.xmlにprefix: /honyararaと書いたり、コマンドラインによる起動であれば–prefx /honyararaとします。
しかし、rails2.3になってからこれがうまく動かなくなってしまったようで、mongrelが出力するログには以下のようなメッセージが残っていました。

** Mounting Rails at /honyarara…
/var/lib/gems/1.8/gems/activesupport-2.3.5/lib/active_support/dependencies.rb:440:in `load_missing_constant’: uninitialized constant ActionController::AbstractRequest (NameError)
from /var/lib/gems/1.8/gems/activesupport-2.3.5/lib/active_support/dependencies.rb:80:in `const_missing’
from /var/lib/gems/1.8/gems/mongrel-1.1.5/lib/mongrel/rails.rb:151:in `rails’
from /usr/bin/mongrel_rails:115:in `cloaker_’
from /var/lib/gems/1.8/gems/mongrel-1.1.5/lib/mongrel/configurator.rb:149:in `call’
from /var/lib/gems/1.8/gems/mongrel-1.1.5/lib/mongrel/configurator.rb:149:in `listener’
from /usr/bin/mongrel_rails:101:in `cloaker_’
from /var/lib/gems/1.8/gems/mongrel-1.1.5/lib/mongrel/configurator.rb:50:in `call’
from /var/lib/gems/1.8/gems/mongrel-1.1.5/lib/mongrel/configurator.rb:50:in `initialize’
from /usr/bin/mongrel_rails:86:in `new’
from /usr/bin/mongrel_rails:86:in `run’
from /var/lib/gems/1.8/gems/mongrel-1.1.5/lib/mongrel/command.rb:212:in `run’
from /usr/bin/mongrel_rails:283

普通に例外が起きています!
設定がおかしいとかではなく、これはもうバグのレベルですね。
Railsプロジェクトはリリースエンジニアリングが割としっかりしているように思っていたのですが、そうでもないのでしょうか^^;
missingになっている定数(Rubyではクラスも宣言すると定数扱い)ActionController::AbstractRequestは2.3からなくなったクラスのようでした…

なにはともあれ、問題を解決しないことは仕事が進まないので、いろいろ対処法を探していたのですが、最初に目についた
Rails2.3.2でmongrel_clusterにprefixを付けると起動しない問題の対策を試してみましたが、こちらのページでも述べられているようにpublic以下のコンテンツに対するURL生成が相対パスを考慮しておらず、静的コンテンツのコピーやシンボリックリンクによる対応はあまりにもダサいので、他にいい方法はないかもう少し探すことにしました。

そこで、サブディレクトリ下で Rails 2.3 を動かすときの注意点を参考に、試してみたところ、うまくいきました!
blogによるプログラミング技術やノウハウの共有は本当に素晴らしいですね。
BPSも本ブログを通じて皆様の役立つ技術やノウハウを共有できるように頑張ります!

古い投稿 »

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