You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:20:34