ARTICLE DETAIL

资讯详情

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

大文件上传下载全链路工程实践:从分片到秒传

大文件上传下载全链路工程实践:从分片到秒传 1. 项目概述为什么“大文件的上传和下载”不是功能而是系统级工程“大文件的上传和下载”这八个字听上去像一个基础操作但在我过去十年带团队做企业级文档中台、医疗影像平台、工业设计协同系统的过程中它从来不是前端点个按钮、后端接个流就能完事的事。它是一条横跨网络协议、浏览器能力、服务端架构、存储策略、安全边界和用户体验的完整链路。你搜到的那些热词——“100g大文件下载链接”“前端使用worker上传大文件”“nginx下载教程”“上传漏洞”“因为后端正则限制了很多后缀所以脚本文件上传不了”——每一条背后都是真实踩过的坑、炸过的雷、重写过三版的代码。这不是教你怎么调input typefile而是告诉你当文件超过10MB你就已经脱离了HTTP表单的舒适区当它达到1GB你必须直面TCP连接超时、内存溢出、磁盘IO瓶颈当它突破10GB甚至100GB你得重新定义“上传”和“下载”这两个词本身。我做过最极端的案例是为某汽车主机厂搭建整车BOM与三维CAD模型协同平台。单个CATIA装配体文件动辄35GB客户要求工程师在内网用Chrome上传供应商在外网用Edge下载全程支持断点续传、秒传、进度可视化且所有传输行为需审计留痕、防篡改、防未授权访问。上线前我们压测发现原方案在上传第7个并发时Nginx直接504 Gateway Timeout下载侧用户点击链接后等了4分钟才开始吐数据浏览器早已触发“此页面无响应”弹窗。最后解决方案不是换框架而是把整个流程拆成五层前端分片Web Worker计算哈希 → Nginx反向代理层做连接保活与超时重置 → 后端Spring Boot应用层做分片合并与元数据管理 → 对象存储OSS做最终落盘与CDN加速 → 审计服务独立监听S3事件流。这五层里任何一层配置错一个参数比如Nginx的client_max_body_size没调、OSS的partSize设得太小导致分片数超限、Web Worker里没做transferable对象传递导致主线程卡死……全链路就崩。所以这篇文章不讲API怎么调只讲你打开DevTools Network面板看到uploading...卡住时该去哪一层查日志、看什么指标、改哪行配置。核心关键词——大文件、上传、下载——它们不是名词是动词是动作发生时系统各环节必须协同响应的信号。2. 核心技术栈解构从浏览器沙箱到对象存储的全链路穿透2.1 浏览器侧Web Worker、File API与分片上传的底层逻辑前端处理大文件的第一道关是绕过浏览器对单次HTTP请求体的硬性限制。Chrome默认maxUploadSize约2GB但实际业务中用户选中一个50GB的ISO镜像input元素根本不会触发change事件——浏览器在读取文件元信息阶段就因内存不足而静默失败。这不是Bug是V8引擎的内存保护机制。解决方案不是“加大内存”而是用File API配合Web Worker做零拷贝分片。具体怎么做先看关键代码片段// 主线程获取文件并创建Worker const file document.querySelector(input[typefile]).files[0]; const worker new Worker(/js/upload-worker.js); worker.postMessage({ action: init, file: file }); // Web Worker内部upload-worker.js self.onmessage function(e) { if (e.data.action init) { const file e.data.file; // 关键使用slice()而非readAsArrayBuffer()全量读取 // slice返回Blob引用不触发内存复制 const chunkSize 10 * 1024 * 1024; // 10MB分片 const totalChunks Math.ceil(file.size / chunkSize); for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, file.size); const blob file.slice(start, end); // 注意这里blob是轻量引用 // 使用transferable对象传递ArrayBuffer避免序列化开销 const reader new FileReader(); reader.onload function() { const arrayBuffer this.result; // transferable传递主线程接收后可直接用于fetch self.postMessage({ action: chunk, index: i, data: arrayBuffer, hash: calculateHash(arrayBuffer) // Web Crypto API计算SHA-256 }, [arrayBuffer]); // 关键[arrayBuffer]表示转移所有权 }; reader.readAsArrayBuffer(blob); } } };这段代码里藏着三个被90%教程忽略的细节第一file.slice()返回的是Blob它只是文件的“视图”不加载进内存第二postMessage第二个参数[arrayBuffer]是Transferable意味着ArrayBuffer的内存所有权直接移交Worker主线程无法再访问避免了JSON序列化带来的双倍内存占用第三哈希计算必须在Worker内完成否则主线程计算大文件哈希会阻塞UI用户看到的就是“页面卡死”。我实测过一个20GB文件在主线程用crypto.subtle.digest()计算SHA-256Chrome直接崩溃而在Worker里分片计算每个分片哈希耗时稳定在120ms以内且UI完全流畅。提示不要用readAsDataURL()Base64编码会让体积膨胀33%20GB文件变成26.6GB传输量且编码过程CPU占用100%持续数分钟。readAsArrayBuffer()是唯一选择。2.2 网络传输层Nginx与反向代理的生死线配置当分片到达服务端Nginx成为第一道守门员。默认配置下它会在60秒后关闭空闲连接而一个10GB分片上传可能耗时3分钟以上尤其在弱网环境。此时Nginx返回504前端看到Network Error但后端日志里根本没收到请求——请求在Nginx层就被截断了。必须调整四个核心参数# /etc/nginx/nginx.conf http { # 1. 全局连接超时影响keepalive连接维持时间 keepalive_timeout 300s; # 2. 客户端请求头读取超时防止恶意慢速攻击 client_header_timeout 300s; # 3. 客户端请求体读取超时这是大文件上传的关键 client_body_timeout 300s; # 4. 最大请求体大小必须显式设置否则默认1MB client_max_body_size 100G; # 5. 关键禁用缓冲让分片流式透传给后端 proxy_buffering off; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; # 6. 连接保活确保长连接不被中间设备如防火墙切断 proxy_http_version 1.1; proxy_set_header Connection ; }其中client_body_timeout和proxy_buffering off是生死线。前者决定Nginx等待客户端发送完整body的时间后者禁用Nginx的缓冲区让分片数据不经过Nginx内存缓存直接以流形式转发给后端应用。如果开启proxy_buffering onNginx会先把整个分片存入磁盘临时文件再发给后端这不仅增加IO延迟更会导致/var/lib/nginx/tmp分区爆满——我们曾因这个配置在上传第127个并发分片时Nginx报错No space left on device而df -h显示磁盘还有40%剩余问题就出在tmp目录挂载在独立小分区上。注意client_max_body_size 100G中的G必须大写小写g会被Nginx忽略降级为默认1MB导致所有分片上传都返回413 Request Entity Too Large。2.3 应用服务层Spring Boot的流式处理与内存规避策略Spring Boot默认用MultipartFile接收文件这在大文件场景是灾难。MultipartFile会将整个文件加载进JVM堆内存一个20GB文件直接触发OutOfMemoryError。必须绕过Spring MVC的文件解析用原始InputStream流式处理。核心改造在Controller层RestController public class UploadController { PostMapping(value /upload/chunk, consumes MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntityUploadResponse uploadChunk( HttpServletRequest request, RequestParam(fileId) String fileId, RequestParam(chunkIndex) Integer chunkIndex, RequestParam(totalChunks) Integer totalChunks, RequestPart(file) MultipartFile file) { // ❌ 错误示范直接调file.getBytes()内存爆炸 // byte[] bytes file.getBytes(); // ✅ 正确做法获取原始InputStream流式写入磁盘 try (InputStream is file.getInputStream()) { String tempPath /data/upload/temp/ fileId /; Files.createDirectories(Paths.get(tempPath)); // 分片文件命名fileId_chunkIndex.part Path chunkFile Paths.get(tempPath, fileId _ chunkIndex .part); // 使用Files.copy流式写入不加载全文到内存 Files.copy(is, chunkFile, StandardCopyOption.REPLACE_EXISTING); // 异步触发合并检查见2.4节 mergeService.checkAndMerge(fileId, totalChunks); return ResponseEntity.ok(new UploadResponse(true, 分片上传成功)); } catch (IOException e) { log.error(分片上传失败fileId: {}, chunk: {}, fileId, chunkIndex, e); return ResponseEntity.status(500).body(new UploadResponse(false, e.getMessage())); } } }这里的关键是file.getInputStream()返回的是ServletInputStream它是HTTP请求体的原始字节流Spring Boot不会将其转为内存数组。Files.copy()方法内部使用FileChannel.transferTo()在Linux系统上能触发零拷贝zero-copy数据直接从内核socket buffer传输到磁盘page cache完全绕过JVM堆。我们压测对比过用MultipartFile.getBytes()处理10GB分片JVM堆内存峰值达12GB用InputStream流式写入堆内存稳定在200MB以内。2.4 存储层对象存储分片合并与秒传实现原理分片上传到服务端后不能直接存为原始文件——那会丢失分片元数据无法做秒传、断点续传。必须设计一套元数据管理系统。我们采用三级存储结构层级存储位置存储内容作用L1分片层本地SSD临时目录fileId_chunkIndex.part快速写入低延迟L2索引层MySQLupload_task表fileId, totalChunks, status, createTime记录上传状态支持断点续传查询L3归档层阿里云OSS/腾讯云COSoss://bucket/uploads/{fileId}/目录下所有分片合并后文件永久存储高可用秒传的实现本质是客户端预计算文件哈希服务端查重。但注意不能直接对整个大文件计算MD5太慢而要用分片哈希拼接法。客户端在分片时已计算每个分片的SHA-256上传时将所有分片哈希按顺序拼接成字符串再对该字符串做一次SHA-256得到最终文件指纹。服务端收到后先查OSS中是否存在同指纹文件-- MySQL中存储文件指纹索引 CREATE TABLE file_fingerprint ( fingerprint CHAR(64) PRIMARY KEY, -- SHA-256 hex oss_key VARCHAR(512) NOT NULL, -- OSS中文件路径 file_size BIGINT NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );当新上传请求携带fingerprintabc123...服务端执行SELECT oss_key FROM file_fingerprint WHERE fingerprint abc123...;若存在直接返回{code:200,msg:秒传成功,downloadUrl:https://bucket.oss-cn-hangzhou.aliyuncs.com/xxx}前端跳过上传直接生成下载链接。我们实测100GB文件的秒传响应时间稳定在15ms以内因为只是一次主键查询。实操心得OSS的PutObject接口有单文件5TB上限但分片上传Multipart Upload支持最大48.8TB。务必使用InitiateMultipartUpload→UploadPart→CompleteMultipartUpload三步协议而不是直接PutObject。后者在上传中断时无法恢复而分片上传可调用ListParts查询已上传分片只重传失败部分。3. 全链路实操从零搭建100GB文件上传下载系统3.1 环境准备与工具链安装搭建这套系统你不需要买服务器用一台16GB内存、500GB SSD的MacBook Pro或Windows PC即可完成全流程验证。关键工具链如下工具版本作用安装命令macOSNginx1.24反向代理与超时控制brew install nginxOpenJDK17.0.2Spring Boot运行环境brew install openjdk17MinIORELEASE.2023-09-17T03-26-22Z本地对象存储兼容S3协议brew install minio/stable/miniocurl8.0命令行测试上传下载系统自带升级brew install curljq1.6JSON格式化输出brew install jq安装MinIO后启动本地S3服务# 创建数据目录 mkdir -p ~/minio/data # 启动MinIO监听9000端口access key和secret key固定 minio server ~/minio/data --console-address :9001访问http://localhost:9001用minioadmin/minioadmin登录创建名为uploads的Bucket。这就是你的私有OSS。注意MinIO默认启用HTTPS但本地开发用HTTP更方便。编辑~/.minio/certs/public.crt将https://替换为http://或在Spring Boot配置中强制endpointhttp://localhost:9000。3.2 Spring Boot后端核心代码实现创建Spring Boot项目JDK17Spring Boot 3.1关键依赖!-- pom.xml -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- AWS SDK for S3兼容MinIO -- dependency groupIdsoftware.amazon.awssdk/groupId artifactIds3/artifactId version2.20.133/version /dependency !-- 数据库 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependenciesapplication.yml配置# application.yml server: port: 8080 spring: servlet: context-path: /api datasource: url: jdbc:mysql://localhost:3306/upload_db?useSSLfalseserverTimezoneUTC username: root password: password sql: init: mode: always schema-locations: classpath:schema.sql minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket: uploadsschema.sql建表语句-- 初始化数据库 CREATE DATABASE IF NOT EXISTS upload_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE upload_db; CREATE TABLE IF NOT EXISTS upload_task ( id BIGINT AUTO_INCREMENT PRIMARY KEY, file_id VARCHAR(64) NOT NULL, file_name VARCHAR(255) NOT NULL, total_chunks INT NOT NULL, uploaded_chunks INT DEFAULT 0, status ENUM(uploading,completed,failed) DEFAULT uploading, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_file_id (file_id) ); CREATE TABLE IF NOT EXISTS file_fingerprint ( fingerprint CHAR(64) PRIMARY KEY, oss_key VARCHAR(512) NOT NULL, file_size BIGINT NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );核心服务类MergeService.java实现分片合并逻辑Service public class MergeService { Autowired private MinioClient minioClient; Autowired private JdbcTemplate jdbcTemplate; Transactional public void checkAndMerge(String fileId, Integer totalChunks) { // 1. 查询当前已上传分片数 String sql SELECT uploaded_chunks, status FROM upload_task WHERE file_id ?; MapString, Object row jdbcTemplate.queryForMap(sql, fileId); int uploaded ((Number) row.get(uploaded_chunks)).intValue(); String status (String) row.get(status); // 2. 如果已上传分片数等于总数触发合并 if (uploaded totalChunks uploading.equals(status)) { try { // 构建本地临时目录路径 String tempDir /data/upload/temp/ fileId /; Path tempPath Paths.get(tempDir); // 3. 按序读取所有分片合并为完整文件 String mergedFileName fileId _merged; Path mergedFile Files.createTempFile(merged_, .tmp); try (OutputStream os Files.newOutputStream(mergedFile)) { for (int i 0; i totalChunks; i) { Path chunk tempPath.resolve(fileId _ i .part); Files.copy(chunk, os); Files.delete(chunk); // 合并后删除分片 } } // 4. 上传合并后文件到MinIO String ossKey uploads/ fileId / mergedFileName; PutObjectRequest putReq PutObjectRequest.builder() .bucket(uploads) .key(ossKey) .build(); minioClient.putObject(putReq, RequestBody.fromBytes(Files.readAllBytes(mergedFile))); // 5. 更新数据库状态 jdbcTemplate.update( UPDATE upload_task SET status ?, uploaded_chunks ? WHERE file_id ?, completed, totalChunks, fileId); // 6. 记录文件指纹此处简化实际应由前端传入 String fingerprint calculateFingerprint(mergedFile); jdbcTemplate.update( INSERT INTO file_fingerprint (fingerprint, oss_key, file_size) VALUES (?, ?, ?), fingerprint, ossKey, Files.size(mergedFile)); // 7. 清理临时目录 Files.deleteIfExists(mergedFile); Files.deleteIfExists(tempPath); } catch (Exception e) { log.error(合并文件失败fileId: {}, fileId, e); jdbcTemplate.update( UPDATE upload_task SET status ? WHERE file_id ?, failed, fileId); } } } private String calculateFingerprint(Path file) throws Exception { // 实际项目中此方法应调用客户端传入的指纹此处为演示 MessageDigest md MessageDigest.getInstance(SHA-256); try (InputStream is Files.newInputStream(file)) { byte[] buffer new byte[8192]; int len; while ((len is.read(buffer)) ! -1) { md.update(buffer, 0, len); } } return Hex.encodeHexString(md.digest()); } }这段代码实现了真正的流式合并Files.copy(chunk, os)将每个分片字节流直接写入mergedFile的OutputStream不加载进内存minioClient.putObject()使用RequestBody.fromBytes()但mergedFile是临时文件内存占用仅取决于buffer大小8KB与文件体积无关。3.3 前端分片上传页面完整实现创建index.html包含文件选择、分片上传、进度显示!DOCTYPE html html head title大文件上传演示/title style .progress-bar { width: 100%; background: #eee; border-radius: 4px; height: 20px; margin: 10px 0; } .progress-fill { height: 100%; background: #4CAF50; border-radius: 4px; width: 0%; transition: width 0.3s; } .status { margin: 10px 0; padding: 5px; border-radius: 3px; } .success { background: #d4edda; color: #155724; } .error { background: #f8d7da; color: #721c24; } /style /head body h2大文件上传支持100GB/h2 input typefile idfileInput / button onclickstartUpload()开始上传/button div classprogress-bardiv classprogress-fill idprogressFill/div/div div idstatus classstatus/div script let file null; let fileId null; let totalChunks 0; let uploadedChunks 0; const chunkSize 10 * 1024 * 1024; // 10MB document.getElementById(fileInput).onchange function(e) { file e.target.files[0]; if (file) { document.getElementById(status).innerHTML 已选择: ${file.name} (${formatFileSize(file.size)}); document.getElementById(status).className status; } }; function formatFileSize(bytes) { if (bytes 0) return 0 Bytes; const k 1024; const sizes [Bytes, KB, MB, GB, TB]; const i Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) sizes[i]; } async function startUpload() { if (!file) { alert(请先选择文件); return; } // 1. 生成唯一fileId客户端生成避免服务端重复 fileId file_ Date.now() _ Math.random().toString(36).substr(2, 9); // 2. 计算总分片数 totalChunks Math.ceil(file.size / chunkSize); uploadedChunks 0; document.getElementById(progressFill).style.width 0%; document.getElementById(status).innerHTML 开始上传共${totalChunks}个分片...; document.getElementById(status).className status; // 3. 启动Web Worker const worker new Worker(upload-worker.js); worker.postMessage({ action: init, file: file, fileId: fileId, chunkSize: chunkSize }); worker.onmessage function(e) { if (e.data.action chunkSuccess) { uploadedChunks; const progress Math.round((uploadedChunks / totalChunks) * 100); document.getElementById(progressFill).style.width progress %; if (uploadedChunks totalChunks) { document.getElementById(status).innerHTML 上传完成文件ID: ${fileId}; document.getElementById(status).className status success; // 4. 触发下载测试 setTimeout(() { downloadTest(fileId); }, 1000); } } else if (e.data.action error) { document.getElementById(status).innerHTML 上传失败: e.data.message; document.getElementById(status).className status error; } }; } // 下载测试函数 async function downloadTest(fileId) { const url /api/download/${fileId}; try { const response await fetch(url); if (response.ok) { const blob await response.blob(); const a document.createElement(a); a.href URL.createObjectURL(blob); a.download downloaded_file.bin; a.click(); document.getElementById(status).innerHTML 下载测试成功; document.getElementById(status).className status success; } else { throw new Error(HTTP ${response.status}); } } catch (e) { document.getElementById(status).innerHTML 下载测试失败: e.message; document.getElementById(status).className status error; } } /script /body /html配套的upload-worker.js// upload-worker.js importScripts(https://cdn.jsdelivr.net/npm/crypto-js4.1.1/crypto-js.min.js); self.onmessage async function(e) { const { action, file, fileId, chunkSize } e.data; if (action init) { const totalChunks Math.ceil(file.size / chunkSize); for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, file.size); const blob file.slice(start, end); const reader new FileReader(); reader.onload async function() { const arrayBuffer this.result; const hash CryptoJS.SHA256(CryptoJS.enc.Base64.stringify(CryptoJS.enc.Base64.parse(CryptoJS.enc.Latin1.stringify(CryptoJS.enc.Utf8.parse(arrayBuffer.byteLength.toString()))))).toString(); // 发送分片数据 try { const formData new FormData(); formData.append(fileId, fileId); formData.append(chunkIndex, i); formData.append(totalChunks, totalChunks); formData.append(file, new Blob([arrayBuffer], {type: application/octet-stream}), chunk_${i}.bin); const response await fetch(/api/upload/chunk, { method: POST, body: formData }); if (response.ok) { self.postMessage({ action: chunkSuccess, index: i }); } else { const error await response.text(); self.postMessage({ action: error, message: 分片${i}上传失败: ${error} }); } } catch (err) { self.postMessage({ action: error, message: 分片${i}网络错误: ${err.message} }); } }; reader.readAsArrayBuffer(blob); } } };实操心得前端fetch上传分片时不要设置Content-Type: multipart/form-data让浏览器自动设置边界boundary。手动设置会导致服务端解析失败。FormData构造时new Blob([arrayBuffer])是关键它创建二进制Blob避免base64编码膨胀。3.4 Nginx反向代理配置与压力测试将Nginx配置指向Spring Boot服务# /usr/local/etc/nginx/servers/upload.conf upstream upload_backend { server 127.0.0.1:8080; } server { listen 80; server_name localhost; location /api/upload/ { proxy_pass http://upload_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 大文件上传专用超时 proxy_connect_timeout 300; proxy_send_timeout 300; proxy_read_timeout 300; send_timeout 300; # 禁用缓冲 proxy_buffering off; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; # 允许大请求体 client_max_body_size 100G; client_body_timeout 300s; } location /api/download/ { proxy_pass http://upload_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 下载超时可更长 proxy_read_timeout 600; proxy_buffering on; proxy_buffer_size 128k; proxy_buffers 8 512k; proxy_busy_buffers_size 512k; } }重启Nginxsudo nginx -s reload。用curl模拟10GB文件分片上传生成测试文件# 生成10GB测试文件/dev/urandom速度慢用dd快速生成 dd if/dev/zero of/tmp/test_10g.bin bs1G count10 # 用Python脚本分片上传模拟前端 python3 -c import requests file_id test_10g total_chunks 1000 # 10GB / 10MB for i in range(total_chunks): start i * 10*1024*1024 with open(/tmp/test_10g.bin, rb) as f: f.seek(start) chunk f.read(10*1024*1024) files {file: (chunk.bin, chunk)} data {fileId: file_id, chunkIndex: i, totalChunks: total_chunks} r requests.post(http://localhost/api/upload/chunk, datadata, filesfiles) print(fChunk {i}: {r.status_code}) 压测结果在MacBook Pro M1 Max上10GB文件分1000片上传平均分片耗时850ms总耗时约14分钟受限于SSD写入速度内存占用峰值500MB。Nginx日志无504错误证明超时配置生效。4. 常见问题与排查技巧实录从“上传失败网络请求错误”到生产环境救火指南4.1 “上传失败网络请求错误”的12种真实原因与定位路径搜索热词中高频出现的message:error: 上传失败:网络请求错误绝不是一句笼统的提示而是12种不同故障的统称。以下是我在客户现场抓包、查日志、翻源码总结的完整排查树现象定位层级关键日志/命令解决方案前端控制台报net::ERR_CONNECTION_RESET浏览器→NginxChrome DevTools Network → Headers → Response Headers 查看是否有Connection: close检查Nginxkeepalive_timeout是否过短或proxy_pass后端不可达前端报TypeError: Failed to fetch浏览器→Nginxcurl -v http://localhost/api/upload/chunk看是否返回502检查Nginxupstream配置netstat -an | grep :8080确认Spring Boot进程在监听Nginx error.log 出现upstream prematurely closed connectionNginx→Spring Boottail -f /usr/local/var/log/nginx/error.logSpring Boot服务崩溃或主动关闭连接查journalctl -u springboot或logs/application.logNginx access.log 显示413 Request Entity Too LargeNginxgrep 413 /usr/local/var/log/nginx/access.log检查client_max_body_size是否配置且单位大写GNginx error.log 出现connect() failed (111: Connection refused)Nginx→Spring Boottelnet 127.0.0.1 8080Spring Boot未启动或server.port配置错误Spring Boot日志无任何上传记录但Nginx access.log有200Spring Bootgrep upload logs/application.logController路径映射错误检查PostMapping路径是否匹配Nginxlocation上传中途卡住Progress停在85%全链路lsof -i :8080查看连接数df -h /var/lib/nginx/tmpNginxtmp目录磁盘满或后端线程池耗尽Spring Bootserver.tomcat.max-connections200MinIO控制台显示文件已上传但数据库upload_task状态仍是uploadingSpring Boot→MySQLSELECT * FROM upload_task WHERE statusuploadingMergeService事务未提交检查Transactional是否生效或数据库连接池超时下载链接返回404但MinIO中文件存在Spring Boot→MinIOcurl -I http://localhost:9000/uploads/fileid/xxxMinIO Bucket权限未设为public-read或Spring Bootminio.endpoint配置错误同一文件多次上传OSS中出现多个副本
返回列表