puma 有两种启动方式:一种是通过标准的 rack 接口启动,一种是通过 puma 提供的命令行工具启动。
首先,Puma 会把自己设置为缺省的 rack handler,见文件 lib/puma/rack_default.rb
module Rack::Handler
def self.default(options = {})
Rack::Handler::Puma
end
end
然后,在 Rack::Handler::Puma 的 run 方法中启动 puma,见文件 lib/rack/handler/puma.rb
def self.run(app, options = {})
......
server = ::Puma::Server.new(app)
min, max = options[:Threads].split(':', 2)
puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
server.add_tcp_listener options[:Host], options[:Port]
server.min_threads = min
server.max_threads = max
yield server if block_given?
begin
server.run.join
rescue Interrupt
puts "* Gracefully stopping, waiting for requests to finish"
server.stop(true)
puts "* Goodbye!"
end
end
run 方法中的参数 app 是一个 rack 应用,options 只支持主机名/端口号/线程数/日志等有限的几个参数。从代码中可见,rack 接口启动 puma 只监听 tcp 端口,且使用单进程模式,不支持集群。所以通过 rack 接口启动 puma 不能完全利用 puam 的高级功能。
通过下面的命令都会以 rack 接口启动 puma:
rackup -s Puma
rails s Puma
另外一种启动 puma 的方法是通过命令行的 bin/puma 命令,这是一个 ruby 写的可执行脚本,代码如下:
require 'puma/cli'
cli = Puma::CLI.new ARGV
cli.run
可见,实际处理命令行参数和启动 puma 的是 Puma::CLI 类。命令行启动的时候,可以传递一些参数给 puma。参数有两种格式:短的和长的,比如-p 和 --port 都是设置监听端口。你可以在命令行把所有的 puma 参数都设置好,也可以把设置参数写在一个文件里,然后通过-C 来引用。
$ puma -b tcp://127.0.0.1:9292 -t 8:32 -w 3
$ puma -C /path/to/config
CLI 启动 puma 的具体流程见下一节。
这里主要讲解命令行启动 puma 的流程。启动过程分两步,首先是初始化 Puma::CLI 类,然后执行 cli.run 方法。
CLI 初始化的代码如下:
def initialize(argv, events=Events.stdio)
......
@events = events
setup_options
generate_restart_data
@binder = Binder.new(@events)
@binder.import_from_env
end
主要步骤包括:初始化事件的标准输出与错误输出、设置命令行参数的缺省值、设置解析命令行参数的代码、设置重启 puma 的命令行、初始化 Binder 类。
setup_options 方法首先设置命令行参数的缺省值,比如缺省的最小和最大线程数是 0 和 16。然后初始化@parser对象,实现命令行参数的解析。解析代码也很简单,每找到一个命令行参数,就把它加入@options数组。
def setup_options
@options = {
:min_threads => 0,
:max_threads => 16,
......
}
@parser = OptionParser.new do |o|
o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
@options[:binds] << arg
end
......
end
然后是获取重启 puma 的命令行,并保存到@restart_argv中。
def generate_restart_data
@restart_dir ||= Dir.pwd
@original_argv = ARGV.dup
if File.exist?($0)
arg0 = [Gem.ruby, $0]
else
arg0 = [Gem.ruby, "-S", $0]
end
# Detect and reinject -Ilib from the command line
lib = File.expand_path "lib"
arg0[1,0] = ["-I", lib] if $:[0] == lib
@restart_argv = arg0 + ARGV
end
由于 generate_restart_data 方法比较难懂,这里演示一下阅读 puma 源代码时的调试过程。首先下载 puma 的源代码并编译。
ylt~/tmp/$ git clone https://github.com/puma/puma.git
ylt~/tmp/$ cd puma
ylt~/tmp/puma$ rake
然后在文件 lib/puma/cli.rb 的 374 行设置断点,我使用的是 byebug。接着使用下面的命令启动 puma:
ylt~/tmp/puma$ ruby -I lib/ bin/puma
[371, 380] in /Users/ylt/tmp/puma/lib/puma/cli.rb
371: else
372: @restart_argv = arg0 + ARGV
373: end
374: byebug
375: end
=> 376: end
377:
378: def restart_args
379: if cmd = @options[:restart_cmd]
380: cmd.split(' ') + @original_argv
(byebug) @restart_argv
["/Users/ylt/.rvm/rubies/ruby-2.1.2/bin/ruby", "-I", "/Users/ylt/tmp/puma/lib", "bin/puma"]
(byebug) $0
"bin/puma"
代码中有不懂的地方,直接在调试环境中执行代码,比如查看@restart_argv和$0到底是什么值。 如果想知道 restart_argv 到底有什么用,用 grep 搜索代码目录,看看在哪个地方使用了这个变量。最终会发现只有一个地方使用了它,简化后的代码是:
def restart!
argv = restart_args
Dir.chdir @restart_dir
argv += [redirects] unless RUBY_VERSION < '1.9'
Kernel.exec(*argv)
end
也就是通过 Kernel.exec 来执行命令行来重启。
###Puma 的运行流程 完成 Puma::CLI 的初始化以后,调用其 run 方法将 puma 实际运行起来,这个 run 方法是 puma 执行的主函数体。
def run
parse_options
set_rack_environment
if clustered?
@events.formatter = Events::PidFormatter.new
@options[:logger] = @events
@runner = Cluster.new(self)
else
@runner = Single.new(self)
end
setup_signals
set_process_title
@status = :run
@runner.run
# 这里等待runner运行结束,一般是收到了KILL类信号
case @status
when :halt
log "* Stopping immediately!"
when :run, :stop
graceful_stop
when :restart
log "* Restarting..."
@runner.before_restart
restart!
when :exit
# nothing
end
end
Puma 运行的第一步是解析命令行参数,然后设置 rack 环境。下一步判断是否运行为集群模式,如果是集群模式,初始化 Cluster 类;如果是单进程模式,初始化 Single 类。然后是设置操作系统信号的处理函数、设置 puma 进程标题。紧接着是设置 puma 主进程的状态为:run,并执行 runner 的 run 方法。run 方法进行几层的封装,最终会执行到 Server 类的 run 方法,其中的关键循环是:
while @status == :run
begin
ios = IO.select sockets
...
end
此时 puma 服务器就可以接收 web 客户端的请求了,同时等待操作系统的退出信号。当收到操作系统的退出信号后,代码执行到 case @status处,这里判断信号类型来决定是停止还是重启 puma。