安装Ruby Gems与运行依赖脚本时的竞态条件解决方法
我们的环境通过Ansible配置Ruby Gems,部分Gems要求安装不低于指定版本(例如aws-sdk-core >=3.104),对应的Ansible执行命令:
gem install -v '>= 3.104' aws-sdk-core
同时有一个每5分钟在数千台服务器上运行的cron任务,脚本包含require 'aws-sdk-core'语句,但偶尔会出现加载错误:
/var/lib/gems/2.5.0/gems/aws-sdk-core-3.166.0/lib/seahorse.rb:3:in `require_relative': cannot load such file -- /var/lib/gems/2.5.0/gems/aws-sdk-core-3.166.0/lib/seahorse/util (LoadError)
通过复现脚本验证:在一个终端循环执行while true; do ./z.rb; done(z.rb含require 'progressbar'),另一个终端循环执行while true; do gem install -v '>= 1.0.0' progressbar; done,1-2分钟后会触发LoadError。原因是Gem安装过程中文件未完全写入时,被cron脚本读取,导致加载失败。
1. 临时目录安装+原子替换
安装Gem时先写入临时目录,确保整个安装流程完成后,再将完整的Gem文件原子性替换到系统Gem目录,避免半安装状态被读取。
Ansible任务示例:
gem install -v '>= 3.104' aws-sdk-core --install-dir /tmp/gem_install_temp && \ # 复制Gem文件到系统目录 cp -r /tmp/gem_install_temp/gems/aws-sdk-core-* /var/lib/gems/2.5.0/gems/ && \ # 复制规格文件 cp /tmp/gem_install_temp/specifications/aws-sdk-core-*.gemspec /var/lib/gems/2.5.0/specifications/ && \ # 清理临时目录 rm -rf /tmp/gem_install_temp
注:如果担心版本冲突,可以先备份旧版本,替换完成后再删除旧版,或者用rsync确保文件同步完整。
2. 加锁限制并发访问
利用文件锁工具flock,在Ansible执行Gem安装时锁定系统Gem目录,阻止cron脚本在安装过程中读取该目录,从根源避免并发冲突。
同时先检查当前是否已有满足版本的Gem,仅在需要更新时执行安装:
- name: 检查aws-sdk-core是否满足版本要求 command: gem list aws-sdk-core -i -v '>= 3.104' register: gem_check_result ignore_errors: true - name: 加锁安装/更新aws-sdk-core command: flock /var/lib/gems/2.5.0/gems/ -c "gem install -v '>= 3.104' aws-sdk-core" when: gem_check_result.rc != 0
3. 使用Bundler隔离运行环境
为cron脚本创建独立的Gem环境,用Bundler管理版本,安装更新时在本地目录完成,确保加载的是完整安装的Gem版本。
步骤:
- 在脚本目录创建
Gemfile:source 'https://rubygems.org' gem 'aws-sdk-core', '>= 3.104' - 修改cron脚本的shebang为:
#!/usr/bin/env bundle exec ruby - Ansible中执行更新部署:
cd /path/to/script/dir && \ bundle update aws-sdk-core --local && \ bundle install --deployment --quiet
Bundler的--deployment模式会将Gem安装到本地vendor/bundle目录,更新时是完整替换,不会出现半安装状态。
4. 错开任务执行时间
如果对Gem更新的及时性要求不高,可以调整Ansible的Gem更新任务执行时间,避开cron任务的运行窗口。例如cron每5分钟执行一次(0,5,10...分),安排Ansible在每个小时的2,7,12...分之外的时间执行更新,减少并发概率。
内容的提问来源于stack exchange,提问作者chooboo

