Ruby 信号处理(Signal Handling)完全指南

信号是操作系统与进程通信的机制。本文讲解 Ruby 的 Signal.trap 用法、优雅停机(Graceful Shutdown)实现、超时控制与自引用管道等高级技巧,覆盖 Web 服务与后台 Worker 场景。

最佳实践
红宝石与应用开发工作台插画

Ruby 用 Signal.trap 注册信号处理器:捕获 SIGTERM/SIGINT 实现优雅停机(完成当前任务、关闭连接再退出)、捕获 USR1/USR2 做运行时控制。生产环境中 Web 服务器和后台 Worker 必须正确处理 SIGTERM,否则容器缩容和部署时会造成请求中断与数据不一致。

信号是什么

信号是内核与进程间的轻量通信:终端里按 Ctrl+C 发 SIGINT、kill PID 默认发 SIGTERM、kill -9 发不可捕获的 SIGKILL。没有自定义处理器时,进程收到多数信号会直接终止——正在写入的文件、处理一半的任务就此悬空。

Signal.trap 基础

Signal.trap('INT') do
  puts '收到 Ctrl+C,正在清理...'
  exit
end

loop { sleep 1 }

常见信号清单:

  • SIGTERM(15):请求终止——容器编排和 systemd 停服务的标准方式,优雅停机的主角;
  • SIGINT(2):终端中断(Ctrl+C);
  • SIGHUP(1):终端挂起,守护进程惯例是收到后重载配置;
  • SIGUSR1/SIGUSR2(10/12):留给应用自定义(如 reopen 日志、切换调试模式);
  • SIGKILL(9)与 SIGSTOP:不可捕获,最后的强制手段。

优雅停机:生产环境的必修课

shutdown = false

Signal.trap('TERM') { shutdown = true }
Signal.trap('INT')  { shutdown = true }

until shutdown
  job = queue.pop(timeout: 1)
  process(job) if job
end

puts '完成手头任务,关闭连接池'
db.disconnect
exit 0

模式要点:trap 块里只设置标志位,主循环检测到标志后跳出、完成清理再退出。为什么不在 trap 里直接清理?因为信号处理器运行在受限上下文(Ruby 的 trap 上下文不允许锁操作,很多方法调用会抛 ThreadError)——把复杂逻辑放主流程是唯一安全写法。

超时控制:Timeout 与信号

Ruby 的 Timeout.timeout 内部就是基于信号触发异常:

require 'timeout'

begin
  Timeout.timeout(5) { slow_external_call }
rescue Timeout::Error
  logger.warn('外部调用超时')
end

注意 Timeout 在线程里raise可能留下不一致状态,网络超时更推荐用客户端自带的超时参数(Net::HTTP 的 open/read_timeout),Timeout 库留给无超时选项的场景。

自引用管道(Self-Pipe)技巧

要在 trap 中做稍复杂的事(如通知事件循环),经典方案是自引用管道:trap 里只向管道写一个字节(异步安全),主循环 IO.select 监听管道读端,读到即执行对应逻辑。Puma、Unicorn 等服务器内部的信号处理都建立在这个模式上。

还原 Ruby 进程退出前的状态

处理 SIGTERM 时,把停止接收请求、等待任务结束和退出结果写入日志。观测云中可结合进程与容器数据,区分发布触发的正常终止、内存压力和应用崩溃。SIGKILL 无法由应用捕获,相关原因应从容器或系统事件补充采集;告警检测窗口也要覆盖实际采集周期,避免将滚动发布中的短暂退出当作故障。

接入文档:进程监控 · 容器与 Kubernetes 采集 · 日志采集 · 监控器与告警策略。

常见问题(FAQ)

Q:为什么我的 SIGTERM 处理器没有执行?

常见原因:进程处于不可中断的系统调用(如卡死的 NFS 读写);Ruby 主线程阻塞在某些 C 扩展中;或信号发给了进程组中错误的 PID。容器里确认应用是 1 号 PID 或用 tini 等 init 转发信号。

Q:trap 里为什么不能直接调 logger 或数据库?

trap 上下文禁止获取互斥锁,而 logger、连接池内部都有锁——调用即抛 ThreadError 或死锁。正确做法就是标志位模式:trap 设置标志,主循环执行真正的逻辑。

Q:Puma/Sidekiq 已处理了信号,我还需要管吗?

框架处理了通用停机逻辑,但业务级清理仍是你的责任:正在跑的长任务如何标记中断、本地临时文件如何清理、外部锁如何释放——通过框架提供的 shutdown 钩子(Puma 的 on_worker_shutdown、Sidekiq 的 quiet/stop 生命周期)挂入业务清理逻辑。


参考文档

本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。

延伸阅读

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台