【问题标题】:503 Slow Down when uploading to S3 with Carrierwave使用 Carrierwave 上传到 S3 时出现 503 减速
【发布时间】:2020-11-28 08:11:10
【问题描述】:

我在后台作业中使用 Rails 和 Carrierwave 将许多小文件上传到 S3,并且达到了 S3 速率限制。我的直接想法是在每次上传之前添加一个sleep 0.1,但这似乎不是一个很好的解决方案。

关于如何通过 S3 API 和某种类型的退避来处理这个问题的任何建议?

正在上传的Ruby代码,这个方法被循环调用数千次:

    def attach_audio(object:, audio_field:, attachment:)
      return true if Rails.env.test?

      language_code, voice_id = language_and_voice(object)

      resp = polly.synthesize_speech(
        output_format: 'mp3',
        voice_id: voice_id,
        text: audio_field.to_s,
        language_code: language_code
      )

      audio_filename = "#{object.class.to_s.downcase}_#{attachment}_#{object.id}_#{voice_id}.mp3"
      audio_path = "#{Rails.root}/db/audio/#{audio_filename}"
      IO.copy_stream(resp.audio_stream, audio_path)

      object.send(attachment + '=', Pathname.new(audio_path).open)
      object.save!
    end

上传者类

class AudioUploader < BaseUploader

  def store_dir
    "uploads/audio/#{model.target_language}/#{self.class.to_s.underscore}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  def extension_whitelist
    %w[mp3]
  end
end
class BaseUploader < CarrierWave::Uploader::Base
  if Rails.env.test?
    storage :file
  else
    storage :fog
  end

  def store_dir
    "uploads/#{self.class.to_s.underscore}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
end

来自 AWS 的响应

Message

Excon::Error::ServiceUnavailable: Expected(200) <=> Actual(503 Service Unavailable) excon.error.response :body => "<Error><Code>SlowDown</Code><Message>Please reduce your request rate.</Message><RequestId>176C22715A856A29</RequestId><HostId>L/+

Traceback

Excon::Error::ServiceUnavailable: Expected(200) <=> Actual(503 Service Unavailable)
excon.error.response
  :body          => "<Error><Code>SlowDown</Code><Message>Please reduce your request rate.</Message><RequestId>176C22715A856A29</RequestId><HostId>xxxxxxxxxxxxxxxxxxxxxxxxx</HostId></Error>"
  :cookies       => [
  ]
  :headers       => {
    "Connection"       => "close"
    "Content-Type"     => "application/xml"
    "Date"             => "Wed, 18 Nov 2020 07:31:29 GMT"
    "Server"           => "AmazonS3"
    "x-amz-id-2"       => "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    "x-amz-request-id" => "176C22715A856A29"
  }
  :host          => "example-production.s3-eu-west-1.amazonaws.com"
  :local_address => "xxx.xx.xxx.xxx"
  :local_port    => 50276
  :path          => "/uploads/audio/fr/audio_uploader/word/audio_file/8015423/word_audio_file_8015423_Mathieu.mp3"
  :port          => 443
  :reason_phrase => "Slow Down"
  :remote_ip     => "xx.xxx.xx.x"
  :status        => 503
  :status_line   => "HTTP/1.1 503 Slow Down\r\n"

  File "/app/vendor/bundle/ruby/2.6.0/gems/excon-0.71.1/lib/excon/middlewares/expects.rb", line 13, in response_call
  File "/app/vendor/bundle/ruby/2.6.0/gems/excon-0.71.1/lib/excon/middlewares/response_parser.rb", line 12, in response_call
  File "/app/vendor/bundle/ruby/2.6.0/gems/excon-0.71.1/lib/excon/connection.rb", line 448, in response
  File "/app/vendor/bundle/ruby/2.6.0/gems/excon-0.71.1/lib/excon/connection.rb", line 279, in request
  File "/app/vendor/bundle/ruby/2.6.0/gems/fog-xml-0.1.3/lib/fog/xml/sax_parser_connection.rb", line 35, in request

etc

编辑

链接的 AWS 文档引用了前缀,这似乎可以解决问题

Amazon S3 自动扩展到高请求率。例如,您的应用程序可以在存储桶中每个前缀每秒至少完成 3,500 个 PUT/COPY/POST/DELETE 或 5,500 个 GET/HEAD 请求。桶中的前缀数量没有限制。您可以通过并行读取来提高读取或写入性能。例如,如果您在 Amazon S3 存储桶中创建 10 个前缀来并行读取,您可以将读取性能扩展到每秒 55,000 个读取请求。

但我不明白如何在 Carrierwave 的上下文中实现它。

【问题讨论】:

  • 似乎有一些前缀形式的解决方案,但是我不是很理解文章docs.aws.amazon.com/AmazonS3/latest/dev/…
  • 那么您认为每秒有多少次上传?你使用 Sidekiq 还是 ActiveJob?
  • 如果您阅读 aws 文档,他们建议在您开始变慢响应 docs.aws.amazon.com/general/latest/gr/api-retries.html 时使用指数退避。我对这些库不太熟悉,但我认为您可以挽救错误,解析 AWS 错误代码的主体,然后等待 (2^retry_count) 秒后再重试
  • s3fs 会是答案吗?而不是上传,您将写入一个 s3fs 挂载的分区,最终结果是相同的。你可以在这里看到我对类似问题的回答stackoverflow.com/questions/60311166/ffmpeg-pipe-segments-to-s3/…
  • 就像休伯特问的那样:您有多少并行上传正在运行?我已经使用 S3 多年了,但我从来没有达到过他们的极限……

