Ruby守护进程

大部分的时间你将使用一个适当的第三方服务器如Unicorn、Resque或RackRabbit,但有时你只是想写一些Ruby代码,让它以守护进程的形式运行。要实现这一点,你可以使用gem安装Dante,或Daemon-kit,这些都是不错的选择,但对一个简单的工作你可以避免额外的gem依赖,进而对底层Unix进程机制有更深层次的理解。

Ruby命令行程序:

OK,考虑一个简单的Ruby脚本 bin/server 包含一个无限的工作循环。在现实生活中,循环可能是监听套接字传入的请求,也可能订阅传入进消息队列的消息,或它可能在数据库中轮询工作表。在我们的示例中,我们将实现一个简单的空闲循环:

#!/usr/bin/env ruby
while true
  puts "Doing some work"
  sleep(2)     # something productive should happen here
end

你可以在命令行运行该ruby程序:

$ bin/server
Doing some work
Doing some work
Doing some work
...

NOTE: 对于生产环境,我们需要能够将此循环作为守护进程运行,从终端分离,并将其输出重定向到日志文件中。

本文将介绍实现此目标所需的步骤…

代码分离

虽然我们的例子是一个简单的空闲循环,在现实中,你的过程将更复杂,可能包含在自己的Ruby类中。 通常的做法是将可执行脚本与主代码分开:

  • lib/server.rb – 执行实际工作的Server类;
  • bin/server – 一个简单的包装器,它解析选项并实例化我们的Server类。

首先,让我们将空闲循环移动到 lib/server.rb 中的Server类:

class Server

  attr_reader :options

  def initialize(options)
    @options = options
  end

  def run!
    while true
      puts "Doing some work"
      sleep(2)
    end
  end

end

现在我们在 bin/server 中的可执行脚本变成一个简单的包装器:

#!/usr/bin/env ruby
require 'server'
options = {}                # see next section
Server.new(options).run!

NOTE:为了运行可执行文件,我们现在需要在Ruby 的 \$LOAD_PATH 中包含我们的lib目录 – 稍后我们将简化这个要求。

$ ruby -Ilib bin/server
Doing some work
Doing some work
Doing some work
...

命令行参数

我们希望守护进程选项可以从我们脚本的命令行控制,我们可能想要指定的一些选项包括:
* –daemonize
* –pid PIDFILE
* –log LOGFILE

我们可以使用Ruby的内置OptionParser扩展我们的 bin/server 脚本

#!/usr/bin/env ruby

require 'optparse'

options        = {}
version        = "1.0.0"
daemonize_help = "run daemonized in the background (default: false)"
pidfile_help   = "the pid filename"
logfile_help   = "the log filename"
include_help   = "an additional $LOAD_PATH"
debug_help     = "set $DEBUG to true"
warn_help      = "enable warnings"

op = OptionParser.new
op.banner =  "An example of how to daemonize a long running Ruby process."
op.separator ""
op.separator "Usage: server [options]"
op.separator ""

op.separator "Process options:"
op.on("-d", "--daemonize",   daemonize_help) { options[:daemonize] = true  }
op.on("-p", "--pid PIDFILE", pidfile_help)   { |value| options[:pidfile]   = value }
op.on("-l", "--log LOGFILE", logfile_help)   { |value| options[:logfile]   = value }
op.separator ""

op.separator "Ruby options:"
op.on("-I", "--include PATH", include_help) { |value| $LOAD_PATH.unshift(*value.split(":").map{|v| File.expand_path(v)}) }
op.on(      "--debug",        debug_help)   { $DEBUG = true }
op.on(      "--warn",         warn_help)    { $-w = true    }
op.separator ""

op.separator "Common options:"
op.on("-h", "--help")    { puts op.to_s; exit }
op.on("-v", "--version") { puts version; exit }
op.separator ""

op.parse!(ARGV)

require 'server'
Server.new(options).run!

NOTE:通过添加标准的ruby选项,如-I,我们可以运行可执行文件,我们的lib目录在Ruby \$LOAD_PATH 稍微简单一点 – 我们将在以后完全删除这个要求。

$ bin/server -Ilib   # a touch simpler than `ruby -Ilib bin/server`
Doing some work
Doing some work
Doing some work
...

扩展Server类

我们现在有一个可执行脚本,它可以解析命令行选项,并将它们传递到新构造的Server类里。 接下来,我们必须扩展Server类以对这些选项执行操作。

我们首先在选项和一些帮助方法上添加一些预处理:

