有一个奇怪的问题:Gemfile 中 spring 本来只放在 development 组,按说生产环境不会用到它,但用 Docker 打包镜像后,生产环境里 spring 依然在运行。
排查后发现是两个原因叠加造成的:
COPY . . 把 bin/spring 也拷贝进了镜像;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 的子进程,导致进程管理混乱、内存驻留、信号处理异常等问题,应该避免。
常见的解决办法,按推荐程度排列:
RUN bundle config set without 'development test' && bundle install
ENV DISABLE_SPRING=1
.dockerignore 中排除 bin/spring:bin/spring
COPY . .
RUN rm -f bin/spring