大文件上传之慢启动上传策略实现动态分片🚙

796 阅读3分钟

最近在调研 大文件上传 这个需求,找资料的过程中发现了大圣在五年前写的一篇文章 字节跳动面试官,我也实现了大文件上传和断点续传,其中指出了一点:要根据当前网络情况,动态调整切片的大小,这个思路非常好,不过在大圣的这篇文章中,没有对其进行详细解释,且动态调整分片大小有些粗暴,本文对这个方案重新实现了一下,并进行了优化。

并行的动态分片本文暂不说明

为什么要动态分片

在很多关于大文件上传的文章资料中对怎么分片并没有解释说明,给我的感觉更像是只要把大文件分的越小,直接加并行来减少传输的时间消耗,但是如果网络情况良好的情况下,传输小文件会跑不满带宽,而且如果有很多用户同时上传文件,对服务器维护 TCP 的开销虽然理想情况下不大也是浪费,当然 TCP 也有可能出现异常,这里我们不详细展开;如果网络情况不好,那就更不用说了,如果用户的网络只能传输 20kb/s ,如果每一片都分 1M,传一块都需要将近 1 分钟了,如果用户不耐烦期间关闭了浏览器,那么这个块已经上传的部分已经白白浪费了🤡(永远不动的进度条)。

慢启动上传策略

这里我们简单提一下:TCP 的慢启动策略是为了解决网络拥塞导致的网络传输效率减低,丢包发生后的解决办法 。本文的大文件慢启动策略是为了解决 上行带宽跑不满/太小造成的资源浪费 的问题 以及再网络波动的场景下如何保证传输效率的问题。 下图是 TCP 的慢启动策略(来源 CSDN): tcp

一般来说,用户上传的速率取决于两个带宽:服务器的下行带宽 BWserverBW_{server}用户网络的上行带宽 BWuserBW_{user} ,换算公示为:

download=min(BWserver,BWuser)download=min(BW_{server},BW_{user})

一般服务器厂商提供的最小下行带宽 也就是 10mbpsupload_l2rt6f7gxgz06l1eutzzolovtdsnxvyy.png 但对个人用户办理的带宽,参考某运营商,非千兆网络的上行带宽为 30mbpsupload_w5pitnualw6cv2s908z3iab8ipb7qurt.png 所以,在当前场景下,用户最理想的上传速度为 10mbps,也就是 1.25mb/s。 此外用户是会通过进度条来感知当前文件上传的状态,我们结合一下 Response Times: The 3 Important Limits 这篇文章的 1S 原则,也就是每秒上传 1280kb 数据就是 ok 的。 不过,现实很残酷,不稳定的网络情况很难达成上述条件,所以我们延长了时间限制改成 5s,只要能在这段时间内上传,我们就认为ok,此外,默认的上传块大小设置为 1280 的 1/5256kb ,以此基础上进行提速。 综上,我们声明如下的几个变量:

chunkWindowSize:分块的窗口大小,默认256kbchunkWindowSize:分块的窗口大小,默认 256 kb
threshold:网络传输的阈值时间(发送+接受),5sthreshold:网络传输的阈值时间(发送+接受), 5s
ackNum:窗口的调整次数ackNum:窗口的调整次数

参考 TCP ,整个分片的调整由一个分段函数进行调整,我们当上传时间在 thresholdthreshold34\frac{3}{4} 下,属于爆炸式增长阶段,函数表达为:

chunkWindowSize=chunkWindowSizeprevious+(ackNum2256)chunkWindowSize=chunkWindowSize_{previous}+(ackNum^{2} * 256 )

一旦时间大于 thresholdthreshold34\frac{3}{4} 时,就不能过分增长了,也进入了 拥塞避免 状态,这时候函数表达式为:

chunkWindowSize=chunkWindowSizeprevious+256chunkWindowSize=chunkWindowSize_{previous}+256

如果这时候上传的时间超过了 5s,那么下一次分片整个 chunkWindowSizechunkWindowSize 要减少原来的 13\frac{1}{3} ,之后重新进入慢启动状态。

核心代码如下: upload_shpnvku5yjp5vx1ek7n9dwabf0dddz9a.png

模拟测试

我们模拟三种情况:以上传 1G 文件为例,看看每次分片上传的大小有什么变化

  1. 超级理想网速,不限网络速度
  2. 理想网速,最大上传速度12m/s
  3. 超级复杂网速,网速随心情变动 upload_36j0kjyhuu4ld7qmcrzoroxabt1g1uhc.png

模拟过程upload_tqf4gted2u2er2nper6a873x24z6rqhp.gif

第一种情况upload_qsvr4zzzn28paa2lws8ox1z2iv7x6baz.png

第二种情况upload_fzbrbp9pmdz8empd99tjejjbb5c5tlxy.png

第三种情况upload_x9ohbc349k3w3hdwhobmlfjyqx764pey.png

上述代码可以在 这里 找到,大文件上传的功能还在实现中...,这个方案也有可能会随时变动🤡


更新

模拟代码写的有问题,针对第二种情况,没考虑 3/4 的时间范围,此处进行更新调整 调整后的模拟代码为:

  public async stable_network_request(chunk_size: number) {
    const max_chunk_size = fileSize.mbToBytes(60);
    if (chunk_size >= max_chunk_size) {
      return new Promise((resolve) => {
        setTimeout(() => resolve(true), 6000);
      });
    } else {
      if (chunk_size >= (max_chunk_size * 3) / 4) {
        return new Promise((resolve) => {
          setTimeout(() => resolve(true), 3800);
        });
      }
      return new Promise((resolve) => {
        setTimeout(() => resolve(true), 1000);
      });
    }
  }

模拟结果如下:

upload_fuu80fx244igmlwmcnxkbbb9p1bqs3ai.png

不过在这个模拟实例中,最终分片也没超过 60m 😓