分享 Puma 源代码分析 - 启动流程

ylt · 2015年02月28日 · 最后由 ylt 回复于 2016年07月03日 · 8534 次阅读
本帖已被管理员设置为精华贴

Puma 的启动方式

puma 有两种启动方式:一种是通过标准的 rack 接口启动,一种是通过 puma 提供的命令行工具启动。

rack 接口

首先,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

命令行工具 bin/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 的流程。启动过程分两步,首先是初始化 Puma::CLI 类,然后执行 cli.run 方法。

Puma 的 CLI 初始化

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。

deploy@iZ94xfgfq3jZ:~/web/ethan$ bundle exec pumactl -C config/puma.rb start
[2066] Puma starting in cluster mode...
[2066] * Version 2.9.1 (ruby 2.2.0-p0), codename: Team High Five
[2066] * Min threads: 0, max threads: 12
[2066] * Environment: production
[2066] * Process workers: 1
[2066] * Preloading application
[2066] * Listening on unix:///var/run/ethan.blog.sock
Permission denied - connect(2) for /var/run/ethan.blog.sock

部署的时候出现如上错误,怎么破?

@ylt 可以使用 ack 来代替 grep,效果更理想 💯

#6 楼 @zhangsm 问题解决了吗?提示访问文件/var/run/ethan.blog.sock 结果没有权限,比如运行 touch /var/run/ethan.blog.sock或者cat /var/run/ethan.blog.sock看报什么错。

#7 楼 @jsvisa 是的,听说过 ack,应该更好用。可能我 grep 用久了,没想过换其它的。

#8 楼 @ylt 解决了,谢谢你!原因是/var/run/文件是 root 的,不是 deploy。所以 sudo chown -R deploy:deploy /var/run/ 就可以了。

ylt Puma 源代码分析 - 概述 提及了此话题。 07月03日 22:43
需要 登录 后方可回复, 如果你还没有账号请 注册新账号