Tech Racho エンジニアの「?」を「!」に。
  • インフラ

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

前回ブログで紹介した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を使ってみた


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。