← 中文资源

简体中文版本

如何分块上传支持续传

将大文件分块并带偏移上传,中断传输从最后确认的块恢复而非重启。

阅读英文原文

概述

本文讲解如何用 chunked upload 解决 resume interrupted uploads 问题。核心思路是:在数据进入业务逻辑之前,用显式约束拒绝不合法的输入,让正常路径保持快速,让失败路径保持可见。

关键设计

Choose a chunk size 是第一个决策,它决定了后续 chunked upload 管道能假设什么。错误的选择会在高负载下暴露为 resume interrupted uploads 问题,你以为 chunked upload 保证的不变量不再成立。在承诺前先测量影响,因为错误选择的代价是事故而非代码评审。

Slice the file 的机制在 chunked upload 中描述简单,但后果会扩散。一个错误的校验会在负载下变成数据问题,你原以为 chunked upload 保证的不变量不再成立。

实现要点

代码先用 chunked upload 定义约束,再在边界处调用校验。校验失败时收集所有错误路径,让用户一次看到全部问题,而不是逐个修复。测试用假时钟处理时间逻辑,用桩注入失败路径,断言用户可见的结果。

Upload with offset 是大多数 chunked upload 实现泄漏性能的地方。一个看起来健康的平均值可能隐藏一个毁掉最慢百分之十用户体验的尾延迟。测量尾延迟,在尾延迟上告警,优化尾延迟,因为平均值会撒谎而尾延迟说出真相。

常见陷阱

  • 跳过 chunked upload 边界检查,让 resume interrupted uploads 未校验即进入系统。
  • 假设 chunked upload 很快,却不测量尾延迟。
  • 让 resume interrupted uploads 静默失败,而非带指标地显式失败。
  • 只测试正常路径,导致 chunked upload 失败在生产中暴露。
  • 跨请求共享可变状态,一个坏输入毒化下一个。

验证方式

编写测试驱动 chunked upload 到失败条件,断言系统优雅降级而非崩溃。用 node:test 或 vitest 运行,检查校验报告每个违规路径,且正常路径不被校验开销拖慢。这样 resume interrupted uploads 就是你维护的属性,而非你期望的功能。

迁移与演进

当契约需要变更时,采用扩展-收缩策略:先发布兼容新旧两种形状的版本,待所有生产者都迁移到新形状后,再发布拒绝旧形状的版本。chunked upload 的版本化能力使这一过程可控,避免破坏性变更导致的生产事故。

Track acknowledged chunks 的难点不在正常路径,而在 chunked upload 失败一半时会发生什么。枚举失败模式,为每种情况决定用户看到什么。提前决定的失败是降级,在生产中发现的失败是事故。用混沌测试注入你担心的故障,是在生产前发现接缝的最廉价方式。

可观测性

chunked upload 的校验决策应当无需调试器即可观测。当 chunked upload 出错时,运维应看到包含被拒绝输入、拒绝规则和所做决定的结构化日志。静默失败的 chunked upload 是无法运维的。在决策点添加结构化日志,在拒绝点添加指标,在整个操作周围添加追踪跨度,使“为什么失败”在仪表板中有答案而非在重新部署中。

衡量 chunked upload 的指标应是用户感受到的——尾延迟、错误率,而非易于绘图的平均值。看起来健康的平均值可能隐藏毁掉最慢百分之十用户体验的尾延迟。在尾延迟上告警,优化尾延迟,因为平均值撒谎而尾延迟说真话。

代码示例

async function uploadChunked(file, url) {
  const SIZE = 5 * 1024 * 1024;
  for (let off = 0; off < file.size; off += SIZE) {
    const chunk = file.slice(off, off + SIZE);
    await fetch(url, { method: 'POST', headers: { 'X-Offset': off }, body: chunk });
  }
}
test('sends correct offsets', async () => {
  await uploadChunked(file, url);
  expect(fetch).toHaveBeenCalledWith(url, expect.objectContaining({ headers: { 'X-Offset': 0 } }));
});