Heroku部署Sinatra应用持久化C依赖库及修复H10报错问题
Heroku部署Ruby FFI调用C库应用H10错误及C库持久化方案
问题背景
- 部署通过Ruby FFI调用C库的Sinatra Web应用时触发H10崩溃错误,应用本地运行完全正常
- 依赖的Swiss Ephemeris C库需要在部署机完成编译,本地环境通过Rakefile执行编译操作,但Heroku采用临时文件系统,运行阶段生成的文件会在dyno重启/销毁后丢失
- 先后尝试自定义Ruby版buildpack、自定义Bash版buildpack、release阶段执行rake编译三种方案,编译阶段日志显示已成功生成目标文件,但应用启动、执行
heroku run bash时均找不到编译生成的libswe.so文件,报错信息如下:
/app/vendor/bundle/ruby/3.0.0/gems/ffi-1.15.5/lib/ffi/library.rb:145:in `block in ffi_lib': Could not open library '/app/src/libswe.so': /app/src/libswe.so: cannot open shared object file: No such file or directory (LoadError)
已尝试的失败方案
1. 自定义Ruby版本buildpack
核心实现代码:
#!/usr/bin/env ruby # This script compiles an application so it can run on Heroku. # It will install the application's specified version of Ruby, it's dependencies # and certain framework specific requirements (such as calling `rake assets:precompile` # for rails apps). $stdout.sync = true $:.unshift File.expand_path("../../../lib", __FILE__) require "language_pack" require "language_pack/shell_helpers" begin # 自定义添加的C库编译逻辑 `mkdir /app/src` `cd /app && curl -s https://www.astro.com/ftp/swisseph/swe_unix_src_2.10.02.tar.gz | tar xzvf -` `cd '/app/src' && make libswe.so` # 自定义逻辑结束 LanguagePack::ShellHelpers.initialize_env(ARGV[2]) if pack = LanguagePack.detect(ARGV[0], ARGV[1]) pack.topic("Compiling #{pack.name}") pack.log("compile") do pack.compile end end rescue Exception => e LanguagePack::ShellHelpers.display_error_and_exit(e) end
2. 自定义Bash版本buildpack
核心实现代码:
#!/usr/bin/env bash # The actual compilation code lives in `bin/support/ruby_compile`. This file instead # bootstraps the ruby needed and then executes `bin/support/ruby_compile` BUILD_DIR=$1 CACHE_DIR=$2 ENV_DIR=$3 BIN_DIR=$(cd $(dirname $0); pwd) BUILDPACK_DIR=$(dirname $BIN_DIR) # 自定义添加的C库编译逻辑 mkdir /app/src cd /app && curl -s https://www.astro.com/ftp/swisseph/swe_unix_src_2.10.02.tar.gz | tar xzvf - cd '/app/src' && make libswe.so # 自定义逻辑结束 source "$BIN_DIR/support/bash_functions.sh" heroku_buildpack_ruby_install_ruby "$BIN_DIR" "$BUILDPACK_DIR" if detect_needs_java "$BUILD_DIR"; then cat <<EOM ## Warning: Your app needs java The Ruby buildpack determined your app needs java installed we recommend you add the jvm buildpack to your application: $ heroku buildpacks:add heroku/jvm --index=1 -----> Installing Java EOM compile_buildpack_v2 "$BUILD_DIR" "$CACHE_DIR" "$ENV_DIR" "https://buildpack-registry.s3.us-east-1.amazonaws.com/buildpacks/heroku/jvm.tgz" "heroku/jvm" fi $heroku_buildpack_ruby_dir/bin/ruby $BIN_DIR/support/ruby_compile $@
3. Release阶段执行编译任务
Procfile配置:
release: bundle exec rake web: bundle exec thin start -R config.ru -e $RACK_ENV -p ${PORT:-5000}
对应Rakefile配置:
#require "bundler/gem_tasks" task default: [:clean, :c_build, :get_ephe] task :clean do `rm ./src/libswe.so` `rm -rf ephe` end task :c_build do `wget https://www.astro.com/ftp/swisseph/swe_unix_src_2.10.02.tar.gz` `tar xvf swe_unix_src_2.10.02.tar.gz` `rm swe_unix_src_2.10.02.tar.gz` `cd src && make libswe.so && echo "Compiled Library"` end task :get_ephe do `mkdir ephe` `wget -P ephe https://www.astro.com/ftp/swisseph/ephe/seas_12.se1` `wget -P ephe https://www.astro.com/ftp/swisseph/ephe/seas_18.se1` `wget -P ephe https://www.astro.com/ftp/swisseph/ephe/sefstars.txt` `wget -P ephe https://www.astro.com/ftp/swisseph/ephe/semo_12.se1` `wget -P ephe https://www.astro.com/ftp/swisseph/ephe/semo_18.se1` `wget -P ephe https://www.astro.com/ftp/swisseph/ephe/sepl_12.se1` `wget -P ephe https://www.astro.com/ftp/swisseph/ephe/sepl_18.se1` end
失败根本原因
- 自定义buildpack时直接操作
/app目录属于路径使用错误:buildpack编译阶段传入的第一个参数$BUILD_DIR才是最终会被打包进运行时slug的目录,编译阶段临时挂载的/app目录内容不会被带入运行时环境,所以编译完成的文件在应用启动后会消失 - Release阶段的命令运行在一次性临时dyno上,任务执行完后该dyno会被直接销毁,生成的所有文件都不会留存,自然无法被Web dyno读取
- Heroku不支持从dyno运行时侧直接推送文件到应用关联的Git仓库
可行解决方案
方案1:修正自定义buildpack逻辑(最优)
- 所有下载、编译操作的输出路径都指向
$BUILD_DIR(即buildpack脚本接收的第一个位置参数),不要直接写入根目录下的/app - 编译完成后,要么把生成的
.so库所在路径加入动态链接库搜索路径,要么在Ruby FFI调用代码里写对运行时/app目录下的库存取路径 - 可以用buildpack传入的第二个参数
$CACHE_DIR缓存下载的源码压缩包,减少每次部署的重复下载量,加快编译速度
方案2:预编译后直接提交到Git仓库
- 在和Heroku运行时环境一致的Linux x86_64环境下提前编译好
libswe.so,连同需要的星历文件一起提交到项目Git仓库 - 部署时直接引用仓库内的预编译文件即可,不需要在部署阶段执行编译操作
注意:预编译环境必须和Heroku运行时的系统架构、基础依赖版本保持一致,否则会出现库兼容性报错
方案3:通过Apt buildpack安装预编译依赖
如果目标C库有现成的预编译deb安装包,可以搭配Apt buildpack在部署阶段直接通过apt安装系统级依赖,不需要手动执行编译流程
内容的提问来源于stack exchange,提问作者Parker McGowan
相关产品推荐
相关产品推荐

