LoginSignup
8
9

More than 5 years have passed since last update.

RubyGemのソースを読む(Unicorn編)

Posted at

前提・下準備

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のソースからパクって来たんですけど忘れました。

Screen Shot 2016-01-23 at 20.50.40.png

  • ログは、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リポジトリから通らない

kgioとかの処理

  • Unicornのネットワーク周りの具体的な処理は kgioがやってるみたいです(多分)
  • UnicornのRubyで書かれている部分は結構大まかなロジックを司っている(と思っています。間違ってたら本当にごめんなさい)
  • しかしながら大学時代に C言語系の勉強サボって 大学にあったXbox で Call of Duty4 Modern Warfareのキャンペーンのベテランをやりこんで、しかも全クリ出来なかったショボいクソだった いたので、あまり同時接続数がどうたらとかは分かりません。
  • 悔しいので後々ちゃんと勉強してソース読んで printf しまくって挙動を追いまくってやりたいな〜とは思っています

Reference

forkとかそういう系

結構Unicornのソースや挙動を追う上で参考になったのが

ソース読んだほうがいい動機とか

謝辞

  • 結構「これわかんないです」みたいなことを相談したらアドバイスくれた先輩達が複数人いました(何か名前出さんでくれって言われたんであえて名前出さないですが)。勝手にここで謝辞します。ありがとうございます。
8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9