V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
humbass
V2EX  ›  Node.js

关于断点续传

  •  
  •   humbass · 122 天前 · 5039 次点击
    这是一个创建于 122 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近一个项目要用到大文件的断点续传,多大文件呢,大的包可能会有 20G ,目前的做法是

    前端取到文件后,按 2m 一个片段进行分片,然后逐个上传 后端收到完整的文件后放在一个隐藏的目录内,等最后的文件传完之后,逐一合并,并移动到指定的文件夹

    实现是实现了,最大的问题不是上传,而是合并以及转移文件相当消耗时间

    有没有更高级的做法呢?

    第 1 条附言  ·  112 天前
    感谢 @25 @33 @42 ,最后采用 tus ,基本上傻瓜式接入,完美!!
    53 条回复
    vivisidea
        1
    vivisidea  
       122 天前
    是需要完全要自己实现么?你们项目有没有用对象存储?对象存储 sdk 一般都支持这个分片上传+合并操作的

    https://aws.amazon.com/cn/blogs/compute/uploading-large-objects-to-amazon-s3-using-multipart-upload-and-transfer-acceleration/
    mx1700
        2
    mx1700  
       122 天前 via Android   ❤️ 6
    移动如果同一硬盘不应该耗时啊
    合并的方式不太对,应该直接创建完整大小的空文件,上传接口拿到的缓冲数据直接写到完整文件的指定位置,不需要合并操作
    humbass
        3
    humbass  
    OP
       122 天前
    @vivisidea 因为是企业内网网络(含广义上的内网)使用,不能使用第三方的云端存储。
    misoomang
        4
    misoomang  
       122 天前
    开源的对象存储 minio 是否可以考虑使用搭建
    InDom
        5
    InDom  
       121 天前   ❤️ 1
    @mx1700 #2 认可 2 楼的思路,分片上传只是在上传过程中分片处理。

    保存时可以分片最后合并,也可以直接写到最终文件的对应位置。

    自己做一下记录都接收到哪些块了,这样也方便协商未完成的部份。
    Ipsum
        6
    Ipsum  
       121 天前   ❤️ 7
    为什么不能直接按全文件大小在指定位置建立空文件,然后 offset 写实际数据进去呢?设置个 cron 。超过 3 天没传完自动删除。
    MoHen9
        7
    MoHen9  
       121 天前 via Android   ❤️ 4
    分成很多个小文件?说明你实现的断点续传不对,断点续传是一个大文件分成多个请求,每个请求只传自己分好的那一段数据,比如 20 个 G 分成 20 个请求,每个请求传 1G ,第一个请求传从 0 到 1G ,第二个请求从 1G 到 2G ,以依次类推,服务器也是一个文件,根据请求写入文件对应的位置。请求头会携带一些分段的信息,这个有标准的 header ,好像是 range ,可以搜一下
    scegg
        8
    scegg  
       121 天前
    1 建立一个临时文件储存区。
    2 上传文件方法分为
    ( 1 )新建临时文件,body 是文件的第一段,post 成功后将数据保存在临时文件区,文件名可以为一个随机 guid ,返回这个 id 。
    ( 2 )附加临时文件,body 是文件的第 N 段,将方法 1 返回的 id 作为 query 参数一并提供,post 成功后将新提供的数据附加在这个 id 指定的文件后。
    3 在原“使用”文件的位置(例如新建文件的方法),增加临时文件 id 参数。将指定临时文件移动到永久存储区。

    客户端流程:
    1 打开一个文件。
    使用“新建临时文件”方法,传送第一块,得到文件 ID ;使用“附加临时文件”方法,并提交文件 ID ,传送后续的块;使用原业务方法,提交文件 ID ,完成从临时文件到永久存储区的转移和使用。
    humbass
        9
    humbass  
    OP
       121 天前
    to: @mx1700 @Ipsum
    说的是同一个事,待验证是否可以按区块在预先创建的文件上直接写数据

    to: @misoomang
    使用开源对象储存,也是一个不错的方式,之前没想到对象存储也有开源的,像这类东西有点大,没信心是否可以驾驭
    libaili
        10
    libaili  
       121 天前
    @humbass #3 可以部署 minio
    qdwang
        11
    qdwang  
       121 天前
    @Ipsum 正解
    rekulas
        12
    rekulas  
       121 天前   ❤️ 1
    移动文件应该很快,耗时应该是在合并上,增加了额外的 io 时间

    所以最佳方案应该就是预申请空间,然后不同的线程可以在不同的段进行 io 写入不会冲突(如果是单线程写入那就更不会了), 只需要验证分块 hash 正确最后的文件应该就没啥问题了
    renmu
        13
    renmu  
       121 天前 via Android
    建议检查代码的实现,理论上不会很耗时
    jiangzm
        14
    jiangzm  
       121 天前
    为啥一定要按分片存储呢, 直接将缓冲不断写入单文件不好吗? 还是说用缓存文件替代缓冲,不管是处理缓冲区(Buffer)还是缓存文件,其实都需要每收到一次请求及时做写入目标文件处理啊。
    humbass
        15
    humbass  
    OP
       121 天前
    @jiangzm 因为要断点续传,直接写入文件,如何告诉客户端从哪里开始上传?后端精确的获得当前文件大小,然后告诉前端从这个位置开始重新上传?
    Suaxi
        16
    Suaxi  
       121 天前 via Android
    领导允许用开源对象存储的话,可以参考一下 minio 的分片上传
    salparadise
        17
    salparadise  
       121 天前
    做过类似断点续传,用的 Oss 分片+合并
    01802
        18
    01802  
       121 天前 via Android
    用 syncthing 去传也行,可以自建
    guanzhangzhang
        19
    guanzhangzhang  
       121 天前
    创建大小文件,http range 和你 server 进程 seek 写就行
    linhua
        20
    linhua  
       121 天前
    @humbass #15 断点续传 和 断点下载是差不多的。一般的做法是 先建立一个 实际文件大小的 占位文件,还有一个 存储当前进度信息的 文本文件
    expy
        21
    expy  
       121 天前
    预分配一个完整文件,用分块序号和分块大小计算要写入的偏移量,前端上传前先查询,后端返回未上传的分块序号。
    listen2wind
        22
    listen2wind  
       121 天前
    @humbass #3 minio 是可以的,我们使用过,部署在内网的。
    jorneyr
        23
    jorneyr  
       121 天前
    4G 左右的文件计算 MD 需要 13+S ,这个耗时前后端都不可避免,需要验证文件的完整性是有必要的。

    至于后端小文件合并成大文件,可以使用一个线程合并,也可以使用多个线程分部分合并,然后继续往上合并。
    后端也可以使用文件内存映射直接写入文件中对应的 fragment ,不把收到的部分写入小文件。

    具体要分析是哪个部分慢,例如是计算 MD5 32 慢,看看是否改成 MD5 16 也能够满足需求。
    lerry
        24
    lerry  
       121 天前
    有轮子就直接用。
    minio 是 golang 实现的,很容易部署,我是用的 docker ,直接下载一个单文件应该也可以。
    TOUJOURSER
        25
    TOUJOURSER  
       121 天前   ❤️ 1
    正好前段时间遇到类似的需求,我们采用的是 tus + minio
    shenyansycn
        26
    shenyansycn  
       121 天前
    放硬盘里,人肉过去传输,最快。
    unknown404
        27
    unknown404  
       121 天前
    请使用支持 S3 的相关存储(公有云对象存储或者私有云 minio),建议 op 好好重读下 http 的 RFC ,https://datatracker.ietf.org/doc/html/rfc7233#section-4.2 有标准的协议不用,你去造轮子
    Rorysky
        28
    Rorysky  
       121 天前
    @MoHen9 你说的是多线程下载,和断点续传有什么关系?
    MoYi123
        29
    MoYi123  
       121 天前
    @unknown404 这是下载, 不是上传.
    xingjue
        30
    xingjue  
       121 天前
    minio
    v2tudnew
        31
    v2tudnew  
       121 天前
    去看看 BT 软件是如何实现就完事了,只是把一对多改成一对一。
    unknown404
        32
    unknown404  
       121 天前
    @MoYi123 原理一样的
    duglik
        33
    duglik  
       121 天前   ❤️ 1
    heiya
        34
    heiya  
       121 天前
    我的做法是:
    1.前端做好分片,为每一个分片生成序号,统计分片的个数。将这些数据传给后端,后端把这些数据记录下来,为这个文件生成一个全局 id ,返回给前端。
    2.前端每次上传分片会连带文件 id+分片 id 一起传过来。分片文件会被上传到一个临时目录。每上传完成一个分片,后端会记录下来分片 id (分片序号)。如果是并发上传的话,要注意已上传分片 id 集合会有线程安全问题,不然会出现某个分片已上传但没记录的问题。
    3.后端返回已上传分片集合。同时会有一个异步线程判断该文件 id 下的分片是否全都上传完毕。如果全都上传完成,调用文件系统 SDK 的合并文件方法(我用的是 minio ),合并完成之后,删除临时文件目录的分片。
    4.与此同时,前端全部分片上传完成之后,循环调用获取文件合并状态接口。
    完成~
    klo424
        35
    klo424  
       121 天前   ❤️ 1
    @humbass #15
    1. 前端生成一个文件 id ,连带文件 size 、offset 和二进制数据传给后端。
    2. 后端生成一个 size 大小的空文件,根据 id 找到文件,再根据 offset 去寻找位置写入二进制数据。
    3. 后端写完数据返回成功,前端 offset 更新继续传下一个二进制数据给后端,以此循环。
    4. 如果断了,下次打开页面时,从后端获取到未完成的文件 id ,再执行 1-3 过程。
    cstj0505
        36
    cstj0505  
       121 天前
    其实分片这东西完全是浏览器限制,不用 js,用个流式写入大文件直接边读边写
    yaodong0126
        37
    yaodong0126  
       121 天前
    好家伙,你们但凡看过断点续传的原理,就说不出什么转移合并的话
    trzzzz
        38
    trzzzz  
       121 天前   ❤️ 1
    @heiya 透传 minio 的 uploadId 给前端,前端每次上传分片就带上 uploadId 和 partNumber 。服务器收到一片也不用写本地,直接透传到 minio ,我习惯这样。
    但这样在弱网环境下会有问题,简单说个 流的 retry 需要自己写,这个是最麻烦的。如果是弱网还是建议先写 tmp 再异步上传
    trzzzz
        39
    trzzzz  
       121 天前
    @cstj0505 弱网环境或并发大场景下,流 reset 不好搞(最终还是需要缓存上传的数据)
    ZZ74
        40
    ZZ74  
       121 天前
    @klo424
    复议 这才是断点续传的最初或者真实的做法。
    OP 可以用这种方式避免重新组合文件。java 这边对应的 radomaccessfile....
    另外为啥要组合?如果服务器不需要读取处理啥的,只是提供存储,直接存分片,下载的时候由前端拼也可以。
    我不懂前端,你这 20G 的文件 前端下载时是否合并的了得打个问号
    jiangzm
        41
    jiangzm  
       121 天前
    @humbass #15 不需要告诉客户端怎么传,而是客户端怎么传告诉服务端。根据分片的大小,分片的索引可以计算 offset ,有 offset 不就可以直接写入了。 你最后上传完合并不也要确定分片的顺序吗, 在等待的过程完全可以边写边传。
    netnr
        42
    netnr  
       121 天前   ❤️ 2
    okakuyang
        43
    okakuyang  
       121 天前 via iPhone
    这些不都是基操吗,检查下自己代码,怕不是有 bug
    hetal
        44
    hetal  
       120 天前
    rsync 不行么?
    zhaokun
        45
    zhaokun  
       120 天前
    内网搭建一台开源的对象存储
    zhaokun
        46
    zhaokun  
       120 天前
    为啥要等都上传完了再合并,可以上传一点合并一点呀,顺序不对的就等着呗,合并完再移?不是直接在目标文件夹下直接合并吗?
    humbass
        47
    humbass  
    OP
       120 天前
    @netnr 这个倒是新东西,俺研究下看看,thx
    victimsss
        48
    victimsss  
       118 天前
    --- 因为是企业内网网络(含广义上的内网)使用,不能使用第三方的云端存储。
    第三方和内不内网有什么关系吗,贵司平时不是用支持私有部署的服务吗
    heiya
        49
    heiya  
       118 天前
    @trzzzz hi ,我还是没 get 到弱网环境下的问题。实际在上传时后端会维护一个 uploadId 下的已上传分片数组,客户端(我这边是 windows 客户端)会根据这个数组尝试重试机制。所以无论网络环境如何、是否是并发上传,只要客户端能正确处理这个数组就可以。
    trzzzz
        50
    trzzzz  
       117 天前
    @heiya 这个是发生在 server 端,server 端透传走客户端传的流(弱网是在 server 端 到 其它地方,比如对象存储)
    heiya
        51
    heiya  
       117 天前
    @trzzzz thanks~
    dode
        52
    dode  
       117 天前
    分块上传,并且要求按顺序上传,服务器顺序写入追加就行了
    dode
        53
    dode  
       117 天前
    客户端按固定大小分块上传,直至文件结束
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2878 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 12:57 · PVG 20:57 · LAX 04:57 · JFK 07:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.