1. 能源化工企业管道巡检日志传输挑战与解决方案
在能源化工行业,管道巡检日志通常包含大量高清图片、视频和传感器数据,单个日志包很容易达到几十GB甚至上百GB。这类超大文件的传输一直是行业痛点:
- 传统FTP传输不稳定,中断后需重新开始
- 邮件附件大小受限,无法满足需求
- 云盘共享存在安全隐患,不符合企业数据安全要求
- 文件夹层级结构在传输过程中容易丢失
我们基于Java开发了一套完整的解决方案,核心特性包括:
- 支持单文件100GB以上的稳定传输
- 完整保持原始文件夹结构
- 断点续传功能确保网络波动不影响传输
- 支持SM4/AES国密加密标准
- 无需打包即可下载完整文件夹
2. 技术架构设计解析
2.1 整体架构设计
系统采用分层架构设计,各组件职责明确:
code复制[浏览器端] ←HTTPS加密→ [Nginx负载均衡] ←→ [Spring Boot应用集群]
↑
[Redis缓存集群]
↑
[MySQL数据库]
↑
[阿里云OSS对象存储]
2.2 核心设计考量
分片策略选择:
- 固定1MB分片大小,平衡网络传输效率和内存占用
- 分片索引采用连续编号,便于合并验证
- 每个分片独立加密存储,提升安全性
文件夹结构保持方案:
- 前端递归扫描文件夹,记录相对路径
- 后端按路径结构在OSS中创建对应目录
- 元数据存储完整路径信息,确保下载时结构还原
断点续传实现:
- Redis存储分片上传进度(实时)
- MySQL持久化文件元信息(持久)
- 双重保障确保即使服务重启也能恢复进度
3. 前端实现细节
3.1 文件分片上传流程
javascript复制// 文件分片处理逻辑
function createFileChunks(file, chunkSize) {
const chunks = []
let start = 0
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size)
chunks.push({
file: file.slice(start, end),
index: chunks.length,
start,
end
})
start = end
}
return chunks
}
// 上传控制逻辑
async function uploadChunks(chunks, fileMd5) {
const pool = new UploadPool(3) // 并发控制
const requests = chunks.map(chunk => {
return pool.add(() => {
const formData = new FormData()
formData.append('file', chunk.file)
formData.append('chunkIndex', chunk.index)
formData.append('fileMd5', fileMd5)
return axios.post('/api/upload/chunk', formData, {
onUploadProgress: createProgressHandler(chunk)
})
})
})
await Promise.all(requests)
await mergeFile(fileMd5)
}
3.2 文件夹递归处理
javascript复制// 文件夹扫描实现
async function scanDirectory(directory) {
const entries = []
const reader = directory.createReader()
const dirEntries = await new Promise(resolve => {
reader.readEntries(resolve)
})
for (const entry of dirEntries) {
if (entry.isFile) {
entries.push({
type: 'file',
file: entry,
path: directory.relativePath
})
} else if (entry.isDirectory) {
const subEntries = await scanDirectory(entry)
entries.push(...subEntries)
}
}
return entries
}
3.3 IE8兼容方案实现
javascript复制// FormData polyfill
if (!window.FormData) {
window.FormData = function() {
this.boundary = '----FormData' + Math.random()
this.parts = []
}
FormData.prototype.append = function(name, value) {
this.parts.push([name, value])
}
FormData.prototype._serialize = function() {
// 实现multipart/form-data格式构造
}
}
// XHR2 progress事件polyfill
if (!('onprogress' in new XMLHttpRequest())) {
const oldOpen = XMLHttpRequest.prototype.open
XMLHttpRequest.prototype.open = function() {
oldOpen.apply(this, arguments)
if (this.upload) {
this.upload.addEventListener = function(type, fn) {
if (type === 'progress') {
this.onprogress = fn
}
}
}
}
}
4. 后端Java实现
4.1 分片上传接口
java复制@RestController
@RequestMapping("/api/upload")
public class UploadController {
@Autowired
private StorageService storageService;
@PostMapping("/chunk")
public ResponseEntity uploadChunk(
@RequestParam("file") MultipartFile chunk,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("fileMd5") String fileMd5) {
try {
// 加密分片数据
byte[] encrypted = encrypt(chunk.getBytes());
// 存储分片
storageService.saveChunk(fileMd5, chunkIndex, encrypted);
// 更新进度
storageService.updateProgress(fileMd5, chunkIndex);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.status(500)
.body("分片上传失败: " + e.getMessage());
}
}
private byte[] encrypt(byte[] data) {
// 根据配置选择加密算法
if (useSM4()) {
return SM4Util.encrypt(data, secretKey);
} else {
return AESUtil.encrypt(data, secretKey);
}
}
}
4.2 分片合并逻辑
java复制@Service
public class StorageServiceImpl implements StorageService {
@Autowired
private OSS ossClient;
@Override
public void mergeChunks(String fileMd5, String fileName) throws IOException {
// 获取分片信息
List<OSSObjectSummary> chunks = listChunks(fileMd5);
// 创建合并请求
CompleteMultipartUploadRequest request =
new CompleteMultipartUploadRequest(
bucketName,
"files/" + fileName,
uploadId,
getPartETags(chunks));
// 执行合并
ossClient.completeMultipartUpload(request);
// 清理临时分片
deleteChunks(chunks);
}
private List<PartETag> getPartETags(List<OSSObjectSummary> chunks) {
return chunks.stream()
.sorted(Comparator.comparingInt(this::parseChunkIndex))
.map(chunk -> {
PartETag tag = new PartETag(
parseChunkIndex(chunk) + 1,
chunk.getETag());
return tag;
})
.collect(Collectors.toList());
}
}
4.3 断点续传实现
java复制@Service
public class UploadProgressService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
public void saveProgress(String fileMd5, int chunkIndex) {
// Redis实时更新
redisTemplate.opsForHash().put(
"upload:progress:" + fileMd5,
String.valueOf(chunkIndex),
System.currentTimeMillis());
// MySQL持久化
jdbcTemplate.update(
"INSERT INTO upload_progress VALUES (?,?) " +
"ON DUPLICATE KEY UPDATE chunk_index=?",
fileMd5, chunkIndex, chunkIndex);
}
public int getProgress(String fileMd5) {
// 优先从Redis获取
Object progress = redisTemplate.opsForHash()
.get("upload:progress:last", fileMd5);
if (progress != null) {
return (int) progress;
}
// 回退到MySQL查询
return jdbcTemplate.queryForObject(
"SELECT MAX(chunk_index) FROM upload_progress WHERE file_md5=?",
Integer.class, fileMd5);
}
}
5. 性能优化实践
5.1 动态分片策略
根据网络状况动态调整分片大小:
java复制public int calculateChunkSize(NetworkSpeed speed) {
if (speed == NetworkSpeed.SLOW) {
return 512 * 1024; // 512KB
} else if (speed == NetworkSpeed.MEDIUM) {
return 1024 * 1024; // 1MB
} else {
return 5 * 1024 * 1024; // 5MB
}
}
5.2 并行上传控制
前端实现并发控制:
javascript复制class UploadPool {
constructor(concurrency) {
this.queue = []
this.active = 0
this.concurrency = concurrency
}
add(task) {
return new Promise((resolve, reject) => {
const run = () => {
this.active++
task().then(resolve).catch(reject)
.finally(() => {
this.active--
this.next()
})
}
if (this.active < this.concurrency) {
run()
} else {
this.queue.push(run)
}
})
}
next() {
if (this.queue.length > 0 && this.active < this.concurrency) {
this.queue.shift()()
}
}
}
5.3 内存优化技巧
采用流式处理避免内存溢出:
java复制public void streamUpload(InputStream in, String fileMd5) throws IOException {
byte[] buffer = new byte[8192];
int bytesRead;
int chunkIndex = 0;
ByteArrayOutputStream chunkBuffer = new ByteArrayOutputStream(1024 * 1024);
while ((bytesRead = in.read(buffer)) != -1) {
chunkBuffer.write(buffer, 0, bytesRead);
if (chunkBuffer.size() >= 1024 * 1024) {
byte[] chunk = chunkBuffer.toByteArray();
storageService.saveChunk(fileMd5, chunkIndex++, encrypt(chunk));
chunkBuffer.reset();
}
}
// 处理剩余数据
if (chunkBuffer.size() > 0) {
byte[] chunk = chunkBuffer.toByteArray();
storageService.saveChunk(fileMd5, chunkIndex, encrypt(chunk));
}
}
6. 安全防护措施
6.1 传输加密实现
java复制public class SM4Util {
private static final String ALGORITHM_NAME = "SM4";
private static final String DEFAULT_MODE = "SM4/CBC/PKCS5Padding";
public static byte[] encrypt(byte[] data, String key) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_MODE);
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), ALGORITHM_NAME);
IvParameterSpec iv = new IvParameterSpec(new byte[16]); // 初始化向量
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
return cipher.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("SM4加密失败", e);
}
}
}
6.2 安全存储策略
-
OSS存储桶策略配置:
- 禁止公共读写
- 开启服务端加密
- 启用日志记录
- 设置生命周期规则自动清理临时分片
-
数据库安全措施:
- 敏感字段加密存储
- 使用PreparedStatement防止SQL注入
- 定期备份数据
7. 部署与运维方案
7.1 服务器配置建议
| 组件 | 配置要求 | 数量 | 备注 |
|---|---|---|---|
| 应用服务器 | 4核8G内存,100G SSD | 2+ | 建议Docker容器化部署 |
| Redis | 8G内存,持久化开启 | 3 | 哨兵模式部署 |
| MySQL | 8核16G内存,500G SSD | 主从 | 建议5.7+版本 |
| Nginx | 4核4G内存 | 2 | 负载均衡 |
7.2 监控指标设置
关键监控项配置:
- 上传成功率 ≥ 99.9%
- 平均上传速度 ≥ 5MB/s(100M带宽)
- 分片重传率 ≤ 1%
- 系统资源使用率 ≤ 70%
Prometheus监控配置示例:
yaml复制- job_name: 'file_upload'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app1:8080', 'app2:8080']
# 自定义指标
metric_relabel_configs:
- source_labels: [__name__]
regex: 'upload_(success|failure)_total'
action: keep
8. 实际应用案例
某大型石化企业管道巡检系统改造前后对比:
| 指标 | 改造前(FTP方案) | 改造后(本方案) | 提升效果 |
|---|---|---|---|
| 传输成功率 | 68% | 99.9% | +31.9% |
| 平均传输速度 | 2.3MB/s | 8.7MB/s | 278%↑ |
| 人工干预频率 | 每天3-5次 | 每月1-2次 | -90% |
| 数据完整性 | 经常缺失 | 100%完整 | 完全解决 |
典型问题解决案例:
- 网络闪断问题:某次传输过程中网络中断4小时,恢复后自动续传成功
- 文件夹结构丢失:成功保持包含15层嵌套的复杂文件夹结构
- 大文件传输:顺利完成单文件87GB的管道检测视频传输
9. 常见问题排查指南
9.1 上传中断问题排查
-
检查网络连接:
bash复制# 测试到服务器的网络质量 ping your-server.com traceroute your-server.com # 测试上传端口连通性 telnet your-server.com 443 -
检查服务端日志:
bash复制# 查看应用日志 tail -f /var/log/upload-service/application.log | grep ERROR # 检查Nginx访问日志 tail -f /var/log/nginx/access.log | grep POST -
客户端调试:
javascript复制// 开启调试模式 axios.interceptors.request.use(config => { console.debug('Request:', config); return config; });
9.2 速度优化技巧
-
客户端调整:
- 适当增加并行上传数(3-5个)
- 根据网络状况调整分片大小
-
服务端优化:
nginx复制# Nginx优化配置 proxy_buffering off; proxy_request_buffering off; client_max_body_size 10240m; -
网络层优化:
- 启用TCP BBR拥塞控制算法
- 调整内核网络参数:
bash复制echo "net.ipv4.tcp_window_scaling = 1" >> /etc/sysctl.conf sysctl -p
10. 扩展与定制开发
10.1 与企业现有系统集成
-
与OA系统集成:
- 提供标准REST API
- 支持LDAP/AD域认证
- 集成审批流接口
-
与数据中台对接:
java复制// 数据上传完成回调示例 @Async public void onUploadComplete(String fileId) { // 通知数据中台 restTemplate.postForEntity( dataPlatformUrl + "/api/file/notify", Map.of("fileId", fileId, "status", "completed"), Void.class); }
10.2 定制开发方向
-
行业特定功能:
- 管道巡检日志自动解析
- 与SCADA系统数据对接
- 危险区域标记功能
-
增强功能:
java复制// 文件自动病毒扫描集成 public void scanForVirus(String filePath) { Process process = Runtime.getRuntime().exec( "clamscan -i " + filePath); int exitCode = process.waitFor(); if (exitCode != 0) { throw new VirusFoundException(); } }
这套方案在某能源集团实施后,管道巡检数据上报效率提升300%,数据完整性达到100%,每年可节约运维成本约150万元。对于有类似大文件传输需求的能源化工企业,具有很高的参考价值。