Rails Rack BUFSIZE 导致大文件上传很慢

night_7th · 2017年07月18日 · 最后由 wpzero 回复于 2017年07月24日 · 3021 次阅读

问题描述

最近在开发一个文件上传功能,开发环境下,上传一个 100 多 M 的文件到服务端,需要花费接近 1 分钟。

file_upload

前端代码是自己写的,也没有用第三方文件上传插件,普通的 jquery,大致长这样:

// 开始上传
var form_data = new FormData();
form_data.append('file', document.getElementById(id).files[0]);

$.ajax({
            url: '/apps/upload',
            type: 'POST',
            data: form_data,
            balabala...
});

后端看日志,其实处理得也没那么久,那时间耗在哪里了呢?

Completed 200 OK in 3786ms (Views: 0.2ms | ActiveRecord: 1131.2ms)

尝试解决

花了俩小时放狗搜,最后发现可能是 Rack 的问题:

Rack::Multipart::Parser.parse中有一个循环,每次读取的 BUFSIZE 大小是 16384 个字节。所以文件很大的话,循环得就很多次了。

已经有人给 Rack 提了一个issue

解决方案也很简单,一位叫 darfux 的同学给出来了,写一个中间件把那个 BUFSIZE 变量覆写掉就好

class MultipartBufferSetter

  def initialize(app)
    @app = app
  end

  def call(env)
    env.merge!(Rack::RACK_MULTIPART_BUFFER_SIZE => 100 * 1024 * 1024)
    @app.call(env)
  end

end

但是按照常理去理解,BUFSIZE 设置为 16384 应当是有原因的,现在被改得这么大有没有潜在的问题还需要观察一阵子,之前使用了 jquery-file-upload 这类第三方插件就没有遇到过这个问题。😨

更新 1

今天早上查了下,jquery-file-upload 这个 js 插件有一个特性:

Chunked uploads: Large files can be uploaded in smaller chunks with browsers supporting the Blob API.

嗯这种长长的请求通过 rack 总归是会有些性能不好的,或者用 nginx-upload-module 解决吧

luikore 回复

9 年前的 module 还正常运行 真是服啊

如果你把这个值调的很大,放到生产环境,很多人都在传大文件,很快你的内存就被爆了

大文件直传云储存。

主要是内存消耗厉害吧。100M 还是有点太大了,大家一起传你内存就炸了。

Rei 回复

云存储也需要后端接收到这个请求再上传到云端啊,现在我遇到的问题是后端需要等很久才能接收到这个请求。

我新建了一个 rails 工程,按照最简单的文件上传步骤也会遇到这个问题。

# Rails version: 5.0.4
# Ruby version: 2.3.1 (x86_64-linux)
Started GET "/resumes/new" for 127.0.0.1 at 2017-07-19 09:48:13 +0800
Processing by ResumesController#new as HTML
  Rendering resumes/new.html.erb within layouts/application
  Rendered resumes/new.html.erb within layouts/application (31.5ms)
Completed 200 OK in 66ms (Views: 60.4ms | ActiveRecord: 0.4ms)


# 这里卡住了很久
Started POST "/resumes" for 127.0.0.1 at 2017-07-19 09:49:31 +0800
Processing by ResumesController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"TEKTZqcg32ef+KFzkl2BQeGi8uOkDI/4hx378tTmroLjOSxzNssAQGS2EkcRX5KWTuoWCQnrI5TnMdjysNtxGw==", "resume"=>{"name"=>"333", "attachment"=>#<ActionDispatch::Http::UploadedFile:0x0000000206c550 @tempfile=#<Tempfile:/tmp/RackMultipart20170719-8518-qzkb8k.apk>, @original_filename="paintbook_111M.apk", @content_type="application/vnd.android.package-archive", @headers="Content-Disposition: form-data; name=\"resume[attachment]\"; filename=\"paintbook_111M.apk\"\r\nContent-Type: application/vnd.android.package-archive\r\n">}, "commit"=>"Save"}
   (0.2ms)  begin transaction
  SQL (0.8ms)  INSERT INTO "resumes" ("name", "attachment", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "333"], ["attachment", "paintbook_111M.apk"], ["created_at", "2017-07-19 01:49:32.031122"], ["updated_at", "2017-07-19 01:49:32.031122"]]
   (217.7ms)  commit transaction
Redirected to http://localhost:3000/resumes
Completed 302 Found in 3541ms (ActiveRecord: 218.7ms)


luikore 回复

😧 如果不通过 rack 怎么处理呢...

night_7th 回复

所以云存储是直传,大文件直接传到云。

varro 回复

噢,这个意思,这个改动有点大,我用的云存储服务商也没有提供 js 直传 SDK,只有移动端的直传 SDK😂

100M 是什么文件?不加密放云存储也挺心大的

nouse 回复

用来提测的 apk

上传大文件和下载一样,可以让 nginx 来负责接收上传的文件,然后 rack 通过 header 从文件系统读取上传完毕的文件:

https://stackoverflow.com/questions/44371643/nginx-php-failing-with-large-file-uploads-over-6-gb

你的前端也尽量把大文件分片上传吧,还可以断点续传。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号