在Heroku中使用delayed_paperclip结合Sidekiq遇文件找不到问题求助
哈哈,这个坑我之前也踩过!Heroku的dyno完全隔离的特性确实会搞出这种问题——web dyno上传的临时文件根本到不了worker dyno,所以delayed_paperclip的worker自然找不到文件。除了直接同步上传S3,还有几个实用的解决方案,按推荐程度排序给你:
1. 先把文件传到S3临时目录,再让worker处理
这是最符合Heroku架构的方案,思路是:web进程先把上传的文件临时存到S3的一个专用目录(比如s3://your-bucket/tmp/uploads/),然后把这个S3路径传给Sidekiq worker;worker从临时目录读取文件,用paperclip处理后存到正式存储区,最后清理临时文件。
具体实现可以这么做:
# Controller 部分 def create @post = Post.new(post_params) if @post.save # 先把上传的文件传到S3临时目录 temp_s3_key = "tmp/uploads/#{SecureRandom.uuid}/#{@post.attachment_file_name}" s3_client = Aws::S3::Client.new s3_client.put_object( bucket: ENV['S3_BUCKET'], key: temp_s3_key, body: @post.attachment.to_file ) # 启动worker处理 ProcessAttachmentJob.perform_async(@post.id, temp_s3_key) redirect_to @post, notice: 'Post created, attachment processing in background!' else render :new end end # Sidekiq Job 部分 class ProcessAttachmentJob include Sidekiq::Job def perform(post_id, temp_s3_key) post = Post.find(post_id) s3_client = Aws::S3::Client.new # 下载临时文件到本地 temp_file = Tempfile.new(['attachment', post.attachment_file_name]) s3_client.get_object( bucket: ENV['S3_BUCKET'], key: temp_s3_key, response_target: temp_file.path ) temp_file.rewind # 交给paperclip处理并保存到正式路径 post.attachment = temp_file post.save! # 清理临时文件 temp_file.close temp_file.unlink s3_client.delete_object(bucket: ENV['S3_BUCKET'], key: temp_s3_key) end end
2. 改用Active Storage + Active Job
如果你的项目还没深度绑定paperclip,强烈建议切换到Rails原生的Active Storage。它天生支持异步上传到S3,而且和Active Job(Sidekiq是它的适配器之一)集成得非常丝滑,完全不用自己处理跨dyno的文件共享问题。
只需要配置好Active Storage的S3服务,然后在模型里用has_one_attached,再启用异步处理:
# config/storage.yml amazon: service: S3 access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> bucket: <%= ENV['S3_BUCKET'] %> region: <%= ENV['AWS_REGION'] %> # 模型里 class Post < ApplicationRecord has_one_attached :attachment, dependent: :destroy # 启用异步处理 after_create_commit :process_attachment_later private def process_attachment_later AttachmentProcessingJob.perform_later(self) end end # Active Job 可以做一些图片裁剪、格式转换等操作 class AttachmentProcessingJob < ApplicationJob queue_as :default def perform(post) if post.attachment.attached? # 比如用vips处理图片 post.attachment.variant(resize_to_limit: [800, 800]).processed end end end
Active Storage会自动处理临时文件的存储和清理,完全不用你操心跨dyno的问题。
3. 把文件二进制数据传给Worker(适合小文件)
如果你的附件都是小文件(比如几MB以内),可以直接把文件的二进制数据编码后传到Sidekiq Job里,Worker再把数据写入本地临时文件,交给paperclip处理。
注意:Sidekiq的Job payload存在Redis里,太大的文件会导致Redis性能下降,所以这个方案只适合小文件。
示例代码:
# Controller def create @post = Post.new(post_params) if @post.valid? # 读取文件二进制数据 file_data = @post.attachment.to_file.read # 先保存模型(不带附件) @post.attachment = nil @post.save! # 启动worker,传入模型ID和文件数据 ProcessAttachmentJob.perform_async(@post.id, file_data, @post.attachment_file_name) redirect_to @post, notice: 'Post created, attachment processing...' else render :new end end # Sidekiq Job class ProcessAttachmentJob include Sidekiq::Job def perform(post_id, file_data, file_name) post = Post.find(post_id) # 创建临时文件并写入数据 temp_file = Tempfile.new(['attachment', file_name]) temp_file.write(file_data) temp_file.rewind # 绑定到模型并保存 post.attachment = temp_file post.save! # 清理临时文件 temp_file.close temp_file.unlink end end
4. 共享文件系统(不推荐)
Heroku有第三方插件支持共享文件系统(比如AWS EFS挂载),这样所有dyno都能访问同一个文件系统,web上传的临时文件worker也能读到。但这个方案不符合Heroku的无状态架构理念,成本高,而且性能和一致性都有隐患,除非万不得已不建议用。
总结一下,优先选方案1或者方案2,这两个最稳定也最符合Heroku的设计思路。
内容的提问来源于stack exchange,提问作者sethi

