拓冰建站拓冰建站
首页 / 资讯中心 / 正文

大文件上传实战:分片上传、断点续传与秒传方案详解

1. 方案背景为什么大文件上传不能“一把梭”但凡做过文件上传功能的同学早晚都会遇到这样一个场景用户往浏览器里拖了一个几个GB的监控视频、一份设计源文件或者一套完整的数据库备份然后点击上传。如果这时候你还在用传统的input typefile配合 FormData 一次性提交前端大概率会直接卡死后端接收请求时内存像漏了一样往下掉网络只要稍微抖动一下整个上传就前功尽弃用户只能咬着牙重新选文件再传一遍。我在实际项目里接手过类似需求一个团队内部的知识管理平台允许用户上传最大 5GB 的培训录像。第一版就是无脑 POST 整体上传结果上线第一周就收获了大量投诉核心问题集中在三块一是请求体太大Nginx 直接返回 413二是服务端接收流时有超时限制上传到一半连接就被断开三是没有任何进度恢复能力一次失败等于白干。这些东西单拎出来任何一个都很致命但合在一起就是大文件上传场景里绕不开的“拦路虎”。要解决它们业界基本已经形成了一套标配组合拳分片上传解决体积和并发问题断点续传解决网络中断和失败重试问题秒传解决重复文件浪费带宽和存储的问题。这篇文章会把这套方案从原理到落地完整拆开讲清楚每一步为什么这么做以及实际开发中那些文档里不会写的坑。2. 整体设计思路先把三个概念的边界划清楚2.1 分片、断点、秒传分别解决什么问题很多文章把这三个概念混在一起讲读者看完感觉是同一个东西但实际它们各自解决的痛点完全不同。分片上传的核心思路是“化整为零”。把一个 2GB 的文件切成比如 4MB 一片一共 512 片然后一片一片地传。这样做的好处非常直接单个请求的体积从 2GB 降到了 4MB服务器不需要为一个大请求预留连续大内存Nginx 的client_max_body_size也能放心地保持在一个合理范围不会因为要放开大文件限制而把服务器暴露在其他风险中。同时多片可以并发上传总体吞吐量反而比单连接上传更高。断点续传的核心思路是“记录进度失败续传”。已经传成功的分片我用某种方式记录下来下次网络恢复了只传没传成功的那些而不是从头再来。这就像看书折了个角不用把前面读过的内容再翻一遍。断点续传依赖分片上传因为它只能做到“片”这个粒度的续传而不可能实时续传到某个字节位置——那样服务端的处理逻辑会复杂到不现实。秒传的核心思路是“数据去重”。当服务端发现你这次要上传的文件在服务器上已经存在一份一模一样的时候就直接返回一个“上传成功”的结果客户端根本不需要真的传数据。它的前提是先对文件内容做指纹计算常见的就是 MD5 或者 SHA-1然后拿着指纹去服务端比对。2.2 为什么分片大小的选择是需要“算”的分片大小这个参数看起来像是拍脑袋定的实际上它直接影响上传的效率和稳定性。我在不同项目中试过 2MB、4MB、8MB、16MB 四种分片这里把权衡逻辑展开说一下。分片太小比如 1MB意味着一个 2GB 文件要切成 2048 个分片。每个分片都会触发一次 HTTP 请求意味着要做 2048 次网络握手如果走 HTTPS 还有 TLS 握手。请求数量上去了服务端的接口调用开销、日志量、数据库记录数都会跟着线性增长而且太小的分片在慢速网络下吞吐量上不去——因为每次请求的固定开销比如请求头、鉴权占比太高了。分片太大比如 50MB就回到了近似整体上传的老路上单个请求传输时间长中途失败的概率大而且并发上传时服务端内存压力会明显上升。一个 50MB 的分片在并发 5 路的情况下意味着服务端最多同时要缓冲 250MB 的数据如果线上机器内存不多直接触发 GC 频繁或者 OOM。从实际工程经验来看4MB 到 8MB 是性价比最高的区间。我自己的项目里通常默认 5MB原因有两个一是 5MB 能保证单个分片在一个 HTTP 请求的合理时间窗内完成传输二是对于 2GB 文件正好分成 400 个左右的片不管是并发调度还是失败重试规模都是可控的。如果你服务的文件大小跨度很大从几 MB 到几十 GB还可以做成动态分片文件小于 100MB 用 2MB 分片大于 1GB 用 10MB 分片通过一个配置接口让前端先查询后再决定切片参数。2.3 整体上传流程的设计蓝图这里把整条链路的流程说清楚后续实现就按照这个骨架来。第一步前端拿到文件后先计算文件的唯一指纹比如 MD5。如果文件很大计算 MD5 本身可能要几秒钟这是后续秒传和断点续传的基础省不掉。第二步拿着指纹去请求服务端的一个预检接口询问三个信息这个文件是不是已经存在秒传如果不存在这个文件之前是否上传过一部分上传了哪些分片断点续传。第三步根据预检返回的结果做分支处理如果文件已存在直接显示上传成功整个过程可能只需要几百毫秒如果存在部分分片前端只上传缺失的分片如果完全不存在前端从第一个分片开始完整上传。第四步所有分片上传完成后前端调用一个合并接口告知服务端所有分片都已就绪请开始合并。服务端把临时分片按顺序拼接成完整文件然后做一次完整性校验比如比对前后端计算的文件 MD5校验通过后返回最终的文件 URL。这套设计的核心价值在于它把“上传”这个大动作拆成了一个状态机预检、调度分片、逐个上传、合并校验。每一步的失败都可以单独重试而且重试的代价是可控的——不会因为最后一步合并失败就让用户重新传整个文件。3. 前端实现核心逻辑切片、并发与进度计算3.1 文件切片与 MD5 指纹计算前端现在处理文件切片主要用原生的Blob.prototype.slice方法。这个方法兼容性非常好主流的现代浏览器都支持。核心代码不复杂function createFileChunks(file, chunkSize) { const chunks []; let start 0; while (start file.size) { const end Math.min(start chunkSize, file.size); chunks.push({ index: chunks.length, start, end, blob: file.slice(start, end) }); start end; } return chunks; }注意这里end用Math.min兜底保证最后一刀不会切越界。实际项目中我会把file的name、size、type等元信息随着分片一起传给服务端因为合并时需要这些信息来还原文件。MD5 的计算我推荐使用spark-md5这个库。它支持增量计算可以把大文件分块读入内存做哈希避免一次性把整个文件读进内存导致浏览器崩溃。一个常见的优化是不需要读全部文件内容来算 MD5而是每隔一段采样一个 chunk比如每 2MB 读取 100KB加上文件头、文件尾各 200KB用这些采样数据算指纹。这样 2GB 的文件实际计算的数据量只有几十 MBMD5 计算时间能从 5 秒压缩到 1 秒以内。代价是冲突概率略微上升但对绝大多数场景这个权衡是值得的。import SparkMD5 from spark-md5; function computeFileHash(file, chunkSize 2 * 1024 * 1024) { return new Promise((resolve, reject) { const spark new SparkMD5.ArrayBuffer(); const reader new FileReader(); const totalChunks Math.ceil(file.size / chunkSize); let currentChunk 0; let hashParts []; // 采样策略读取首尾和中间若干小块 const sampleSize 100 * 1024; for (let i 0; i totalChunks; i) { if (i 0 || i totalChunks - 1 || i % 7 0) { const start i * chunkSize; const end Math.min(start sampleSize, file.size); hashParts.push(file.slice(start, end)); } } // 按顺序读取并喂给 spark-md5 function loadNext(index) { if (index hashParts.length) { resolve(spark.end()); return; } reader.onload (e) { spark.append(e.target.result); loadNext(index 1); }; reader.onerror reject; reader.readAsArrayBuffer(hashParts[index]); } loadNext(0); }); }提示MD5 不是密码学用途这里只做文件去重和完整性校验不是安全用途性能优先没问题。如果你对碰撞有更高要求可以在 MD5 之外再加一个文件大小的校验组合。3.2 并发控制分片不是一次全发出去切片切好了并不意味着要把几百个分片一次性全部并发发送。我把 200 个请求同时发出去浏览器能扛住但服务端不一定能扛住而且网络拥塞反而会让整体速度变慢。实践里我用的是一个并发池的思路限制同时最多跑 3 到 5 个上传请求每完成一个就从队列里补一个进来始终让在途请求维持在一个稳定水位。async function uploadChunksInPool(chunks, uploadFn, concurrency 4) { const results new Array(chunks.length); let index 0; async function worker() { while (index chunks.length) { const current index; try { results[current] await uploadFn(chunks[current]); } catch (err) { results[current] { error: err }; // 这里不 throw让其他分片继续 } } } const workers Array.from({ length: concurrency }, () worker()); await Promise.all(workers); return results; }并发数其实是另一个需要权衡的参数。并发太高前端会占用大量内存保存每个分片的请求状态服务端接收临时文件的磁盘 IO 也可能打满并发太低比如只有 1大文件的上传速度会明显慢于宽带上限。我一般取 4 到 6 作为默认并发在 4G/5G 网络下都能比较稳定地跑满带宽。分片上传的进度计算不能简单地用“已上传分片数 / 总分片数”来算。因为每个分片大小相同这个公式没问题但真实场景里有失败重试、有断点跳过更稳妥的方式是根据“已上传字节数 / 总字节数”来算。每次分片成功累计chunk.end - chunk.start这个字节数这样即使用户重试同一个分片也不会重复计算进度。3.3 断点续传的前端状态管理断点续传的前端核心是“预检后决定传什么”。我常用的做法是在上传开始前调用一个checkFile接口入参是文件指纹和文件大小返回结构长这样{ exists: false, uploadedChunks: [1, 2, 3, 5, 6], chunkSize: 5242880, fileId: a1b2c3d4 }这里uploadedChunks是一个数字数组告诉前端服务端已经有哪些分片了。前端拿到这个数组只需要从完整的分片列表里过滤掉那些已经上传的剩下的才是真正需要传的const needUpload chunks.filter( (chunk) !checkResult.uploadedChunks.includes(chunk.index) );这个方案的优点是服务端实现简单只要在分片上传接口里每成功接收一个分片就往数据库里记录一行包含文件指纹和分片序号的数据。预检时查一下这个表返回给前端即可。如果你不想让预检接口返回一个很大的数组比如上千个分片还可以改成返回一个位图字符串用 0/1 表示第 i 个分片是否已上传。这能显著减小响应体但对绝大多数项目来说返回一个uploadedChunks数组就够用了。4. 服务端实现要点临时文件、分片校验与合并策略4.1 分片接收接口的设计服务端接收分片的上传接口我通常设计成POST /api/chunk表单字段包含fileId文件唯一标识前端生成或者预检时服务端返回index分片序号从 0 开始totalChunks总分片数chunk分片的二进制内容这里每个字段都有它存在的理由。fileId用来在服务端定位这个文件的所有分片index用来排序和去重totalChunks用来判断什么时候所有分片都齐了。接收分片时需要把每个分片先写入一个临时目录。这个临时目录的结构按照fileId/index来组织比如/tmp/uploads/a1b2c3d4/ 0.part 1.part 2.part ...为什么不直接把分片内容存数据库因为大文件的分片数据动辄几 MB数据库存储和读取的开销远高于文件系统而且最终合并还是要从磁盘读。用文件系统做临时存储性能和实现复杂度都更友好。每个分片接收完成后需要注意校验分片的大小是否与预期一致。合法的分片除了最后一片大小都应该等于chunkSize最后一片应小于等于chunkSize。如果校验失败应该返回错误并删除刚写入的临时文件防止脏数据堆积。# 伪代码分片接收逻辑 CHUNK_SIZE 5 * 1024 * 1024 app.post(/api/chunk) async def receive_chunk(file_id: str, index: int, chunk: bytes): if index 0: raise HTTPException(400, index invalid) if index total_chunks - 1: max_size last_chunk_size else: max_size CHUNK_SIZE if len(chunk) max_size: raise HTTPException(400, chunk size mismatch) file_dir f/tmp/uploads/{file_id} os.makedirs(file_dir, exist_okTrue) chunk_path os.path.join(file_dir, f{index}.part) with open(chunk_path, wb) as f: f.write(chunk) # 记录分片上传状态到数据库 db.record_chunk(file_id, index) return {file_id: file_id, index: index, ok: True}4.2 合并接口与完整性校验当所有分片都上传完毕前端会调用POST /api/merge入参是fileId和文件原始信息。服务端拿到请求后第一步是先数一下临时目录里的分片数量看是否等于totalChunks不满足直接返回错误提醒前端还有分片没传完。数量对上之后就开始按序合并。合并的时候要注意必须按照分片的index顺序逐个写入最终文件不能并发写否则文件内容会错乱。实现上可以用分片序号对分片文件排序然后以追加方式写入最终文件。app.post(/api/merge) async def merge_chunks(file_id: str, total_chunks: int, file_name: str): file_dir f/tmp/uploads/{file_id} final_path f/data/uploads/{file_id}_{file_name} with open(final_path, wb) as out_f: for i in range(total_chunks): chunk_path os.path.join(file_dir, f{i}.part) if not os.path.exists(chunk_path): raise HTTPException(400, fchunk {i} missing) with open(chunk_path, rb) as in_f: out_f.write(in_f.read()) # 合并后清理临时分片 shutil.rmtree(file_dir) return {file_id: file_id, url: f/files/{file_id}}这里有个非常关键的细节合并后一定要做完整性校验。我在实际项目中踩过一个坑前端 MD5 算的是原始文件的指纹但服务端合并出来以后由于某个分片数据损坏导致合并后的文件播放到一半出现花屏。不加校验根本发现不了。校验的方式有两种思路一是前端计算整个文件的 MD5合并完成后服务端再算一次合并文件的 MD5两者比对二是更轻量的做法前端在切分时记录每个分片的 MD5服务端接收时逐个校验最后再校验总文件的大小是否等于file.size。如果大小对但内容有损坏这种轻量校验可能漏过所以如果业务对数据完整性要求高还是推荐全量 MD5 比对。4.3 秒传的服务端实现逻辑秒传的实现在服务端其实是一个“查重”操作。预检接口里我先用文件指纹去数据库里查如果找到了相同指纹的记录而且这个记录对应的文件状态是“已完成”就直接返回exists: true。前端收到这个结果跳过所有上传步骤直接视为上传成功。app.post(/api/file/check) async def check_file(file_hash: str, file_size: int): record db.find_file_by_hash_and_size(file_hash, file_size) if record and record.status done: return {exists: True, file_id: record.id, url: record.url, uploaded_chunks: []} uploaded_chunks db.get_uploaded_chunks(file_hash) return {exists: False, uploaded_chunks: uploaded_chunks, file_id: file_hash}这里要注意秒传的判断条件必须同时包含“指纹相同”和“文件大小相同”因为理论上存在不同文件拥有相同 MD5 的可能性虽然概率极低但加了大小条件后碰撞概率基本可以忽略。另外秒传在业务上有一个典型的应用场景团队协作工具里成员 A 上传了一个设计文档成员 B 后来也拖了同一个文档进来。如果每次都让 B 重新传一个几百 MB 的文件既浪费带宽也浪费存储。用了秒传B 这边秒开上传完成体验非常顺滑。而且文件尺寸越大这种体验提升越明显——一个 3GB 的安装包如果是重复文件用户几乎感觉不到“上传”这个过程的存在。5. 前端上传流程的完整落地代码5.1 核心控制流程从预检到合并的调度器前面把前端切片、并发、断点、秒传的各自逻辑讲完了这一节把它们串起来写一个完整的上传控制器。这个控制器负责整个上传任务的调度对外暴露start()方法和进度事件。class LargeFileUploader { constructor({ file, chunkSize 5 * 1024 * 1024, concurrency 4 }) { this.file file; this.chunkSize chunkSize; this.concurrency concurrency; this.chunks []; this.aborted false; this.uploadedBytes 0; } async start() { // 1. 切片 this.chunks createFileChunks(this.file, this.chunkSize); // 2. 计算文件指纹 this.fileHash await computeFileHash(this.file); // 3. 预检 const checkResult await this.checkFile(); // 4. 秒传判断 if (checkResult.exists) { this.onComplete?.(checkResult.url); return; } // 5. 过滤已上传分片 const needUpload this.chunks.filter( (ch) !checkResult.uploadedChunks.includes(ch.index) ); // 6. 并发控制上传 await this.uploadChunksInPool(needUpload); // 7. 合并 const mergeResult await this.merge(); // 8. 完成回调 this.onComplete?.(mergeResult.url); } }这个类的结构很直白start()方法按顺序执行五个阶段每个阶段的失败都有对应的处理逻辑。比如说第 3 步预检报错那这次上传直接终止提示用户检查网络第 6 步某个分片失败会放到失败重试队列里最多重试 3 次超过则整个任务标记为失败。这种设计的好处是“主流程单一、边界能力强”。不管用户是关掉浏览器再回来还是上传到一半掉线重新执行start()时预检接口都能准确地告诉前端还缺哪些分片前端只需要补传缺失部分不用重复劳动。5.2 失败重试与防止重复上传失败重试这块我做了一个“重试队列”。每个分片上传函数被包裹在一个withRetry方法里它会尝试最多 3 次每次失败后等待 1 秒、2 秒、4 秒指数退避再重试。这样既保证了网络抖动时可以恢复又不会因为立即重试而加重网络拥堵。还有一个容易被忽略的点即使服务端已经接收过一个分片前端也可能因为网络问题没收到成功响应从而触发重试。这时候如果服务端不做幂等处理就会两次写入同一个分片文件虽然第二次覆盖第一次的结果通常是无害的但也是一个 IO 浪费。更稳妥的做法是服务端在写分片前先检查是否已存在同序号的.part文件如果存在且文件大小正确直接返回成功不再重新写入。这样保证接口幂等前端重试也放心。async uploadSingleChunk(chunk) { const formData new FormData(); formData.append(fileId, this.fileHash); formData.append(index, chunk.index); formData.append(totalChunks, this.chunks.length); formData.append(chunk, chunk.blob, this.file.name); for (let attempt 1; attempt 3; attempt) { try { const res await fetch(/api/chunk, { method: POST, body: formData }); if (!res.ok) throw new Error(upload failed: ${res.status}); // 累加字节进度回调给 UI 层 this.uploadedBytes (chunk.end - chunk.start); this.onProgress?.(this.uploadedBytes, this.file.size); return; } catch (err) { if (attempt 3) throw err; await new Promise((r) setTimeout(r, 1000 * attempt)); } } }5.3 用户取消与刷新后的恢复现场实际使用中用户随时可能点了“取消上传”然后刷新页面。这时候如果没有任何恢复机制前面传了半天的分片就白费了。有了断点续传恢复现场就很简单页面重新加载后用户重新选择同一个文件程序重新计算 MD5如果采样策略速度很快然后预检接口会返回之前已经传过的分片列表前端继续从断点处上传。这里有一个体验细节值得注意为了让用户更容易恢复我会在浏览器 localStorage 里存一份上传记录key 是文件指纹value 是文件名、文件大小、上传时间。当用户再次选择文件时先查 localStorage 看有没有相同指纹的记录如果有就直接提示“检测到未完成的上传点击续传”而不是静默地直接开始。这样做给了用户预期不会觉得奇怪。取消上传的实现最直接的方式是在上传控器里设置一个aborted标志位在每个分片开始前检查这个标志如果为 true 就停止继续取队列中的任务。已经发出的请求可以通过AbortController中止。abort() { this.aborted true; // 中止所有在途的 fetch this.controllers.forEach((controller) controller.abort()); }6. 常见问题与排查技巧实录6.1 上传过程中的网络断流和超时现象小文件上传一切正常大文件传到一半控制台显示请求挂起随后返回net::ERR_CONNECTION_RESET或504 Gateway Timeout。原因中间的任何一层代理Nginx、网关、负载均衡都可能设置了请求超时时间。分片上传之后单个请求时长变短这个问题会缓解很多但并未完全消失。如果分片设置得太大或者某一片上传时网速特别差依然可能触发。排查思路先拿一个分片请求单独测试看是不是稳定超时。如果是检查 Nginx 的proxy_read_timeout、proxy_send_timeout和client_max_body_size这三个配置。前两个建议设置成 300 秒以上最后一个设置为你要支持的最大分片大小的 1.5 倍左右。client_max_body_size 10m; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_connect_timeout 60s;6.2 合并后的文件大小不对或内容损坏现象前端显示上传成功但下载下来的文件打不开或者ls -l显示的字节数和原文件不一致。排查思路先确认分片是不是有重叠或遗漏。我在项目里遇到一次奇怪的大小错误排查后发现是前端在生成分片时end的计算写错了导致最后一片和倒数第二片重叠。这个 bug 在大多数文件上不会触发因为最后一片通常比较小但遇到特定尺寸的文件就会出事。所以强烈建议前端写分片逻辑时对start chunkSize做一次和file.size的比较打印日志确认边界。另一个常见错误是服务端合并时按照字典序读取分片文件而分片序号 10 会排在 2 前面导致合并顺序错乱。必须严格按照整数 index 排序不能直接用文件名字符串排序。6.3 秒传误判和文件指纹计算耗时问题现象用户第一次上传一个文件结果秒传生效了但实际上服务器上并没有这个文件。原因文件指纹概率性碰撞或者指纹计算时只采样了文件的部分字节而采样的几个位置恰好拼出一个已存在的指纹。这几乎不会发生但一旦发生影响是用户得到了一个错误的 URL。规避方法如果业务对数据准确性要求高比如金融单据、医疗影像可以采用“指纹 文件大小 文件头/尾片段”的复合校验。预检时除了传 MD5 和大小还可以传文件前 256 字节的哈希、文件后 256 字节的哈希服务端把这三个哈希都做了匹配才算命中秒传。这种方案能把误判概率降得非常低。MD5 计算耗时的问题上文已经说过用采样策略可以大幅缩短时间。另外还可以用 Web Worker 把计算放到后台线程避免阻塞 UI。实测一个 2GB 的文件用完整读取算 MD5 需要约 8 到 10 秒用采样策略可以压到 1 秒以内而且 UI 不卡顿。6.4 服务端临时文件清理现象服务器磁盘被/tmp/uploads目录下的临时分片文件占满但因为文件是不可见的运维排查了很久才找到原因。原因用户上传了一半就离开了或者合并接口失败了临时分片没有及时清理。只要有用户这么操作几次几个 GB 甚至几十 GB 的临时文件就堆起来了。解决方案两个层面。第一在上传接口和合并接口的代码里对错误路径做好清理——接收失败删除刚写入的分片合并失败保留临时文件但标记状态。第二写一个定时任务定期扫描临时目录删除超过 24 小时未被修改的.part文件。这个清理任务在实现上是简单的find命令加 crontab但在生产环境是非常必要的。find /tmp/uploads -name *.part -mtime 1 -delete6.5 并发上传导致服务端内存溢出现象上线后业务高峰期服务端频繁 Full GC甚至 OOM。排查后发现是分片上传接口里把整个分片数据readAllBytes()读到了内存里高并发时 6 个并发 × 5MB 还好但几十个并发一起涌进来很快就撑爆了。解决分片上传接口应该以流式方式接收数据边读边写临时文件不要一次性把整个分片读入内存。Java 的InputStream.transferTo()、Python 的shutil.copyfileobj()、Node.js 的pipeline()都可以处理这种场景。这个优化之后并发能力能提升好几倍。7. 扩展思考这套方案还能怎么演进如果你只是想把大文件上传功能做出来前面这些内容已经足够支撑一个完整的上传模块。但真要往工程化、产品化的方向走有几个方向值得继续折腾。第一个方向是异步化。目前所有分片上传完之后前端立刻调合并接口合并过程可能在几秒内完成也可能因为大文件、慢磁盘等原因需要较长时间。如果合并超过浏览器的等待极限前端就会因为超时报错。更健壮的设计是合并接口接受请求后把任务丢给一个异步任务队列立即返回一个任务 ID前端通过轮询或 WebSocket 监听合并任务的状态等状态变成“完成”后再展示结果。这样无论合并多久前端的体验都是稳定的。第二个方向是跨设备的秒传。秒传目前只能做到“本服务端已有相同文件”时生效。如果你不只有一个存储节点秒传的查询需要做一层分布式去重索引或者在上传前对文件指纹做全局查重。对于小规模的团队一个集中式的文件元数据表就能搞定不需要引入额外的分布式组件。第三个方向是上传监控和统计。我在实际做运维时发现没有数据支撑很难判断当前的上传体验好不好。所以在服务端我加了日志记录每个文件的指纹、分片数、各分片耗时、合并耗时、失败原因定期汇总到监控大盘。这样当用户反馈“变慢了”时不用拍脑袋猜直接看 90 分位的分片上传耗时就知道瓶颈在哪。这里我想特别强调一个容易被忽略的细节分片上传的日志量会很大。一个 2GB 文件切成 400 片意味着 400 条分片接收日志如果当天有 1000 个用户上传大文件一天的日志就有几十万条。不要把这些日志全部发到业务日志文件里最好按独立 topic 隔离或者抽样记录避免日志系统先撑不住。回到最开始的问题为什么大文件上传不能“一把梭”因为网络是不稳定的服务端资源是有限的用户耐心是经不起消耗的。分片上传、断点续传、秒传这三板斧分别从“体积”“恢复”“去重”三个维度解决了大文件传输的痛点。它们不是互相独立的技术方案而是一个组合拳——分片是基础断点续传依赖分片的状态记录秒传则是建立在文件指纹之上的一层优化。这三者结合起来才能让用户面对一个 5GB 的大文件时不会在等待和挫折中流失掉。我个人在实际项目里最深的一个体会是上传功能看起来是个“小功能”但它的实现质量直接决定了用户对产品的第一印象。下载慢用户还可以怪带宽上传失败用户只会觉得这个产品不行。所以不要嫌这套方案复杂花时间把分片、断点、秒传做扎实收益是长期且稳定复利的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门