1. 项目背景与核心需求
在政府机构和央企的数字化转型过程中,大文件传输一直是困扰IT部门的痛点问题。传统的FTP或HTTP单线程上传方式在面对100GB级别的工程设计图纸、高清影像资料等大文件时,经常出现传输中断、速度缓慢、内存溢出等问题。特别是在信创环境下,还需要兼顾国产化操作系统、数据库和浏览器的兼容性要求。
我们团队在山西某大型IT服务项目中,遇到了以下典型场景:
- 城市规划部门需要定期上传50-100GB的BIM建模文件
- 能源企业需传输井下探测的高清视频(单个文件常超过30GB)
- 政务系统要求文件夹结构完整上传且支持断点续传
这些场景对技术方案提出了四大核心要求:
- 超大文件支持:稳定传输100GB级文件不崩溃
- 断点续传能力:网络中断后可恢复,进度不丢失
- 信创环境适配:兼容国产操作系统和数据库
- 数据安全保障:传输和存储全程加密
2. 技术架构设计
2.1 整体架构方案
我们采用分层解耦的设计思想,将系统划分为以下核心模块:
code复制[前端层]
├─ Vue2/React上传组件(兼容IE8)
└─ 进度管理SDK
[接入层]
├─ Nginx负载均衡
└─ HTTPS双向认证
[服务层]
├─ 分片上传服务(5MB/片)
├─ 元数据管理(MySQL/达梦)
├─ 加密服务(AES+SM4)
└─ 断点续传控制
[存储层]
├─ 阿里云OSS(私有化部署)
└─ 本地文件系统
2.2 关键技术选型
2.2.1 分片上传机制
采用5MB固定分片大小,基于以下考虑:
- 内存占用:IE8最大可用内存约500MB,100GB文件需要约20000个分片
- 网络效率:实测在政务专网环境下,5MB分片可实现最优吞吐量
- 断点恢复:小分片粒度更利于精确恢复
分片上传流程:
- 前端计算文件MD5指纹
- 按5MB切片并编号(0~N)
- 每个分片独立加密传输
- 服务端按编号存储分片
- 全部分片上传完成后触发合并
2.2.2 断点续传实现
关键技术点:
- 进度持久化:使用Redis+MySQL双写策略
- Redis存储实时进度(TTL 7天)
- MySQL持久化最终状态
- 指纹校验:合并前验证所有分片MD5
- 异常处理:
java复制// 分片上传异常处理示例 try { uploadChunk(chunk); } catch (IOException e) { logger.error("分片上传失败", e); // 记录失败分片索引 failRecords.add(chunkIndex); // 3秒后自动重试 Thread.sleep(3000); retryUpload(chunk); }
2.2.3 文件夹结构保持
解决方案:
- 前端递归遍历文件夹,生成树形结构:
javascript复制function scanDirectory(dir) { let tree = { name: dir.name, path: dir.webkitRelativePath, files: [], dirs: [] }; // 递归处理子目录... return tree; } - 服务端按路径映射存储:
code复制/uploads/2024/03/15/{task_id}/ ├─ 项目文档/ │ ├─ 设计方案.pdf │ └─ 预算表.xlsx └─ 影像资料/ └─ 现场照片.jpg
3. 核心代码实现
3.1 前端关键代码
文件分片处理(兼容IE8):
javascript复制// 使用File API切片(IE10+原生支持,IE8通过polyfill)
function createFileChunks(file, chunkSize) {
const chunks = [];
let start = 0;
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size);
// IE8需使用File.slice而非File.prototype.slice
const chunk = file.slice(start, end);
chunks.push({
index: chunks.length,
file: chunk
});
start = end;
}
return chunks;
}
// 分片上传逻辑
async function uploadChunk(taskId, chunk, retry = 3) {
const formData = new FormData();
formData.append('taskId', taskId);
formData.append('chunkIndex', chunk.index);
formData.append('file', chunk.file);
try {
const res = await fetch('/api/upload/chunk', {
method: 'POST',
body: formData
});
if (!res.ok) throw new Error('上传失败');
return true;
} catch (err) {
if (retry > 0) {
return uploadChunk(taskId, chunk, retry - 1);
}
throw err;
}
}
进度本地持久化:
javascript复制// 使用localStorage保存进度(兼容IE8)
function saveProgress(taskId, progress) {
const data = {
timestamp: Date.now(),
progress: progress
};
localStorage.setItem(`upload_${taskId}`, JSON.stringify(data));
}
// 恢复未完成任务
function resumeTasks() {
const tasks = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('upload_')) {
const data = JSON.parse(localStorage.getItem(key));
// 只恢复24小时内的任务
if (Date.now() - data.timestamp < 86400000) {
tasks.push({
taskId: key.replace('upload_', ''),
progress: data.progress
});
}
}
}
return tasks;
}
3.2 后端关键代码
分片接收与合并:
java复制@RestController
@RequestMapping("/api/upload")
public class UploadController {
@PostMapping("/chunk")
public ResponseEntity<?> uploadChunk(
@RequestParam String taskId,
@RequestParam int chunkIndex,
@RequestParam MultipartFile file) {
// 1. 临时存储分片
String tempPath = "/tmp/" + taskId + "/" + chunkIndex;
file.transferTo(new File(tempPath));
// 2. 更新进度
redisTemplate.opsForHash().put(
"upload:" + taskId,
String.valueOf(chunkIndex),
"1");
// 3. 检查是否全部完成
long completed = redisTemplate.opsForHash()
.keys("upload:" + taskId).stream()
.filter(k -> "1".equals(redisTemplate.opsForHash()
.get("upload:" + taskId, k)))
.count();
if (completed == totalChunks) {
mergeChunks(taskId);
}
return ResponseEntity.ok().build();
}
private void mergeChunks(String taskId) throws IOException {
// 合并所有分片...
}
}
国密SM4加密实现:
java复制public class SM4Util {
private static final String ALGORITHM_NAME = "SM4";
private static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
public static byte[] encrypt(byte[] data, byte[] key) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING);
SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
return cipher.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("SM4加密失败", e);
}
}
}
4. 信创环境适配
4.1 国产化组件兼容方案
| 组件类型 | 国产替代方案 | 适配要点 |
|---|---|---|
| 操作系统 | 统信UOS、麒麟OS | 文件路径兼容性处理 |
| 数据库 | 达梦DM8 | 修改SQL方言和JDBC驱动 |
| 浏览器 | 红莲花安全浏览器 | 降级到ES5语法+IE8兼容模式 |
| 加密算法 | 国密SM4 | 替换原AES加密模块 |
| CPU架构 | 龙芯/飞腾 | 编译本地库文件 |
4.2 典型适配代码示例
操作系统路径兼容:
java复制public String getStoragePath() {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("uos") || os.contains("kylin")) {
// 国产系统使用特定存储目录
return "/opt/app/uploads/";
}
return "/data/uploads/";
}
达梦数据库适配:
sql复制-- 原MySQL建表语句
CREATE TABLE upload_progress (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL
);
-- 达梦修改后
CREATE TABLE upload_progress (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
task_id VARCHAR(64) NOT NULL
);
5. 部署与性能优化
5.1 服务器配置建议
| 组件 | 最低配置 | 推荐配置 |
|---|---|---|
| 前端服务器 | 2核4G | 4核8G(Nginx负载均衡) |
| 应用服务器 | 4核8G | 8核16G(SpringBoot集群) |
| 数据库 | 4核8G+100G SSD | 8核32G+RAID10 SSD |
| Redis | 2核4G | 4核8G(持久化开启) |
5.2 关键参数调优
Nginx配置优化:
nginx复制# 文件上传大小限制
client_max_body_size 1024G;
# 超时设置(单位:秒)
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
# 缓冲区优化
client_body_buffer_size 512k;
proxy_buffers 16 512k;
SpringBoot性能调优:
yaml复制server:
tomcat:
max-threads: 200
min-spare-threads: 20
max-connections: 1000
connection-timeout: 600000
servlet:
multipart:
max-file-size: 5MB
max-request-size: 1024GB
6. 实测性能数据
在政务专网环境下(带宽1Gbps)的测试结果:
| 文件大小 | 并发线程数 | 传输时间 | 平均速度 |
|---|---|---|---|
| 1GB | 5 | 25s | 40MB/s |
| 10GB | 10 | 4m12s | 38MB/s |
| 100GB | 20 | 42m | 35MB/s |
断点续传测试:
- 主动中断后恢复上传,进度误差小于0.01%
- 重复断点5次以上仍可正常完成
7. 常见问题解决方案
7.1 分片上传失败
现象:部分分片反复上传失败
排查步骤:
- 检查网络MTU值(建议设为1500)
- 验证防火墙是否拦截大包
- 查看服务端磁盘空间
- 检查文件权限(特别是国产系统)
7.2 国产浏览器兼容问题
典型问题:上传组件无法正常渲染
解决方案:
- 引入ES5-shim polyfill
- 禁用CSS Flex布局
- 使用jQuery替代原生DOM操作
javascript复制// 代替document.getElementById $('#fileInput').on('change', function(e) { // 处理文件选择 });
7.3 大文件合并内存溢出
优化方案:
- 使用NIO文件通道合并:
java复制try (FileChannel dest = FileChannel.open(targetPath, CREATE, WRITE)) { for (File chunk : chunks) { try (FileChannel src = FileChannel.open(chunk.toPath(), READ)) { src.transferTo(0, src.size(), dest); } } } - 设置JVM参数:
code复制-XX:+UseG1GC -Xms4g -Xmx8g
8. 项目演进方向
- 智能分片:根据网络状况动态调整分片大小
- P2P加速:在内部网络实现节点间传输
- 增量同步:基于文件差异的增量上传
- 跨域协作:支持多地政务云间文件同步
在实际部署中,这套方案已成功应用于3个省级政务云平台,累计传输文件超过500TB。其中某能源企业的井下探测视频传输项目,将原本需要8小时的传输过程缩短到40分钟,且再未出现传输中断情况。