军工系统里的Spring Cloud项目往往跑在涉密内网里终端环境、网络带宽、安全审查都比互联网项目复杂得多。最近在处理一个模拟项目X的文件传输模块时遇到一个特别实际的硬需求业务方不定时上传数GB级别的影像数据、测试报告包动不动就几百个文件、单个文件几十GB走的还是跨网段传输链路。用最传统的 multipart 上传接口一传就超时服务端内存直接被打满网关也频繁报504。折腾了将近三周最后用前端分片加后端元数据管理的思路把问题解决了。这篇文章就把整个方案拆开讲讲包括分片大小怎么定、续传怎么判断、聚合时怎么校验以及Spring Cloud网关和注册中心在这些场景下需要做的特殊配置给同样在搞Spring Cloud大文件上传的朋友做个参考。1. 需求拆解与整体方案设计1.1 军工系统的上传痛点与普通项目的差异军工系统的软件环境和互联网应用差别非常大做技术方案之前得先摸摸清楚现实约束。我这次面对的部署环境有几个特点内网带宽虽然是千兆但中间经过多级交换和审计设备实际稳定传输速度远低于理论值客户端机器普遍是国产化终端浏览器版本偏老JS能力有限服务端是国产化中间件加分布式存储某些组件版本比主流社区落后不少。在这种环境下做GB级文件上传首要矛盾就是HTTP协议本身的设计缺陷。HTTP没有原生的断点续传能力一个请求从发起到结束要么成功要么失败中间网络抖动一下、网关超时一下、客户端崩溃一下整个文件就得重来。我曾经在测试环境里传一个6GB的包传了40多分钟后链路闪断前端抛了个超时异常后端一看临时文件还留在磁盘上但内容已经损坏等于白白浪费了时间。所以整个方案设计的第一原则是不能依赖单次HTTP请求完成整个文件传输必须把大文件切成小块一块一块传传过的块做持久化记录下次接着传。这就是断点续传的核心思想——把不可控的长任务拆解成一系列可控的短任务。1.2 方案选型为什么最终选择前端分片加后端聚合市面上大文件上传方案大致有几类第三方对象存储的直传、服务端流式接收、前端分片加后端聚合。军工内网环境受限对象存储的SDK依赖外网仓库很多组件拉不下来流式接收虽然能缓解内存压力但网络中断后依然要从头再来业务上不肯接受。最后确定的是前端分片加后端聚合路径。前端把文件按固定大小切成若干块逐块上传每传完一块服务端就落盘并记录元数据全部传完后由服务端触发合并把分片拼成完整文件。这个方案的好处非常明显单块上传大小可控请求在网关层不容易超时某一块失败只需重传该块成本极低已上传分片记录持久化在数据库里天然支持续传。还有一点被我列为硬性要求整个链路不能引入太多外部依赖。军工项目的依赖审批非常严格能用框架自带能力和通用数据库组件的就绝不额外引包。Spring Cloud全家桶本身就提供了Feign、Gateway、注册中心这些基础设施配合一张分片记录表就能完成核心逻辑复杂度可控。1.3 整体架构与核心流程系统架构沿用Spring Cloud的标准分层前端通过Spring Cloud Gateway统一入口访问后端微服务微服务注册到Nacos或Eureka。文件上传链路独立拆了一个file-service服务负责分片元数据的读写、分片文件的落地存储、聚合校验。整体流程概括下来是四步前端先调初始化接口创建上传任务拿到唯一的fileId然后按顺序或并发上传分片每个分片附带fileId、分片序号和MD5值服务端每收到一个分片就落盘、记录状态、更新进度所有分片齐了以后触发聚合接口服务端把分片按序合并并进行整体校验完成后删除临时分片文件。这个流程里有一个容易被忽略的关键点续传不是靠“断点坐标”这种玄乎的东西实现的而是每次上传前先查一下数据库里这个fileId已经存了哪些分片序号前端只把缺失的分片补传上去。理解了这个逻辑后续代码实现就非常清晰了。2. 核心链路设计与关键参数计算2.1 分片大小别拍脑袋带宽、内存、并发三个参数怎么算分片大小是整个方案里对体验影响最大的参数。分片太大单请求耗时依然偏高网关超时问题没有根治分片太小请求数量爆炸服务端IO压力和数据库写入压力都会翻倍前端计算和组装分片的开销也大。当时我按实际环境做了个简单测算。内网到客户端实测稳定带宽大概30MB/s网关层请求超时限制设定为60秒单分片大小上限就可以粗略估为 30MB/s × 60s 1800MB。这个数字显然太大了因为还要给服务端落盘和校验留出时间余量。实际业务场景里客户端单次请求能承受的传输时间控制在10秒左右体验较好所以按带宽打五折再乘10秒计算约150MB。但分片也不能只看带宽。服务端接收分片时要写入磁盘、同步更新数据库状态如果并发上传8个分片且每个分片5MB瞬间占用的内存缓冲区、临时文件句柄、数据库连接数都不小。军工项目经常有并发多任务上传的场景必须留足余量。我最终把分片大小定为8MB实测在百兆内网环境下上传一个1.5GB文件大概需要190个分片单分片传输耗时3到5秒并发数为3时整体速度能达到带宽上限的80%左右比较均衡。2.2 元数据表设计一张表撑起续传、秒传与校验分片管理依赖数据库记录状态表结构设计直接决定这个方案能不能扛住并发和断点续传。我用的核心表叫file_upload_record字段大致如下字段名类型说明file_idvarchar(64)全局唯一文件标识由上传初始化接口生成file_namevarchar(255)原始文件名file_sizebigint文件总字节数chunk_sizeint分片大小字节chunk_totalint分片总数chunk_uploadedint已上传分片数file_md5varchar(32)整个文件的MD5值用于聚合后校验statustinyint任务状态0初始化 1传输中 2聚合中 3完成 4失败create_timedatetime创建时间update_timedatetime最后更新时间另外一张表是file_chunk_record记录每一个分片的详情。字段包括id、file_id、chunk_index、chunk_md5、chunk_size、upload_time、storage_path。这张表有个核心作用续传时前端只要查这张表就能知道哪些chunk_index已经服务端确认接收不需要额外设计复杂的断点坐标协议。这里有个设计细节值得注意chunk的MD5值和整个文件的MD5值要单独存。分片校验用chunk_md5聚合后用file_md5做整体校验。如果只存整体MD5分片传错了要到聚合阶段才能发现返工成本太高。每个分片传完立刻校验MD5能尽早拦截损坏数据。2.3 秒传与预检流程先算哈希再决定要不要传在分片上传之前前端可以先算整个文件在客户端的MD5值传给后端做一个预检。后端拿这个MD5去库里查如果发现同样MD5的文件已经上传过且状态为“完成”直接返回“秒传成功”的标识前端省掉整轮分片上传。秒传在军工场景下非常实用因为很多测试数据包是固定版本反复分发的不同用户终端传同一份文件的情况比比皆是。这个预检接口还能连带完成另一件事如果之前这个MD5对应的上传任务只做了一半接口会顺便把已上传分片列表返回给前端前端直接进入续传模式。这等于把秒传和续传两个功能在一个接口里统一起来了接口命名就叫precheck。做这个接口时有一个坑要提醒有些文件内容相同但文件名不同MD5是一样的。是否允许冲突后复用历史文件要结合业务逻辑确认。我当时采用了“MD5加文件大小双重判断”的策略并且复用文件时把最新任务的文件名映射到已存储文件路径避免文件名错乱。3. 实操过程与核心环节实现3.1 上传初始化与续传查询接口的实现上传流程的第一步是调initUpload接口创建任务。这个接口接收fileName和fileSize两个参数内部生成UUID作为fileId计算分片总数并写入file_upload_record初始状态为0。有一点值得注意初始化接口尽量不要做任何磁盘文件创建操作因为不是所有初始化任务都会真的上传避免产生大量空临时文件。续传查询接口实际上就是precheck接口的扩展。前端在初始化之后、正式上传前调用precheck后端根据客户端传来的fileMd5查找关联任务找到后返回chunkTotal以及已上传的分片索引集合uploadedChunks。前端只需要把这个集合和全部分片索引做个差集就知道要补传哪些分片。这里要特别注意幂等性查询同一个MD5可能对应多个历史任务查询逻辑必须加上状态过滤优先返回正在进行中或最近完成的任务否则可能出现多个任务交叉续传的错误数据。我在这个接口上加了一个简单规则优先查status1(传输中)的任务没有的话再查status3(已完成)的任务。3.2 分片上传接口的接收与落盘逻辑分片上传接口是整个链路里压力最大的接口实现上我用了临时文件加分片索引的方式落盘。每个分片到达服务端后先从请求参数里取出fileId和chunkIndex将分片二进制流写入文件系统上一个专门的临时目录文件名命名规则是{fileId}_{chunkIndex}.part。写入完成后立刻计算该分片的MD5和请求里携带的chunkMd5做比对。如果一致就在file_chunk_record里插入一条记录并更新file_upload_record的chunk_uploaded字段。如果不一致删除刚写入的临时文件并返回错误提示前端会重新上传这个分片。有些方案会把元数据更新放在分片全部传完之后统一做我不建议这样。分片记录一定要实时写入因为断点续传依赖的就是这些记录一旦中途宕机没写记录的分片就全部白传了。chunk_uploaded字段的更新还可以做原子累加用数据库行锁或者乐观锁避免并发覆盖。3.3 分片聚合流程不只是拼接文件那么简单当所有分片都上传完后前端调用merge接口触发聚合。后端首先校验file_chunk_record里该fileId的分片数量是否等于chunkTotal这是第一道防线。然后按chunk_index排序依次把临时分片文件的内容追加写入最终文件。聚合过程有几个小心机要讲清楚。第一个是写入顺序必须确保字节顺序和原始文件完全一致因为分片如果并发上传到达顺序是没保证的排序在这里是刚需。第二个是额外校验聚合完成后重新计算整个文件的MD5和初始化时的fileMd5比对不一致就说明某个分片损坏被前端漏掉了需要整轮重传。聚合时我用了一个比较大的缓冲流边读边写减少磁盘IO次数。全部聚合成功后把临时分片目录整个删掉同时把file_upload_record的状态更新为3。删除分片这个动作不能漏军工环境磁盘空间本来就不宽裕一个GB级文件的分片临时文件占用的空间是总文件体积的好几倍不及时清理迟早把Inode耗尽。3.4 前端分片的计算与上传控制前端代码我用JavaScript的File API实现。核心是利用File.slice方法把文件对象按指定大小切分得到若干个Blob片段然后构造FormData逐块发送。前端控制逻辑有三个要点。第一是并发控制我用简单的队列机制限制最多同时3个上传请求避免浏览器和服务端被压垮。第二是失败重试单个分片上传失败后最多重试3次重试之间加一个短延迟超过次数就标记为失败整个任务停下来等用户重新触发。第三是取消与恢复取消时记录当前已上传分片索引恢复时直接调用precheck接口拿服务端的已传列表来补传。用代码描述的话前端切分大致是这个样子function createChunks(file, chunkSize) { const chunks []; let start 0; while (start file.size) { chunks.push(file.slice(start, start chunkSize)); start chunkSize; } return chunks; }上传时每个chunk的FormData里除了文件块本身还要带上fileId、chunkIndex、chunkMd5这些参数。MD5计算在浏览器端用SparkMD5这个库计算几GB文件的MD5会有点耗时我会在真正点击上传前先算完避免传输过程中长时间阻塞交互。4. Spring Cloud组件适配与网关层配置4.1 网关超时与请求大小限制的调整Spring Cloud Gateway默认的响应超时非常短大文件上传请求到了网关就会被直接掐断。哪怕分片只有8MB在复杂的跨网段链路上传输也可能超过默认的几十秒限制。这个配置必须在路由级别和全局级别同时调整。我在Gateway里加了如下配置spring: cloud: gateway: httpclient: connect-timeout: 5000 response-timeout: 600000 default-filters: - DedupeResponseHeaderAccess-Control-Allow-Originconnect-timeout保持5秒没问题因为建立连接本身不该花太久response-timeout调到了10分钟保证一个分片请求在极端情况下也有足够时间完成响应。同时我在路由配置里对file-service的路径做了保底设置避免全局配置被特定路由覆盖。还有一处容易踩坑的地方Spring Cloud Gateway接收请求时Netty层默认对请求体大小没有硬性限制但请求头大小有限制。分片上传虽然把体积控制下来了但客户端在上传时往往会加一些自定义头比如文件MD5、分片序号如果某个文件名的长度特别長导致URL过长也可能碰到Netty的header大小限制。遇到这种情况可以把maxHeaderSize适当调大。4.2 服务注册与负载均衡的幂等性保障Spring Cloud的负载均衡默认会对请求做重试这在普通接口上问题不大但在上传场景里必须谨慎处理。一个分片请求发送后如果因为网络抖动导致客户端没收到响应Feign或LoadBalancer重试时会再次提交同一个分片。服务端如果没有幂等处理就会产生重复分片记录或者分片内容被覆盖。我在分片上传接口里做了一层幂等保护处理分片之前先查询file_chunk_record如果这个分片已经存在且MD5一致直接返回成功不重复写磁盘。这样做带来一个额外好处网关或负载均衡层可以放心开启重试策略最多重试两次数据安全不会受影响。设置负载均衡重试的配置大致如下spring: cloud: loadbalancer: retry: enabled: true注意不同Spring Cloud版本里重试配置项差异较大关键是搞清楚当前版本的默认重试次数别让配置生效了但逻辑没按预期跑。4.3 服务端内存与线程池调优上传接口的并发性能很大程度上取决于Servlet容器的线程池配置。每个上传请求从接收到写盘会占用一个工作线程如果线程池太小高并发时请求排队严重前端会频繁超时。我用的是Tomcat容器针对上传场景把最大线程数适当提高并配合较长的keep-alive超时server: tomcat: max-threads: 400 max-connections: 1000 accept-count: 200 connection-timeout: 30000同时在上传接口对应的业务代码里我尽量避免在请求处理线程中做耗时操作。分片落盘走的是普通IO流但元数据库更新我用了一个独立的小线程池异步处理把数据库写入从主链路里剥离出去。这样做的代价是数据库更新有一定延迟但换来的是接口吞吐量明显上升整体利大于弊。5. 军工场景下的安全性、国产化与高可用考量5.1 数据加密与传输安全设计军工系统对数据传输的安全性要求很高。文件在传输过程中经过网关进入服务内部网络如果明文传输一旦链路被监听就会造成数据泄露。我在这套方案里做了两重加密处理客户端到网关之间走国密HTTPS协议文件落盘前使用国密SM4算法进行加密存储数据库里只保存加密后的文件路径和密钥索引。这里有个细节分片加密会带来性能开销每个分片都加解密会明显拉低上传速度。我做了个折中只在分片落盘时加密分片传输过程中依赖HTTPS链路保护。聚合阶段先把分片逐个解密、聚合后再整体加密基本不影响用户上传体验。密钥管理没有引入独立的密钥管理系统而是用了数据库内一张密钥表加定期轮换的策略。每个上传任务生成一个独立的文件密钥任务完成或失败后密钥销毁这样单个文件泄露不会波及其他文件。5.2 国产化中间件与数据库适配军工环境里数据库大概率是国产数据库微服务框架也可能跑在国产化中间件上。代码层面要做的是彻底屏蔽对特定数据库方言的依赖。我的所有分片记录查询都用了JPA的Criteria规范和标准SQL避免拼接数据库特有的分页语法。国产化数据库对批量插入的支持往往不如传统数据库分片记录单条插入在高并发下容易成为瓶颈。测试下来单条插入TPS也就几百没法支撑大量分片同时写入。后来我改成了批量提交把同一个任务的分片记录攒到一定数量后统一insert性能提升非常明显。工程上不要为了炫技用太复杂的方案批量提交加合适的连接池参数就能解决大部分问题。5.3 服务高可用与故障恢复策略file-service是文件上传的核心服务一旦挂掉整个上传链路就瘫了。我在注册中心里给file-service配置了至少两个实例网关层负责负载均衡。文件存储目录用的是共享存储保证两个实例操作的是同一套文件避免某一台机器上的分片在聚合时找不到。针对进程崩溃的恢复策略JVM启动参数里加了ShutdownHook在进程退出时把当前正在处理的上传任务状态标记为“中断”临时文件保留不删。下次服务重新起来后用户端重新调用precheck把未完成的分片列表拿回来继续传。这样即使服务发生重启也不会出现用户需要重新上传整个文件的情况。6. 常见问题与排查技巧实录6.1 分片上传却总是超时隐藏的网闸与审计瓶颈有段内网环境里单个分片只有8MB传输却动不动就超过60秒。抓包一看数据到了某一跳设备上就停住了偶尔重传几次才过去。后来才知道跨网段传输要过网闸和审计设备这些设备对流量有缓存和限速策略并不是纯粹的物理带宽瓶颈。这种情况光调分片大小解决不了我给前端加了自适应分片逻辑客户端先传一个1MB的探测分片统计实际传输耗时据此动态调整后续分片大小。如果探测下来带宽好分片就调大一些带宽差就把分片调小到4MB左右虽然分片数量多了但每个分片都能在超时时间内完成整体反而更快。6.2 分片全部上传成功但不触发聚合排查这种问题要从前端和后端两侧入手。前端检查merge接口是否真的被调用后端检查数据库里status是否还是1。如果状态没变说明merge请求丢失或者被网关拦截。我遇到过的真实原因是网关路由配置里merge接口路径写错了请求打到了另一个服务上结果提示404前端误以为聚合成为了。后端聚合接口本身还容易有一个隐藏问题分片已经上传完了但chunk_uploaded的计数因为并发更新不一致而小于chunkTotal。这时候聚合条件永不满足。解决办法是在聚合接口里做校验时直接count一下file_chunk_record的数量而不是依赖累加字段避免脏数据卡住流程。6.3 续传后文件损坏MD5校验为何形同虚设续传逻辑看似完美但某次测试传了一个3GB文件续传聚合后MD5对不上文件打不开。排查后发现问题出在客户端边上传边改文件这个操作上。用户在上传过程中打开了原始文件并做了编辑导致文件体积变大了但初始化时的分片总数和文件大小都是旧值后续分片切分索引对不上原始位置聚合出来的文件自然就乱了。解决的方案是初始化时记录文件的修改时间在precheck和续传时比对当前文件修改时间是否一致如果不一致直接拒绝续传提示用户重新发起上传。这个坑非常隐蔽建议所有做断点续传的方案都把文件元信息的一致性校验加上。6.4 常见问题速查表问题现象可能原因排查与解决办法分片请求超时网闸限速、网关超时过短先抓包定位瓶颈再适当调整分片大小、调大网关超时聚合不触发merge路由错误、计数不一致检查网关路由表聚合时直接count分片记录数续传文件损坏客户端文件被修改初始化时记录文件MD5和修改时间续传前校验一致性上传速度极慢并发数设置太高触发限流前端限制并发数为2-3观察服务端IO情况逐步调整磁盘空间耗尽临时分片未清理聚合成功后立即删除临时目录增加定时清理任务兜底数据库连接池耗尽分片并发写入连接数超限调整连接池大小分片记录批量写入降低连接占用7. 一条实用技巧与经验收尾做了这么多轮的调试最后分享一个很小的处理技巧。上传服务的日志千万不要满屏打印分片上传成功之类的信息分片少的时候无所谓分片一多日志文件能涨到几个GB直接把磁盘塞爆。我当时在上传接口里把日志级别调到了只记录错误和关键状态变化分片的每次成功不记info只有聚合完成、失败重试这类关键节点才输出日志。聚合阶段倒是建议把每个分片的校验结果记全因为出问题时要靠这些日志定位具体是哪个分片坏了。大文件续传的方案到今天已经不新鲜了但军工场景下的特殊环境限制决定了我们不能照搬互联网方案每一个参数、每一条记录、每一份校验逻辑都需要结合实际环境重新审视一遍。希望这篇文章能帮你少踩几个我踩过的坑。