ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大文件上传稳定性实战:WebUploader分片上传与断点续传及TS流对齐

大文件上传稳定性实战:WebUploader分片上传与断点续传及TS流对齐 银行系统的视频监控文件回传一直是个让人头疼的环节。监控点位分散在各网点单文件动辄几百 MB 到几个 GB链路还要过网闸、跨地域专线链路质量稍差就直接超时。更麻烦的是监控视频绝大多数是 TS 流格式对字节边界极其敏感分片切得不对合并出来的文件要么花屏要么压根没法播放。我们部门以前用原生 XMLHttpRequest 硬传文件一大基本靠运气日志里隔三差五就是 Connection reset、Request timed out。后来把上传能力整体切到百度 WebUploader 组件利用它的浏览器端分片能力配合服务端做断点续传和完整性校验回传成功率从 70% 多拉到了 99.5% 以上。这篇内容我会把这套方案的选型理由、分片参数调优、Chrome 90 等浏览器兼容性处理、TS 分片边界对齐、服务端协同以及生产环境踩过的坑全部展开讲一遍。不管你是正在做银行监控回传还是在做其他大文件上传场景里面的配置项和经验都能直接抄走。1. 银行监控视频上传暴露的不只是“大文件”这一个问题银行项目里的监控视频文件天然带着几道枷锁。监控点位分布在各个网点视频产生后先汇聚到网点前置机再由业务系统回传到数据中心。这个链路里既有局域网也有跨区域专线不同网点的出口带宽、链路稳定性差异巨大。有些网点链路质量差到令人发指传一个 2GB 的文件中途断个七八次都是常事。如果上传方案本身没有针对这种网络环境做设计运维同事就会被折腾得天天接网点电话。更麻烦的是视频文件的格式。监控视频基本都是 TS 流MPEG-2 Transport Stream数据按 188 字节的定长 packet 组织内部还嵌套着 PAT/PMT 表和关键帧信息。这种格式对字节位置极其敏感浏览器端如果只是简单按固定大小把文件切开合并时又没有检查边界最终得到的文件就可能在播放时出现无法解析、花屏、拖动卡顿等问题。普通文档上传测试根本测不出这类问题必须用真实监控视频才能复现这也是很多团队容易在验收阶段栽跟头的地方。这套稳定方案解决的核心痛点有三个一是单文件太大整体上传容易超时服务端接收超时断开后整个传输就报废了二是网络中途断线后传统方案必须从头重传浪费带宽也浪费时间三是服务端无法对“半截文件”做校验数据完整性得不到保证。WebUploader 的分片机制恰好覆盖了这三点它会在内部把二进制文件按字节偏移切成多个小块为每个分片打上序号服务端按序合并也可以在任一分片失败时单独补传不用整个文件重来。2. 方案选型为什么是 WebUploader 而不是原生 H5 上传2.1 原生 XMLHttpRequest 直传的硬伤先回头看我们为什么放弃原生直传。XMLHttpRequest 加 FormData 的方式代码确实简单一个 POST 请求把整个文件塞进去就行但问题非常集中。单请求承载整个文件服务端接收超时就会断开连接前端拿到报错后没有任何补偿手段。断线之后没有续传逻辑一切归零。浏览器和网关对单请求体大小、时长都有隐式限制跨网闸链路尤其明显。多个文件同时传时并发控制还要自己实现排队的优先级、失败后的重试策略都要从头写。我整理过一张对比表可以更直观地看出两种方案的能力差异能力维度原生 XHR 直传WebUploader 分片上传大文件支持不稳定依赖环境分片后单请求体积可控断点续传不具备配合服务端可以做到进度反馈有但粒度粗分片级加文件级双进度失败重试只能手动重传整个文件分片级自动重传并发控制自行实现内置队列调度机制这里面的核心区别在于“失败成本”。直传模式下任何一次网络抖动都可能让整个文件的传输作废重试的开销是 GB 级的分片模式下单片失败只损失一个 2MB 或 5MB 的小块重传代价可以忽略不计。银行网点的上行链路普遍不稳定稳定性的本质其实就是把失败成本不断拆小拆到少量链路抖动不再影响整体任务完成为止。2.2 WebUploader 的分片模型与事件机制WebUploader 对上传稳定性的贡献来自它对整个上传生命周期的模型化抽象。文件进入队列后组件根据 chunkSize 配置把文件切成多个 Blob每个 Blob 是一个独立的上传单位组件内部维护一个任务队列同一时间最多只有 threads 个请求在跑每个分片请求都有独立的进度回调组件再把所有分片进度汇总成文件级进度。这套模型解决的不只是“传输”本身还有“调度”这是手工实现很容易出bug的部分。我实际项目里最常用的几个事件是 beforeFileQueued、fileProgress、chunkUploaded、fileUploaded、error。其中 chunkUploaded 事件非常关键它在每一个分片上传完成后触发可以在里面把服务端返回的接收校验信息和本地记录进行比对。这个钩子是我们后来实现“零丢失上传”的关键位置。如果没有这个事件分片是否真正落盘就只能靠猜断点续传的准确性也会打折扣。2.3 为什么在银行私有化环境里敢选它选择 WebUploader 有三个原因。第一它的运行环境完全在浏览器端不依赖外部 CDN压缩包丢到静态资源目录就能用对银行这种要求资产包自管理的环境天然合适。第二它对老版本浏览器有明确的适配路径虽然 Flash 兜底模式在 Chrome 90 里已经不可用但只要 H5 模式配置正确浏览器兼容性是有保证的。第三它的 API 面小事件驱动模型简单出了问题团队能快速定位不像一些企业级上传框架那样黑盒化出了问题根本不知道从哪查起。我也对比过 vue-simple-uploader、uppy 等组件。最终没有选它们并不是说这些组件不好而是银行项目的选型原则是低风险、少依赖、易审计WebUploader 在这些维度上的综合得分最高团队也有多年使用经验。在技术选型这件事上稳定性和可控性往往比功能丰富重要得多尤其是一个要跑在生产环境、还要长期维护的上传组件。3. 影响稳定性的三个关键参数怎么调3.1 分片大小默认 512KB 不适合视频文件WebUploader 默认的 chunkSize 是 512KB这个值适合常规文档和小文件但对 GB 级监控视频并不合适。分片太小分片总数会爆炸服务端合并时文件系统调用次数剧增整体 IO 开销非常大分片太大单片失败时重传的惩罚成本变高断点续传的优势就被稀释了。我们拿一个 2GB 的真实 TS 视频做过对比测试结果很有参考价值。512KB 分片约 4096 片服务端合并阶段明显变慢整体耗时比 2MB 分片慢约 20%。2MB 分片约 1024 片在网关链路质量一般的网点单片失败率中等重传开销可控。5MB 分片约 410 片单片传输时间适中即使失败重传代价也只有 5MB 流量。10MB 以上单片成功后吞吐最大但弱网环境下单片失败率显著上升用户体验反而变差。最终我们默认值定在 5MB并由后端按网点上下发配置链路质量差的网点自动切到 2MB。分片大小的选择不能只从传输效率一个维度出发要综合“失败惩罚、重传代价、服务端合并开销”三重因素来平衡。视频文件尤其不能往小设因为 TS 流的对齐本身要求一定粒度分片次数过多还会放大边界校验的开销。3.2 并发线程数稳定性的隐形杀手WebUploader 的 threads 参数控制同时上传的分片数量。我见过有同事一上来就设 10理由是传得快但实测在银行跨网闸链路上线程并发超过 5 之后带宽并没有涨反而因为浏览器和网闸之间 TCP 连接数过多触发限流或丢包整体速度打折失败率上升。线程数不是越大越好它和网络环境之间存在一个平衡点过了这个点收益曲线掉头向下。我们根据网点的实际链路情况整理了一张推荐表网络特征推荐 threads说明内网千兆5可吃满带宽失败率低跨网闸专线3避免连接过多被限流弱网链路2成功率为第一优先级另一个容易被忽视的点是 threads 和 chunkSize 的组合效应。总吞吐约等于 threads 乘以单分片带宽在链路质量一般的场景宁可用“小分片加中等并发”也不要“大分片加高并发”。大分片加高并发一旦触发链路限流所有分片同时卡住重试风暴马上就来。我们在一次参数调整中把 threads 从 8 降回 3成功率提升了 5 个百分点这就是活生生的教训。3.3 超时与重试自动重试必须有指数退避WebUploader 在默认配置下分片请求失败后会抛出 error 事件但不会自动重传。银行场景里网络抖动是常态让用户手动点重传根本不现实所以我在这层封装了一个重试队列。每个失败分片自动重传最多重试 3 次重试间隔按指数退避第 1 次失败等 2 秒第 2 次等 4 秒第 3 次等 8 秒。连续失败超过 6 次的暂停整个上传队列提示操作员检查网络。指数退避的核心作用是防止重试风暴如果没有退避间隔重试请求会在网络恢复前连续撞击链路把原本就脆弱的路由彻底堵死。超时设置同样要细心。WebUploader 依赖 jQuery 的 ajax 配置我们把 timeout 设为 60 秒超过这个时间主动 abort让请求进入重试逻辑。timeout 如果太短比如设成 30 秒在跨网闸传输 5MB 分片时很容易误判因为慢链路下单片的实际传输时间可能确实超过 30 秒。误判引发的重试如果追不上网络恢复速度就会陷入“永远在重试”的死循环日志里全是重复错误用户感知就是传了一天也没传完。4. 浏览器端兼容性工程Chrome 90 和 TS 分片的两个大坑4.1 银行终端的 Chrome 90 环境需要注意什么银行办公终端的浏览器版本普遍滞后我们部门大量终端锁在 Chrome 90既不允许自动升级也不允许回退。Chrome 90 这种版本对于 WebUploader 而言有两点和其他浏览器版本不同。第一Flash 上传路径在 Chrome 84 之后逐步被封禁到 90 版本已经彻底不能作为兜底第二V8 引擎对 Blob 内存的回收策略和其他更新版本有差异GB 级大文件长时间上传时内存占用会持续累积而且回收效率明显不如 Chromium 的新版本。这两个差异在我们项目里引发过两个实际问题。一个是旧版 WebUploader 0.1.5 在 Chrome 90 上有概率出现分片索引错位chunk 序号和实际 Blob 内容对不上一个是连续传完两三个 GB 级文件后页面内存占用直冲上限卡到无法操作。这两个问题单独拿出来都能解决但叠加在 Chrome 90 环境里排查难度就上了一个台阶。后来我们升级到 WebUploader 2.x 的稳定版把并发 threads 降到 3 到 5 之间并在每个分片请求的 formData 里显式带上 start、end 字节偏移才彻底把错位问题压住。Chrome 90 的 Blob 切分还有一个记忆深刻的细节。Blob 对象在切分文件时并不是复制文件内容而是创建对底层文件字节范围的引用。这意味着大量分片对象会一直持有原文件的字节范围引用即使上传完成GC 也不一定能立即回收。解决办法只能是“用完即弃”每个分片成功后释放对应引用整个文件传完后调用 uploader.reset()业务层实现“一个文件一个组件实例”传完就销毁重建不让组件复用。4.2 TS 视频分片为什么必须做边界对齐监控视频使用 TS 流封装数据按 188 字节定长 packet 组织内部还嵌套着 PAT/PMT 表和 IDR 关键帧。WebUploader 只按字节切文件完全不会理解 TS 包的内部结构所以我们必须自己保证“切出来的分片恰好落在完整 TS 包的起始位置”。理想的分片边界有两个要求分片起始位置是 188 的整数倍如果条件允许尽量让分片从 IDR 关键帧开始方便服务端做视频索引。如果边界对齐没做好会发生什么直接表象是合并文件能播放但拖动进度条时卡顿明显严重时播放器直接报 “No data available” 或花屏。银行监控文件是留证材料通常要保存三个月以上这种数据一旦出问题影响就不是一个网点的事了。我们的做法是在文件预处理阶段用 FileReader 读取分片起始位置的几个字节检查是否为 TS 同步字 0x47不是的话就向前回退到最近一个 0x47 的位置把该分片的实际起始偏移记录下来和分片数据一起传给服务端。服务端合并时严格按照“分片序号加字节偏移加数据长度”来组装并校验偏移的连续性。这里我要强调一下WebUploader 默认只传 chunk 和 chunks 两个参数不会告诉服务端每个分片在原文件里的字节范围所以自定义 formData 里的 start、end 字段必须自己加。这个设计帮我们拦截了好几次“高分片成功率掩盖的低级丢片”问题。4.3 一个必须处理的隐性坑重复上传与文件 MD5浏览器端分片稳定还有一个容易忽略的方向重复上传。操作员看到页面卡顿下意识又点了一次上传队列里就会出现两个相同的文件服务端收到两套相同分片如果没有去重逻辑合并时就会出现分片内容互相打架的情况。WebUploader 的 duplicate 参数默认允许重复文件进入队列我建议在业务里显式设置 duplicate: false同时在上传前用 spark-md5 对文件计算 MD5得到 fileMd5 后传到服务端。服务端发现相同 MD5 的文件已有完成记录直接返回“文件已存在”前端弹出提示不再触发二次传输。MD5 计算对 GB 级文件确实有性能开销完整读一遍文件可能要几十秒但这个开销换来的稳定性是值得的。而且把 fileMd5 放在 formData 里每个分片请求都会带上同一个值服务端可以据此做分片归属校验避免两个文件的分片互相串扰。我们线上出过一起“串片”事故根源就是两个文件并发上传时分片被写进了同一个上传任务目录加了 fileMd5 和 taskId 双重校验后这个问题彻底消失。5. 稳定的另一半服务端协同与断点续传闭环5.1 四个基础接口把分片和合并规范化前端调得再稳没有服务端的配合稳定性也只是一句空话。我们围绕分片上传在服务端抽象了四个接口全部基于常规的 Java Web 框架就能实现不需要引入复杂中间件。第一个是 init 接口接收 fileMd5、fileName、fileSize、chunkSize 参数返回一个 uploadId 和已接收的分片列表第二个是 chunk 接口接收 uploadId、chunkIndex、start、end 以及分片数据完成单片的接收和落盘第三个是 complete 接口前端确认所有分片发送完毕后触发服务端做完整性校验和文件合并第四个是 status 接口用于断点续传时查询接收进度。分片临时文件的命名规则是 uploadId 加 chunkIndex 加 start 加 end 的组合后缀是 .part。合并时按 chunkIndex 排序逐个二进制追加。追加之前必须先检查 start 偏移是否连续任何一个缺口都可能导致最终视频花屏。合并完成后服务端要重新计算整个文件的 MD5和前端传入的 fileMd5 比对一致才向业务系统返回成功。同时把合并时间、操作人、文件大小、MD5、分片数量全部写入审计表。银行系统必须留痕这块虽然是额外的开发成本但绝不能省。5.2 断点续传的完整时序断点续传是整个稳定性方案里体验提升最明显的部分。刷新页面或进程中断后用户重新进入上传页面前端从 localStorage 恢复任务列表调用 status 接口拿到服务端已接收的分片索引集合然后只补传缺失的分片。整体的时序可以拆成四步第一步前端读取本地缓存的上传任务拿到文件的基本信息第二步调 init 接口服务端返回 uploadId 和已完成的分片列表第三步前端通过 uploader.option(formData) 注入 uploadId 和 skip 列表把组件状态切到续传模式第四步队列启动后每一个分片请求都带上当前 chunkIndex服务端发现该片已存在就直接返回成功前端跳过其他分片照常传输。这里有个关键细节断点续传时不能用组件的自动模式因为组件默认会把所有分片从头到尾传一遍。我们的做法是在 beforeFileQueued 或 fileQueued 事件里动态注入 skipChunks 参数服务端在 chunk 接口里判断当前 chunkIndex 是否已接收已存在的分片直接返回“已接收”状态前端也就不会再传这个分片的字节。用这种方式续传流量只会集中在缺失分片上几乎不浪费带宽。这套机制上线后网点上行链路抖动导致的失败从“整个文件重传”变成“只补最后几个分片”回传成功率稳定在 99% 以上。5.3 配置示例和一把关键参数把前面讲的内容落到配置上大概就是下面这个样子的。前端初始化 WebUploader 的配置核心参数是 chunked、chunkSize、threads、duplicate 这几个var uploader WebUploader.create({ swf: /resources/Uploader.swf, server: /api/upload/chunk, pick: #picker, accept: { title: TS Video, extensions: ts, mimeTypes: video/mp2t }, chunked: true, chunkSize: 5 * 1024 * 1024, threads: 3, duplicate: false, auto: false, formData: { fileMd5: , uploadId: }, fileVal: file });文件进入队列时计算 MD5 并注入 formDatauploader.on(beforeFileQueued, function (file) { var fileMd5 computeFileMd5(file); uploader.option(formData.fileMd5, fileMd5); });NGINX 侧需要放开上传体积限制和超时时间否则分片请求也会被代理层挡下来client_max_body_size 10m; proxy_http_timeout 300s; proxy_send_timeout 300s;这些参数在银行场景下的价值最终体现为“可排查、可审计、可复现”。尤其可复现这一点很关键生产环境出了问题能够按相同步骤还原场景才能谈得上后续的优化和整改。6. 生产环境常见问题与排查实录6.1 “传完花屏”的典型案例与定位过程我们线上出过一次典型的“传完花屏”问题。运维反馈某网点上传的监控视频有 1 秒多的花屏用户换了几台机器重传都一样基本排除了播放器和终端的问题。我先让后端把所有分片的 start、end 记录打印出来比对发现两片 start 相同但字节内容不同说明同一时间有两个不同文件的同名分片混进了同一个 uploadId。进一步排查是操作员先后拖了两个文件进队列两个文件的分片在网络层并发交错恰好写入了同一个 uploadId 的临时目录。这个问题的根因不在 WebUploader 组件本身而在业务层没有做上传任务互斥。我们随后加了全局锁队列里已有任务时新任务只能等待不能并发。排查途中最有价值的动作其实是把服务端落盘日志和前端 formData 日志对齐通过对比上传时的 taskId 和 fileMd5 定位到“是谁的数据写进了谁的任务”。这次排障让我意识到浏览器端分片稳定性问题中很大一部分来自任务编排而不是传输链路组件只提供能力业务流程的边界还得自己守。6.2 内存飙升问题的定位与组件销毁策略连续传完三个 GB 级文件后页面卡到无法交互。这个问题在 Chrome 90 环境里尤为明显因为 Blob 的对象引用机制和 V8 的回收策略共同作用内存只增不减。用 DevTools 的 Memory 面板拍了两张 heap snapshot 做对比发现 WebUploader.File 对象和 Blob 对象各占了几百 MB内存迟迟不释放。解决方案分三层第一每个分片成功后显式释放本地 Blob 引用第二整个文件完成后调用 uploader.reset()清空内部队列第三每个文件独立使用一个组件实例传完就销毁重建不跨文件复用。这套策略在 Chrome 90 上效果尤其明显因为我们实测过相同逻辑在新版 Chrome 上内存回收要快得多而在 90 版本里只有主动销毁才靠得住。6.3 NGINX 的 413 问题分片也被坑分片请求按理说体积不大但我们差点被 NGINX 的默认配置坑了。client_max_body_size 默认值是 1MB当分片大小 5MB 加上 multipart 编码膨胀之后请求体很容易超过 1MBNGINX 直接返回 413前端没做对应处理时现象就是“传一下就断了”而且日志里看不到明显的报错因为 413 被前端误读成了通用失败。我们后来把 client_max_body_size 调到 10MBproxy_http_timeout 调到 300 秒这个问题才彻底解决。这也是一个提醒分片不代表一定不会触发代理层的体积限制设置代理层时要按“单片大小加编码膨胀加一定冗余”来计算不能想当然觉得分片之后请求体就肯定很小。我们在排查这个问题时还发现个别网点走的代理链路上还有一层自定义网关这层网关的超时设置也需要一并检查否则 NGINX 放行了网关又给断了。6.4 常见问题速查表最后把生产环境里遇到过的问题和对应解法整理成一张速查表方便以后排查时直接对照现象根因解决方案上传到一半 413NGINX client_max_body_size 过小调到 10MB 以上视频局部花屏TS packet 边界错位分片起始偏移对齐 0x47合并后无法解码缺分片未被发现服务端按 start/end 校验连续性页面越来越卡Blob 引用未释放传完 reset 并销毁组件实例重复文件重复上传duplicate 默认 true设 false 并加 MD5 去重重试死循环timeout 太短且无退避60 秒超时加指数退避分片串扰队列并发未做互斥业务层全局锁单任务Chrome 90 分片错位旧版组件加高并发升级组件并降 threads续传后又从头传没有传 skip 分片列表status 接口动态注入已接收索引7. 一点亲历的体会这套方案刚上线时我以为最大的挑战会来自服务端合并性能或者前端兼容性但真正做完才发现稳定性问题大多不是单一因素造成的而是“前端参数、服务端校验、业务编排、浏览器环境”四条线同时作用的结果。每一个环节单独看都说得过去合在一起就暴露出各种边角问题。比如分片大小和并发线程之间、超时设置和重试策略之间、Blob 内存和组件生命周期之间都有隐藏的联动关系只调一个参数往往会按下葫芦浮起瓢。我个人的操作习惯是任何参数改动都要在真实监控视频上跑一遍“断网-恢复-续传”用例然后对比服务端记录的 start/end 偏移日志和文件 MD5确认没有边界问题才放行到生产。这个验证流程帮我拦下了至少两轮“测试通过但线上花屏”的回归事故。测试用的样本也很有讲究不能只拿一个短片反复测要覆盖不同码率、不同时长的真实录像才能把边界情况暴露出来。另外还想说一句WebUploader 本身是个老组件了但它所代表的“浏览器端分片上传”思路并不过时。在银行这种对稳定性和可控性要求极高的环境里把分片粒度调适中、并发数调克制、重试加上退避、TS 边界对齐、服务端校验完整、断线可续传、内存释放干净这几件事组合起来才叫真正的稳定性工程。工具只是顺手的一环真正稳住的是围绕它设计的工程规范。
返回列表