最近在调研 大文件上传 这个需求,找资料的过程中发现了大圣在五年前写的一篇文章 字节跳动面试官,我也实现了大文件上传和断点续传,其中指出了一点:要根据当前网络情况,动态调整切片的大小,这个思路非常好,不过在大圣的这篇文章中,没有对其进行详细解释,且动态调整分片大小有些粗暴,本文对这个方案重新实现了一下,并进行了优化。
并行的动态分片本文暂不说明
为什么要动态分片
在很多关于大文件上传的文章资料中对怎么分片并没有解释说明,给我的感觉更像是只要把大文件分的越小,直接加并行来减少传输的时间消耗,但是如果网络情况良好的情况下,传输小文件会跑不满带宽,而且如果有很多用户同时上传文件,对服务器维护 TCP 的开销虽然理想情况下不大也是浪费,当然 TCP 也有可能出现异常,这里我们不详细展开;如果网络情况不好,那就更不用说了,如果用户的网络只能传输 20kb/s ,如果每一片都分 1M,传一块都需要将近 1 分钟了,如果用户不耐烦期间关闭了浏览器,那么这个块已经上传的部分已经白白浪费了🤡(永远不动的进度条)。
慢启动上传策略
这里我们简单提一下:TCP 的慢启动策略是为了解决网络拥塞导致的网络传输效率减低,丢包发生后的解决办法 。本文的大文件慢启动策略是为了解决 上行带宽跑不满/太小造成的资源浪费 的问题 以及再网络波动的场景下如何保证传输效率的问题。
下图是 TCP 的慢启动策略(来源 CSDN):
一般来说,用户上传的速率取决于两个带宽:服务器的下行带宽 和 用户网络的上行带宽 ,换算公示为:
一般服务器厂商提供的最小下行带宽 也就是 10mbps。
但对个人用户办理的带宽,参考某运营商,非千兆网络的上行带宽为 30mbps。
所以,在当前场景下,用户最理想的上传速度为 10mbps,也就是 1.25mb/s。
此外用户是会通过进度条来感知当前文件上传的状态,我们结合一下 Response Times: The 3 Important Limits 这篇文章的 1S 原则,也就是每秒上传 1280kb 数据就是 ok 的。
不过,现实很残酷,不稳定的网络情况很难达成上述条件,所以我们延长了时间限制改成 5s,只要能在这段时间内上传,我们就认为ok,此外,默认的上传块大小设置为 1280 的 1/5 及 256kb ,以此基础上进行提速。
综上,我们声明如下的几个变量:
参考 TCP ,整个分片的调整由一个分段函数进行调整,我们当上传时间在 的 下,属于爆炸式增长阶段,函数表达为:
一旦时间大于 的 时,就不能过分增长了,也进入了 拥塞避免 状态,这时候函数表达式为:
如果这时候上传的时间超过了 5s,那么下一次分片整个 要减少原来的 ,之后重新进入慢启动状态。
核心代码如下:
模拟测试
我们模拟三种情况:以上传 1G 文件为例,看看每次分片上传的大小有什么变化
- 超级理想网速,不限网络速度
- 理想网速,最大上传速度12m/s
- 超级复杂网速,网速随心情变动
模拟过程:
第一种情况:
第二种情况:
第三种情况:
上述代码可以在 这里 找到,大文件上传的功能还在实现中...,这个方案也有可能会随时变动🤡
更新
模拟代码写的有问题,针对第二种情况,没考虑 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);
});
}
}
模拟结果如下:
不过在这个模拟实例中,最终分片也没超过 60m 😓