标签: ruby-on-rails amazon-s3


【解决方案1】:

来自here

例如,您的应用程序至少可以达到 3,500 每个前缀每秒 PUT/COPY/POST/DELETE 或 5,500 个 GET/HEAD 请求 在一个桶里。

你知道你的极限是什么。现在您需要了解前缀 是什么,这很容易。考虑一下:

/uploads/audio/fr/audio_uploader/word/audio_file/8015423/word_audio_file_8015423_Mathieu.mp3

这里的前缀是什么?答:

/uploads/audio/fr/audio_uploader/word/audio_file/8015423

前缀是除对象名称之外的所有内容。因此,您的问题的答案在于您设计方案的能力,以便您永远不会超过亚马逊为每个 prefix 定义的限制。

例如,您可以使用一个旋转计数器,比如说从 0 到 99,并将正在保存的对象与它所在的旋转计数器位置之间的关系存储在某处已存储[以便您以后可以阅读]。如果你要实现这个,你的问题将减少到现在的 1/100;您实际上可能不需要一直增加到 100,如果需要,您可以随时增加它。所以现在,这个:

/uploads/audio/fr/audio_uploader/word/audio_file/8015423/word_audio_file_8015423_Mathieu.mp3

将存储在:

/uploads/audio/fr/audio_uploader/word/audio_file/00/8015423/word_audio_file_8015423_Mathieu.mp3

下一个在 .../01/... 以此类推,第 100 个对象存储在 .../99/... strong> 然后将第 101 个对象存储回 .../00/... [您显然不必使用这两个字符]。

此过程为您的逻辑带来的额外步骤是,出于检索目的,您需要知道 word_audio_file_8015423_Mathieu.mp3 位于 .../00/...例如,word_audio_file_8015424_Mark.mp3.../01/... 等。这意味着您必须存储对象与其保存位置之间的关系。另一方面,如果可以接受搜索所有地点以寻找您想要的对象,您甚至可能不需要这样做。

我强烈认为这会解决您的问题。

【讨论】:

  • 这很有帮助:“前缀是除对象名称之外的所有内容”。我仍然需要弄清楚如何更改 Uploader 类以自动执行此操作。
  • 如果前缀是“/uploads/audio/fr/audio_uploader/word/audio_file/8015423”,最后一部分(8015423)是数据库ID,所以它总是唯一的。我认为这意味着前缀也是唯一的?
  • 你是说每个文件夹只上传一个吗?如果您浏览文件夹 /uploads/audio/fr/audio_uploader/word/audio_file/8015423 那里只有一个文件?
  • AWS 消息非常有意义。您正在以超出限制的速率点击前缀 /uploads/audio/fr/audio_uploader/word/audio_file/。您会看到,/uploads/audio/fr/audio_uploader/word/audio_file/ 是所有数据库 ID 的前缀,并且您正在以非常快的速度创建数据库 ID。我觉得我提出的解决方案会起作用,最坏的情况下你可以并行化甚至超过 100 个;如果 100 仍然给出 503 错误,您可以转到 1000。
【解决方案2】:

如果您在没有 ActiveJob 的情况下使用 Sidekiq,您可以使用 sidekiq-throttled gem 和阈值选项来减慢后台作业中的上传速度。

例子:

class UploadWorker
  include Sidekiq::Worker
  include Sidekiq::Throttled::Worker

  sidekiq_options :queue => :uploads
  sidekiq_throttle({
    # Allow maximum 1K jobs being processed within one second window.
    :threshold => { :limit => 1_000, :period => 1.second }
  })

  def perform
    # do your thing
  end
end

【讨论】:

  • 有趣的方法。我想知道 S3 文档中的前缀是什么意思。
【解决方案3】:

根据AWS docs prefix aka key prefix 类似于目录名称,可以让您将类似的数据存储在存储桶中的同一目录下。您需要找到如何对上传进行分组的方式。在您的情况下,它可能会从 object.id 值创建其他目录作为名称。

【讨论】:

  • 谢谢,已经是这样了:“/uploads/audio/fr/audio_uploader/word/audio_file/8015423/word_audio_file_8015423_Mathieu.mp3”,另见:“”uploads/audio/#{model .target_language}/#{self.class.to_s.underscore}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  • Ups,对不起,我没有注意到。根据文档,我认为这是解决方法。
【解决方案4】:

我尝试使用https://github.com/nickelser/activejob-traffic_control,但无法正常工作。

最后,我找到了一个超级简单的解决方案:我将 S3 中每个单词的音频创建和存储移到了一个新的 ActiveJob 类中。然后只调用了 1000 次,它就会被 Sidekiq 并发设置自动限制。

config/sidekiq.yml

---
:concurrency: 10
:max_retries: 3
:queues:
  - [urgent, 4]
  - [nlp, 3]
  - [default, 2]
  - [low]

【讨论】:

    猜你喜欢
    • 2017-12-14
    • 1970-01-01
    • 2013-08-25
    • 2012-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多