表題の通り、WEBrickはシングルスレッドなアプリサーバではなくマルチスレッドです。
にも関わらず、Web上にはWEBrickがシングルスレッドであるかのような記事が多く、Wikipediaでも
Unlike most of the servers that are used in production,
WEBrick is not scalable since it is a single threaded web server by default.
と記載があり、とても誤解をまねきやすいです(デフォルトで、と言っているので完全に間違っているとは言い難いけど)
今回は、WEBrickがマルチスレッドサーバである根拠と、なぜシングルスレッドと誤解されているのかをRailsのバージョンの話も交えて書いていきます。
WEBrickがマルチスレッドである根拠
どのバージョンを見ても、acceptしたsocketに対してThread.newで別スレッドを作ってそのスレッド内でHTTPハンドリングをしてます。かなり省略していますが、こんな感じなコードになっています。# webrick/lib/webrick/server.rb
module WEBrick
class GenericServer
def start(&block)
# ...
server_type.start{
# ...
begin
while @status == :Running
begin
sp = shutdown_pipe[0]
if svrs = IO.select([sp, *@listeners], nil, nil, 2.0)
# ...
svrs[0].each{|svr|
@tokens.pop # blocks while no token is there.
if sock = accept_client(svr)
unless config[:DoNotReverseLookup].nil?
sock.do_not_reverse_lookup = !!config[:DoNotReverseLookup]
end
th = start_thread(sock, &block)
th[:WEBrickThread] = true
thgroup.add(th)
# ...
def start_thread(sock, &block)
Thread.start{
begin
Thread.current[:WEBrickSocket] = sock
# ...
block ? block.call(sock) : run(sock)
# ...
試しにsleepするだけのRackアプリをを用意して、curlを連続で叩くと、sleepで指定した秒数の後に同時にレスポンスが返ってきます。
#!/usr/bin/env ruby
# sleep-rack.rb
require 'rack'
app = Proc.new do |env|
sleep 5
['200', {'Content-Type' => 'text/html'}, ['hoge.']]
end
Rack::Handler::WEBrick.run app
$ bundle exec ruby ./sleep-rack.rb
$ for i in {1..3}; do curl "localhost:8080" &; done
なぜシングルスレッドであると誤解されているのか
Rails4ではrails serverしたときのデフォルトのアプリサーバとしてWEBrickが使われます。Rails4の時点ではautoloadがスレッドセーフではないため、同時アクセスがある場合にconst_missing内の定数ロードによってコンフリクトが発生する可能性があります。Circular dependency detected while autoloading constant Hoge
そのため、Railsではこのようなスレッドセーフではないautoloadに起因するエラーを防ぐため、WEBrickにおいてはRack::Lockのミドルウェアによって同時実行制御を行っており、結果としてシングルスレッドであるような振る舞いをrails serverに付与しています。
# lib/rails/commands/server.rb
module Rails
class Server < ::Rack::Server
# snip...
def middleware
middlewares = []
if RUBY_VERSION < '2.0.0'
middlewares << [Rails::Rack::Debugger] if options[:debugger]
end
middlewares << [::Rack::ContentLength]
# FIXME: add Rack::Lock in the case people are using webrick.
# This is to remain backwards compatible for those who are
# running webrick in production. We should consider removing this
# in development.
if server.name == 'Rack::Handler::WEBrick'
middlewares << [::Rack::Lock]
end
Hash.new(middlewares)
end
コメントにも記載があるようにproductionでWEBrickを使っているユーザに対して、後方互換のためにRack::Lockを入れた、とのことです。後方互換と言っているのはおそらくRails3のallow_concurrencyのデフォルトがfalseなので、そのままproductionで利用するとシングルスレッドで動くわけですが、Rails4のallow_concurrencyのデフォルトはtrueなので、そのままproductionで動かすとマルチスレッドで動いてしまいます。Rails3でもallow_concurrencyをtrueにすればWEBrickでもマルチスレッドで動いたりautoloadのバッティングが発生することは確認済みです。
まとめると、Rails3のデフォルトではallow_concurrencyはデフォルトfalseであったことや、Rails4は後方互換のためにWEBrickをシングルスレッドで動かしていて、そのためWEBrick自体はマルチスレッドなアプリサーバであるにも関わらず、シングルスレッドであるかのような記事が見受けられる、ということになります。
ちなみにコードや挙動の確認以外にも以下のような記事があり、マルチスレッドであることを確認しています。
- Is WEBrick Webscale?
- planetruby/awesome-webservers: A collection of awesome Ruby web servers (single-threaded, multi-threaded, multiplexed, etc.)
- Richard Schneemanさんのツイート: “What happened when I compared the “slowest” server in Ruby to NGINX? The results surprised even me https://t.co/kGjLc8ZbBV”
Rails5ではどうなっているか
Rails5では特に設定をしなくてもマルチスレッドで動きます。コードを読むとmiddlewareがごっそりなくなっています。def middleware
Hash.new([])
end
また、Rackのミドルウェアもallow_concurrencyが明示的にfalseが指定されていなければコンカレントに動きます。Rails4まではデフォ値のnilのときはRack::Lockを入れるような処理になっていました。
module Rails
class Application
class DefaultMiddlewareStack
# snip...
def build_stack
if config.allow_concurrency == false
# User has explicitly opted out of concurrent request
# handling: presumably their code is not threadsafe
middleware.use ::Rack::Lock
end
さらにautoload自体がDependencies.load_interlockによって、スレッドセーフになっているっぽいです。そのためCircular dependency…のエラーが起きづらくなっているようです(というか起こす術がわからん…)
module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
def require_or_load(file_name, const_path = nil)
# snip...
Dependencies.load_interlock do
まとめ
- WEBrickはacceptの度にThread.newを作って別スレッドにHTTP処理を委譲するマルチスレッドサーバである
- Rails4ではWEBrickがデフォルトのアプリサーバとなっており、rails serverしたときにシングルスレッドで動作するような設定になっている
- Rails4ではautoloadがスレッドセーフではなかったためシングルスレッドで動作するような仕組みにしていたと思われる。
- Rails3のデフォルト値の関係で、後方互換のためにシングルスレッドで動作するようにしたっぽい
- Rails5ではWEBrickでrails serverしたときも、マルチスレッドで動作するようになっている。
- Rails5ではautoloadがスレッドセーフになったとともに、productionではauto_loadではなくeager_loadを推奨する作りになっているのでマルチスレッドでも問題ないような実装になった
参考URL
- Rails5でenable_dependency_loading = trueするのはどうなのか - にこにこインターネット
- remove Rack::Lock for webrick · rails/rails@cc60b5e
- Concurrent load interlock (rm Rack::Lock) by matthewd · Pull Request #17102 · rails/rails
- Rails 4.2.2 still includes Rack::Lock (even in production mode) · Issue #20660 · rails/rails
- Rack を読む / WEBrick が起動するまで - Qiita