pumaのコードリーディングメモ
前提と準備
この時点のソースを読んでいく
実際に読んでいく前に、cloneしたpumaを動かせる状態にする
まずbundle install
する
そしてbin/puma
を参考に以下の内容のスクリプト(app.rb
)を作る
require './lib/puma/cli'
cli = Puma::CLI.new ARGV
cli.run
次に以下の内容のconfig.ru
を作る
run do |env|
[200, {}, ["Hello World"]]
end
ruby app.rb
を実行してpumaを起動
引数無しでpumaを起動した場合、自動的にconfig.ru
が読み込まれる
curl localhost:9292
を実行してレスポンスが返ってきたら成功
目標の設定
今回の目標は以下の2つ
- pumaが行っている「リクエストを受け取ってからレスポンスを返すまでの一連の処理の流れ」を掴む
- pumaのリポジトリのissueの中から調査できそうな物を探して調査する
Entry Point
pumaはbin/puma
がentry pointになる
このスクリプト内でPuma::Cli
クラスのrun
メソッドを呼び出している
早速lib/puma/cli.rb
の中身を覗く
Handles invoke a Puma::Server in a command line style
というコメントがある
どうやらPuma::Cli
のインスタンスはコマンドラインとPuma::Server
との橋渡しを行うようだ
Puma::Cli#initialize
の処理を見る
渡ってきた引数をパースしたりインスタンス変数を初期化したりして、最終的にはPuma::Launcher
のインスタンスをインスタンス変数に保存していた
重要そうなインスタンスだが後で使われる時になって調べる事にした
Puma::Cli#run
の処理を見る
先程のPuma::Launcher
のインスタンスのrun
メソッドが呼ばれていた
Puma::Launcher
lib/puma/launcher.rb
を開いてPuma::Launcher
が何者か確かめる事にした
クラス定義の上に以下のコメントがあった
# Puma::Launcher is the single entry point for starting a Puma server based on user
# configuration. It is responsible for taking user supplied arguments and resolving them
# with configuration in `config/puma.rb` or `config/puma/<env>.rb`.
#
# It is responsible for either launching a cluster of Puma workers or a single
# puma server.
どうやらPuma::Launcher
はpumaサーバの唯一のentry pointらしい
そしてPuma::Launcher
がpuma workersのクラスターもしくは単一のpumaサーバを立ち上げる責務を負うとの事
つまりはLauncher
はpumaの実体を立ち上げる抽象レイヤーで、Cli
から渡ってきた設定情報を元にpuma workersもしくは単一のpumaサーバを立ち上げる
Puma::Launcher
の正体が分かった所でPuma::Launcher#run
が何をしているのか見ることにした
以下の事を行っていた
- シグナルのハンドラを登録
- プロセス名を設定
- systemdとの連携
一番大事なのはこれだろう
# This blocks until the server is stopped
@runner.run
Puma::Launcher#initialize
の中で@runner
は以下のように初期化されていた
if clustered?
@options[:logger] = @log_writer
@runner = Cluster.new(self)
else
@runner = Single.new(self)
end
Cluster
のインスタンスがworkersを取りまとめるインスタンスでSingle
のインスタンスが単一のpumaサーバを担当するのだろう
なので次はPuma::Single
クラス及びPuma::Single#run
を見る
Puma::Single
Puma::Single#run
に以下のコードを発見
@server = server = start_server
server_thread = server.run
Puma::Server
コメントによるとPuma::Single
またはPuma::Cluster
から1個以上のPuma::Server
が作られるようだ
start_server
はPuma::Server
のインスタンスを作る
Puma::Server#run
は以下の処理を行う
-
Puma::ThreadPool
インスタンスの初期化 -
Puma::Reactor
インスタンスの初期化 -
Puma::Server#handle_servers
を呼び出す
Puma::ThreadPool
とPuma::Reactor
が何者なのかの調査はアーキテクチャのドキュメントも参照しながら行った
Puma::Reactor
はpumaのworkerが持つ単一のスレッド(を管理するクラス)
これのインスタンスの役割は、socketに到着したリクエストを拾い上げバッファリング(リクエストが全て届くまで待つ)し、todo
リストに入れる。リクエストはtodo
に入った時点では何も処理されない
Puma::ThreadPool
はpumaの各workerが持つスレッドプール(Puma::Reactor
とは別のスレッド)
todo
の中のリクエストを取り出し、rackアプリケーションを実行する
そしてPuma::Server
のインスタンスはSingle
またはCluster
から1個以上作られる事を考えると、Puma::Server
は名前に反してpumaのworkerに対応するクラスのようだ
Puma::Server#handle_servers
Reactor
とThreadPool
の役割が分かったのでhandle_servers
の中身を見る
一番大事そうな所はここ
while @status == :run || (drain && shutting_down?)
begin
ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : nil)
...
end
end
@status
が:run
の間、IO#select
を行い続けている
IO#select
は引数に与えられたIOオブジェクトの中から準備ができた物を返す
つまり今回の場合は第一引数に渡されたsockets
の中から準備が出来たオブジェクトを返している
ちなみにIO#select
の第一引数は入力待ちIOオブジェクトを期待している
sockets
には@binder.ios
が代入されている
そして@binder
はPuma::Binder
のインスタンス
Puma::Binder
lib/puma/binder.rb
の中のPuma::Binder
のクラス定義周辺には何のコメントも書かれていないが、おそらくソケットをbindするオブジェクトだと思う
ソケット通信についておさらいしておく
ソケットはプロセスが他のプログラムとの通信を行う時のインターフェースである
他のプログラムと通信する際にはそのソケットを対象に送信を行い、他のプログラムからの送信はソケットが受けとる
ソケット通信に必要な物はお互いを一意に特定する為の情報(名前)である
名前はソケットのドメインによって異なるが、この名前を設定する事をbind
と呼ぶ
IO#select
に渡すIOオブジェクトとして@binder.ios
が使われているので、@binder.ios
に何のIOオブジェクトがどのタイミングで追加されているのか調べる
lib/puma/binder.rb
の中で@binder
を検索すると以下のメソッドの中で何らかのIOオブジェクトが追加されていた
add_tcp_listener
inherit_tcp_listener
add_ssl_listener
inherit_tcp_listener
add_unix_listener
inherit_unix_listener
ssl listenerが何かは分からないが、tcp listenerとunix listenerはそれぞれINETドメインソケットとUNIXドメインソケットだと思う
一番簡単そうなadd_unix_listener
を見ると以下の処理があった
# Tell the server to listen on +path+ as a UNIX domain socket.
#
def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
...
s = UNIXServer.new path.sub(/\A@/, "\0") # check for abstract UNIXSocket
s.listen backlog
@ios << s
...
end
UNIXServerのインスタンスを作ってlisten
している
UNIXストリーム型接続のサーバ側のソケットのクラスであるUNIXServer
のインスタンスを作り、listen
し、そして@ios
に追加している
つまりソケットを作ってbind、そしてlistenまで行っている
ではこれはどのタイミングで呼ばれているのだろうか?
RubyMineで辿っていくと最終的にPuma::Single#run
の中でadd_unix_listener
が呼ばれる事がわかった
ここまでのまとめ
以下のような流れ
-
Puma::Cli#run
がPuma::Launcher#run
を呼び出す -
Puma::Launcher#run
がPuma::Single#run
を呼び出す - 2の時点でUNIXソケットやINETソケットが作られlistenする
-
Puma::Single#run
がPuma::Server#run
を呼び出す -
Puma::Server#run
の中でPuma::Reactor
とPuma::ThreadPool
を初期化し、Puma::Server#handle_servers
を呼び出す -
Puma::Server#handle_servers
の中で3で作ったソケットにリクエストが来るのを待つ
Puma::Binder#handle_serversの続き
IO#select
からIOオブジェクト(つまりリクエストが来たソケット)が返ってくると、以下の処理が行われる
- スレッドプール内のbusyなスレッドが最大スレッド数を下回るまで待つ(
Puma::ThreadPool#wait_until_not_full
) - MRIの場合、更にほんの少しだけ待つ(
Puma::ThreadPool#wait_for_less_busy_worker
)
1に関しては直感的に理解できるが、2に関してはよく分からない
どうやら2の処理を挟む事でlatencyが下がるらしい(https://github.com/puma/puma/pull/2079)
そして次の処理が行われる
- ソケットのaccept(accept_nonblockメソッド)
-
Puma::Client
のインスタンス化と、それを引数にPuma::ThreadPool#<<
の呼び出し
2が重要そうなのでPuma::Client
とPuma::ThreadPool#<<
をそれぞれ見ていく
Puma::Client
lib/puma/client.rb
のPuma::Client
の定義の上に以下のコメントがある
# An instance of this class represents a unique request from a client.
# For example, this could be a web request from a browser or from CURL.
#
# An instance of `Puma::Client` can be used as if it were an IO object
# by the reactor. The reactor is expected to call `#to_io`
# on any non-IO objects it polls. For example, nio4r internally calls
# `IO::try_convert` (which may call `#to_io`) when a new socket is
# registered.
一段落目は「このクラスのインスタンスはクライアントからの一意なリクエストを表現する。例えばブラウザやCURLからのリクエストとか」
二段落目は「Puma::Client
はリアクターからIOオブジェクトの様に扱われる。リアクターはポーリングしている非IOオブジェクトに#to_io
を呼び出す事を期待されている。例えばnir4r
は新しいソケットが登録された時に内部的にIO::try_convert
を呼ぶ」とある
正直二段落目は意味不明なので一旦放置