Rails Spring在生产环境启动的问题

Posted by songjian on June 12, 2024 · 2 mins read

有一个奇怪的问题:Gemfile 中 spring 本来只放在 development 组,按说生产环境不会用到它,但用 Docker 打包镜像后,生产环境里 spring 依然在运行。

排查后发现是两个原因叠加造成的:

  1. COPY . .bin/spring 也拷贝进了镜像;
  2. 镜像里 bundle install 时没有排除 development 组,spring gem 被实际安装。

背后是 Rails 的 binstub 机制:rails new 生成的 bin/rails 顶部本来就有一段尝试加载 bin/spring 的代码:

begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end

bin/spring 会检查 Gemfile.lock 中是否存在 spring 条目(lockfile 始终包含它,与安装了哪些组无关),存在就加载 spring/binstub 并接管之后的命令:

lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
spring = lockfile.specs.detect { |spec| spec.name == "spring" }
if spring
  Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
  gem 'spring', spring.version
  require 'spring/binstub'
end

所以只要镜像里装了 spring gem、bin/spring 又在,生产环境的 rails 命令就会走进 spring。

反过来也说明:如果镜像用 --without development 安装 gem,即使有 bin/spring 也不会运行(gem 加载失败会被 binstub 的 rescue 吞掉)。

spring 是开发环境用的 application preloader,生产环境让它运行会把应用进程变成 spring 的子进程,导致进程管理混乱、内存驻留、信号处理异常等问题,应该避免。

常见的解决办法,按推荐程度排列:

  1. 生产镜像不安装 development/test 组 gem,从根上解决,也减小镜像体积:
RUN bundle config set without 'development test' && bundle install
  1. 直接用环境变量禁用 spring,最省事:
ENV DISABLE_SPRING=1
  1. .dockerignore 中排除 bin/spring
bin/spring
  1. 或像我最初那样,COPY 后删除 binstub。这种方法有效,但只是删掉了触发器,没有解决生产镜像装了开发组 gem 的问题,属于治标:
COPY . .
RUN rm -f bin/spring

rails docker