class Server

  attr_reader :options, :quit

  def initialize(options)

    @options = options

    # daemonization will change CWD so expand relative paths now
    options[:logfile] = File.expand_path(logfile) if logfile?
    options[:pidfile] = File.expand_path(pidfile) if pidfile?

  end

  def daemonize?
    options[:daemonize]
  end

  def logfile
    options[:logfile]
  end

  def pidfile
    options[:pidfile]
  end

  def logfile?
    !logfile.nil?
  end

  def pidfile?
    !pidfile.nil?
  end

  # ...

接下来是关键的补充,扩展 #run! 方法,以确保进程在进行长时间运行工作之前获得守护进程通过使用几个关键辅助方法(我们将在后面单独访问):

  • check_pid – 确保服务器尚未运行
  • daemonize – 将进程从终端分离
  • write_pid – 将进程的PID写入文件中
  • trap_signals – 捕获QUIT信号以允许正常关闭
  • redirect_output – 确保输出重定向到日志文件
  • suppress_output – 禁止日志输出(如果没有提供日志文件)

#run! 方法的扩展如下:

class Server

  def run!

    check_pid
    daemonize if daemonize?
    write_pid
    trap_signals

    if logfile?
      redirect_output
    elsif daemonize?
      suppress_output
    end

    while !quit
      puts "Doing some work"
      sleep(2)
    end

  end

  # ...

NOTE:我们还添加了一个 :quit 属性,并用它来终止长运行循环。 此属性将由我们的UNIX信号在后段处理设置为true。

PID文件管理

一个守护进程应该将它的PID写入一个文件,并在进程退出时删除该文件:

def write_pid
  if pidfile?
    begin
      File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY){|f| f.write("#{Process.pid}") }
      at_exit { File.delete(pidfile) if File.exists?(pidfile) }
    rescue Errno::EEXIST
      check_pid
      retry
    end
  end
end

PID文件还可用于确保在服务器首次启动时实例尚未运行:

def check_pid
  if pidfile?
    case pid_status(pidfile)
    when :running, :not_owned
      puts "A server is already running. Check #{pidfile}"
      exit(1)
    when :dead
      File.delete(pidfile)
    end
  end
end

def pid_status(pidfile)
  return :exited unless File.exists?(pidfile)
  pid = ::File.read(pidfile).to_i
  return :dead if pid == 0
  Process.kill(0, pid)      # check process status
  :running
rescue Errno::ESRCH
  :dead
rescue Errno::EPERM
  :not_owned
end

守护进程

从终端和守护进程分离的实际行为涉及两个操作,设置会话ID,以及改变工作目录。 Jesse在自己的书中很好的解释了守护进程,并幸运的是:他向我们发表了一个样例章节:

def daemonize
  exit if fork
  Process.setsid
  exit if fork
  Dir.chdir "/"
end

这里的示例代码与Jesse的书的主要区别是拆分出 \$stdout 和 \$stderr 重定向到一个单独的方法…

输出重定向

如果调用者指定了一个日志文件,那么我们必须重定向 \$stdout 和 \$stderr:

def redirect_output
  FileUtils.mkdir_p(File.dirname(logfile), :mode => 0755)
  FileUtils.touch logfile
  File.chmod(0644, logfile)
  $stderr.reopen(logfile, 'a')
  $stdout.reopen($stderr)
  $stdout.sync = $stderr.sync = true
end

如果调用者没有指定一个日志文件,但确实要求进行守护进程,那么我们必须从终端断开 \$stdout 和 \$stderr 并抑制输出:

def suppress_output
  $stderr.reopen('/dev/null', 'a')
  $stdout.reopen($stderr)
end

信号处理

对于一个简单的守护进程(例如:不创建子进程的程序),我们可以使用默认的Ruby信号处理机制。 如果我们想要执行正常关闭,我们可能想要捕获的唯一信号是QUIT:

def trap_signals
  trap(:QUIT) do   # graceful shutdown of run! loop
    @quit = true
  end
end

所有的代码块合并

总结我们刚刚进行的步骤:

  • We started with a simple long-running Ruby Process
  • We separated our concerns into bin/server and lib/server.rb
  • We parsed command line options
  • We extending the server in order to…
  • Manage the PID file
  • Daemonize the process
  • Redirect output to a logfile
  • Trap the QUIT signal for graceful shutdown

您可以在GitHub上找到完整的工作示例:

相关链接

…购买Jesse的书:
* 理解UNIX进程

…探索第三方的Ruby守护进程:

…其他有关的文章:

参考资料

  1. Daemonizing Ruby Processes

暂无评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Time limit is exhausted. Please reload CAPTCHA.