前提・下準備
- RubyGemのソースを読む(設定編) をちょろっと読んで、設定しとくと良いです。
Unicornの特徴
- Masterは何もしないことは無いが、Workerが何か死んだら何かしてくれる(これあんまり分かってないです)
- Workerが基本的にリクエストを処理する
- スレッドとかイベントドリブンとか難しいから、なぁ...シンプルにしようや...ってのが哲学っぽいです。
もっと詳しく哲学とか知りたい人向け
以下とか参考になるのでは無いかと思います
実際に読んで見る
起動まで
- とりあえず
git clone
します
$ git clone git@github.com:r-fujiwara/django.git
- とりあえず
bundle install
して起動します。
$ bundle install --path vendor/bundle
$ bundle exec unicorn -c config/unicorn.rb
- うまくいくとこんな画面が出ます
なんか適当なsinatraのソースからパクって来たんですけど忘れました。
- ログは、
log/unicorn.stderr.log
に出ますので、以下みたいな感じに書きました。
$ tail -f log/unicorn.stderr.log
I, [2016-01-23T20:50:24.622132 #16518] INFO -- : unlinking existing socket=/tmp/unicorn.sock
I, [2016-01-23T20:50:24.622225 #16518] INFO -- : listening on addr=/tmp/unicorn.sock fd=10
I, [2016-01-23T20:50:24.622261 #16518] INFO -- : worker=0 spawning...
I, [2016-01-23T20:50:24.623837 #16518] INFO -- : master process ready
I, [2016-01-23T20:50:24.623922 #16545] INFO -- : worker=0 spawned pid=16545
I, [2016-01-23T20:50:24.624088 #16545] INFO -- : Refreshing Gem list
I, [2016-01-23T20:50:25.057550 #16545] INFO -- : worker=0 ready
I, [2016-01-23T20:50:26.810465 #16518] INFO -- : SIGWINCH ignored because we're not daemonized
192.168.33.1 - - [23/Jan/2016:20:50:38 +0900] "GET / HTTP/1.1" 200 1809 0.0244
192.168.33.1 - - [23/Jan/2016:20:50:39 +0900] "GET /favicon.ico HTTP/1.1" 404 512 0.0012
ソース読む手順
- 起動
- ログを読む
- 気になる所に
puts
を挟む - 再起動
- ログを読む
地道にやれば何となく色々わかってくると思います
エントリポイント
- 結構細かいオプションはありますが、今回の場合のソースはbin/unicornの126行目です。
Unicorn::HttpServer.new(app, options).start.join
- 何となくこの時点で
Unicorn::HttpServer.newでインスタンス生成
インスタンスメソッドのstartを読んでいる
startにチェーンしてjoinを読んでる
だからstartの返り値はインスタンス返しているので、地道にstartとjoinの挙動を追う
- とか勘がはたらくと良いかもです。
- 最初は地道に読んでましたけど、ある程度アタリをつけながらソースを読んでると、ソースを読むスピードは上がる気がします(気がするだけ)
時は来た!それだけだ... これからが本番です
-
Unicorn::HttpServer.new
ですが、ソースはvendor/bundle/ruby/2.2.0/gems/unicorn-4.9.0/
配下のvendor/bundle/ruby/2.2.0/gems/unicorn-4.9.0/lib/unicorn/http_server.rb
にあります。 - なのでここを読んでいきましょう。
ピックアップすべき点
http_server.rb#start
- 色々大事なんですが、ここで大事なのは
spawn_missing_workers
です。
http_server.rb#start
def start
inherit_listeners!
# this pipe is used to wake us up from select(2) in #join when signals
# are trapped. See trap_deferred.
@self_pipe.replace(Unicorn.pipe)
@master_pid = $$
# setup signal handlers before writing pid file in case people get
# trigger happy and send signals as soon as the pid file exists.
# Note that signals don't actually get handled until the #join method
@queue_sigs.each { |sig| trap(sig) { @sig_queue << sig; awaken_master } }
trap(:CHLD) { awaken_master }
# write pid early for Mongrel compatibility if we're not inheriting sockets
# This is needed for compatibility some Monit setups at least.
# This unfortunately has the side effect of clobbering valid PID if
# we upgrade and the upgrade breaks during preload_app==true && build_app!
self.pid = config[:pid]
build_app! if preload_app
bind_new_listeners!
# ここ
spawn_missing_workers
self
end
http_server.rb#spawn_missing_workers
- ここでやっているのは
Workerのプロセスを生成
Masterのプロセスをfork
Workerのプロセスをloopさせる(worker_loop)
spawn_missing_workes.rb
def spawn_missing_workers
worker_nr = -1
until (worker_nr += 1) == @worker_processes
@workers.value?(worker_nr) and next
worker = Unicorn::Worker.new(worker_nr)
before_fork.call(self, worker)
if pid = fork
@workers[pid] = worker
worker.atfork_parent
else
after_fork_internal
worker_loop(worker)
exit
end
end
rescue => e
@logger.error(e) rescue nil
exit!
end
- forkがよく分からない人はググるか、後述するなるほどUnixプロセス ― Rubyで学ぶUnixの基礎 を読むといいです。
- ページ数は薄いですが、分かりやすい語り口と短いサンプルコードで個人的には凄く良いと思っています。
- 他の言語でもforkやプロセスの概念は応用が利くと思います。
http_server.rb#worker_loop
- ちょっとココらへんの細かい挙動は忘れてしまいましたが、process_client が大事です。
- ここが Rackアプリケーションの入り口になります。
- 細かい挙動はガチで
puts
ゲーになると思いますが、基本的には
Workerプロセスを無限ループさせる
process_clientを呼ぶ
worker_loop.rb
# runs inside each forked worker, this sits around and waits
# for connections and doesn't die until the parent dies (or is
# given a INT, QUIT, or TERM signal)
def worker_loop(worker)
ppid = @master_pid
readers = init_worker_process(worker)
nr = 0 # this becomes negative if we need to reopen logs
# this only works immediately if the master sent us the signal
# (which is the normal case)
trap(:USR1) { nr = -65536 }
ready = readers.dup
@logger.info "worker=#{worker.nr} ready"
begin
nr < 0 and reopen_worker_logs(worker.nr)
nr = 0
worker.tick = time_now.to_i
tmp = ready.dup
while sock = tmp.shift
# Unicorn::Worker#kgio_tryaccept is not like accept(2) at all,
# but that will return false
if client = sock.kgio_tryaccept
# ここ
process_client(client)
nr += 1
worker.tick = time_now.to_i
end
break if nr < 0
end
# make the following bet: if we accepted clients this round,
# we're probably reasonably busy, so avoid calling select()
# and do a speculative non-blocking accept() on ready listeners
# before we sleep again in select().
unless nr == 0
tmp = ready.dup
redo
end
ppid == Process.ppid or return
# timeout used so we can detect parent death:
worker.tick = time_now.to_i
ret = IO.select(readers, nil, nil, @timeout) and ready = ret[0]
rescue => e
redo if nr < 0 && readers[0]
Unicorn.log_error(@logger, "listen loop error", e) if readers[0]
end while readers[0]
end
http_server.rb#process_client
- ここは Rackサーバ(Unicorn)からRackアプリケーション(RailsとかSinatraとか)に処理を渡しています。
- 具体的には 562行目でしょうか
- これは多分Rackに渡しているのはソース読むのだと分かりづらいので、また別途記事を書こうかなと考えています。
# once a client is accepted, it is processed in its entirety here
# in 3 easy steps: read request, call app, write app response
def process_client(client)
# ここ
status, headers, body = @app.call(env = @request.read(client))
begin
return if @request.hijacked?
if 100 == status.to_i
e100_response_write(client, env)
status, headers, body = @app.call(env)
return if @request.hijacked?
end
@request.headers? or headers = nil
http_response_write(client, status, headers, body,
@request.response_start_sent)
ensure
body.respond_to?(:close) and body.close
end
unless client.closed? # rack.hijack may've close this for us
client.shutdown # in case of fork() in Rack app
client.close # flush and uncork socket immediately, no keepalive
end
rescue => e
handle_error(client, e)
end
http_server.rb#join
- 詳しくは追って無いですが、ソースをざっと読む感じシグナルを受け取った時に、workerの挙動をどういう風に制御するか、ということが書かれていると思います。
- ここらへんはあまり自信がありません...
http_server.rb#join
# monitors children and receives signals forever
# (or until a termination signal is sent). This handles signals
# one-at-a-time time and we'll happily drop signals in case somebody
# is signalling us too often.
def join
respawn = true
last_check = time_now
proc_name 'master'
logger.info "master process ready" # test_exec.rb relies on this message
if @ready_pipe
begin
@ready_pipe.syswrite($$.to_s)
rescue => e
logger.warn("grandparent died too soon?: #{e.message} (#{e.class})")
end
@ready_pipe = @ready_pipe.close rescue nil
end
begin
reap_all_workers
case @sig_queue.shift
when nil
# avoid murdering workers after our master process (or the
# machine) comes out of suspend/hibernation
if (last_check + @timeout) >= (last_check = time_now)
sleep_time = murder_lazy_workers
else
sleep_time = @timeout/2.0 + 1
@logger.debug("waiting #{sleep_time}s after suspend/hibernation")
end
maintain_worker_count if respawn
master_sleep(sleep_time)
when :QUIT # graceful shutdown
break
when :TERM, :INT # immediate shutdown
stop(false)
break
when :USR1 # rotate logs
logger.info "master reopening logs..."
Unicorn::Util.reopen_logs
logger.info "master done reopening logs"
soft_kill_each_worker(:USR1)
when :USR2 # exec binary, stay alive in case something went wrong
reexec
when :WINCH
if $stdin.tty?
logger.info "SIGWINCH ignored because we're not daemonized"
else
respawn = false
logger.info "gracefully stopping all workers"
soft_kill_each_worker(:QUIT)
self.worker_processes = 0
end
when :TTIN
respawn = true
self.worker_processes += 1
when :TTOU
self.worker_processes -= 1 if self.worker_processes > 0
when :HUP
respawn = true
if config.config_file
load_config!
else # exec binary and exit if there's no config file
logger.info "config_file not present, reexecuting binary"
reexec
end
end
rescue => e
Unicorn.log_error(@logger, "master loop error", e)
end while true
stop # gracefully shutdown all workers on our way out
logger.info "master complete"
unlink_pid_safe(pid) if pid
end
超えられてない壁
ビルドがgithubのUnicornリポジトリから通らない
- bundlerでgithub上のunicornを強引に入れる を読んで試したりしたんですが、ビルドが通りませんでした。
- 俺の意識が低すぎるのが原因ですね
kgioとかの処理
- Unicornのネットワーク周りの具体的な処理は kgioがやってるみたいです(多分)
- UnicornのRubyで書かれている部分は結構大まかなロジックを司っている(と思っています。間違ってたら本当にごめんなさい)
- しかしながら大学時代に C言語系の勉強サボって
大学にあったXbox で Call of Duty4 Modern Warfareのキャンペーンのベテランをやりこんで、しかも全クリ出来なかったショボいクソだったいたので、あまり同時接続数がどうたらとかは分かりません。 - 悔しいので後々ちゃんと勉強してソース読んで
printf
しまくって挙動を追いまくってやりたいな〜とは思っています
Reference
forkとかそういう系
結構Unicornのソースや挙動を追う上で参考になったのが
- なるほどUnixプロセス ― Rubyで学ぶUnixの基礎 です。
- Resqueとかもマルチプロセスモデルなので、Unicornで学んだ知識は応用が効くのでは無いかと思います。
ソース読んだほうがいい動機とか
-
勉強が出来ない奴はプログラマになれ!(バカだからできる勉強法)
- ここで出てくるイメージモデルを作るとか、手元にソースを持ってくるとかは結局やってることあんま変わんないのかなぁと思います。
- ソース書き換えたりまでしたりはしてないし、その点で言えば俺はまだまだですが。
謝辞
- 結構「これわかんないです」みたいなことを相談したらアドバイスくれた先輩達が複数人いました(何か名前出さんでくれって言われたんであえて名前出さないですが)。勝手にここで謝辞します。ありがとうございます。