Files
senmesh-zc/backend/docs/传输链路性能优化方案.md
KodouWZF f817bf0701 docs(传输): 跨网传输链路性能优化方案与现场验证方法
现场反馈一页文件要 2 分钟才进内网。实测组包 19.6MB 耗时 45.9s,
其中 SQL 仅 57ms,其余全在文件 I/O,折算 2.33 秒/MB,比正常 deflate
慢 50-100 倍,根因是 512B/8KB 小缓冲在 bind mount 上放大 syscall 次数。

沉淀耗时归因模型、不改代码即可切分端到端 6 段的现场测量方法、
6 项优化的改法与验收标准,以及本次明确不做的事及理由。
2026-11-03 21:57:15 +08:00

411 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 跨网传输链路性能优化方案
> 版本:v1.0(2026-11-03)
> 适用后端:s-file(alita-bjy-uygur)
> 状态:待评审 → 待实施 → 待现场验证
> 关联代码:`TransferUserFileServiceImpl`(发送/组包)、`TransferFileMonitorServiceImpl`(接收/解包)、`TransferFileMonitorJob`(调度)、`FileServiceImpl` / `LocalFileStorage`(物理文件读写)
---
## 1. 背景与现象
现场反馈:**一页文件也要 2 分钟才能传到内网**,希望找到瓶颈。
自有服务器复现:**单个文件从点击发送到组包完成需要 20–30 秒**。
本文只解决「**慢**」,不涉及「传失败/传丢」类问题(那是另一条线,不在此文档范围内)。
---
## 2. 实测数据(本地环境,非推算)
数据来源:`sfile-iw-app` 容器日志中 `ControllerLogAspect` 输出的 `useTime` 字段(单位毫秒),以及 `TransferUserFileServiceImpl` 的「传输文件打包完成」日志时间戳。
### 2.1 单文件组包耗时
| 请求样本 | 内容 | useTime |
|---|---|---|
| userFileId=23 | 单个 txt,2.8 KB | 159 ms |
| userFileId=18 | 单个小文件 | 127 ms |
| userFileId=17 | 单个小文件 | 126 ms |
| userFileId=10 | 单个小文件 | 167 ms |
| userFileId=12 | 单个小文件 | 332 ms |
| userFileId=13 | 单个小文件 | 543 ms |
| userFileId=31 | 文件夹,9 个文件,合计约 19.6 MB | **45,934 ms** |
**结论 1:小文件的组包本身就是几百毫秒,不慢。**
### 2.2 46 秒那次的分段(关键证据)
```
12:22:02.305 复制用户文件时跳过排除文件: id=41 ...
12:22:02.362 复制用户文件时跳过排除文件: id=32 ... ← 到此所有 SQL 执行完毕
12:22:02.362 ───────────── 45.7 秒无任何日志 ─────────────
12:22:48.110 传输文件打包完成: /data/transfer/send/1_45.zip
```
| 分段 | 耗时 | 占比 |
|---|---|---|
| 安全检测判定 + 建传输文件夹 + 复制文件记录(全部 SQL) | **57 ms** | 0.1% |
| `packTransferFiles`:读源文件 + 压缩 + 写摆渡目录 | **45,873 ms** | 99.9% |
**结论 2:耗时几乎全部在文件 I/O,与 SQL、索引、数据量无关。**
### 2.3 折算成单位成本
19.6 MB / 45.87 s = **2.33 秒/MB** = 0.43 MB/s。
正常 deflate 压缩速率是 20–40 MB/s,实测慢了约 **50–100 倍**,因此瓶颈不是 CPU,是每次读写的开销。
---
## 3. 耗时归因模型(可用来预测任意大小文件)
### 3.1 代码里的两处小缓冲
| 位置 | 缓冲大小 | 说明 |
|---|---|---|
| `TransferUserFileServiceImpl:139` | 读源文件 `byte[8192]` | 每 8 KB 一次 read() |
| `TransferUserFileServiceImpl:125` | `new ZipOutputStream(...)` | JDK8 中 `ZipOutputStream` 的内部 deflate 输出缓冲**硬编码 512 字节**,无法通过构造参数调整 → 每 512 B 一次 write() |
### 3.2 为什么小缓冲在这里是致命的
`/data/upload`(源文件)和 `/data/transfer/send`(摆渡目录)**都是宿主机 bind mount**:
- `deploy/offline/docker-compose.yml:92,95,96`
- `deploy/docker-compose.yml:53,55,56`
bind mount 若落在 Docker Desktop 文件共享(FUSE/9p)、NFS、SMB,或**光闸厂商的监控目录驱动**上,每次 syscall 都是一次跨层往返,成本在毫秒级;落在本地 ext4/xfs 上则只要几微秒。**同一段代码在两种挂载上的耗时可以差 100 倍。**
### 3.3 模型与实测吻合验证
```
组包耗时 ≈ 文件大小 × (1/512 + 1/8192) 次 syscall/字节 × 单次 syscall 耗时 + 压缩 CPU 时间
```
以实测反推单次 syscall 耗时:
- 每 MB 的 syscall 次数 = 1,048,576/512 + 1,048,576/8192 = 2048 + 128 = **2176 次**
- 实测 2.33 秒/MB,扣除压缩 CPU(约 0.04 秒/MB)→ 2.29 s / 2176 ≈ **1.05 ms/次**
这个数值与 FUSE/网络挂载的典型往返延迟一致,模型成立。
> **注意:1.05 ms/次 是本地 Windows Docker Desktop 环境的系数。**
> 客户现场(Linux + 光闸目录)必须用第 4 节的方法重新实测该系数,收益预测按现场系数换算。
### 3.4 按模型预测的组包耗时
| 文件大小 | 现状(2.33 s/MB) | 改 64 KB 缓冲后(约 0.075 s/MB) | 提升 |
|---|---|---|---|
| 100 KB(一页文档) | 0.23 s | 0.008 s | 29× |
| 10 MB | 23 s | 0.75 s | 31× |
| **50 MB** | **117 s(约 2 分钟)** | **3.8 s** | **31×** |
| 200 MB | 466 s(7.8 分钟) | 15 s | 31× |
| 500 MB | 1165 s(19 分钟) | 38 s | 31× |
| 1 GB | 2330 s(39 分钟) | 76 s | 31× |
改后系数 0.075 s/MB 的构成:I/O 约 0.035 s/MB(每 MB 只剩 32 次 syscall)+ 压缩 CPU 约 0.04 s/MB。
> **50 MB 的回答:现状约 2 分钟,改完约 4 秒。**
> 若再叠加 5.4 节(已压缩格式不二次压缩),50 MB 可降到约 2 秒。
>
> **重要:500 MB 以上的文件现状组包需要 19 分钟,而这段时间是占用在一个 HTTP 请求 + 一个数据库事务里的**,浏览器/nginx 早就超时了。这解释了大文件传输在界面上"点了没反应"的现象。
---
## 4. 现场分段测量方法(实施前必须先测,改后同样用它验收)
不用改代码,只用文件时间戳和现成日志,把端到端时间切成 6 段。
| # | 取数位置 | 含义 | 相邻差值的意义 |
|---|---|---|---|
| T1 | 外网 send 目录 `xxx.zip` 的 mtime | 组包完成 | — |
| T2 | 内网 receive 目录 `xxx.zip` 的 mtime | 光闸送达内网 | **T2−T1 = 去程光闸耗时** |
| T3 | 内网日志 `开始解包文件: xxx.zip` | 监听任务发现它 | **T3−T2 = 内网轮询等待(0–60 s)** |
| T4 | 内网日志 `文件解包成功: xxx.zip` | 解包入库完成 | **T4−T3 = 解包入库耗时** |
| T5 | 内网 send 目录 `xxx.zip.finish` 的 mtime | 回执生成 | — |
| T6 | 外网日志 `处理完成标记文件: xxx.zip.finish` | 外网翻转状态 | **T6−T5 = 回程光闸 + 外网轮询等待** |
取数命令:
```bash
# T1 / T5
ls -la --time-style=full-iso "$GATE_SEND_DIR" | grep <folderId>
# T2
ls -la --time-style=full-iso "$GATE_RECV_DIR" | grep <folderId>
# T3 / T4 / T6
docker logs --since 30m <app容器> 2>&1 | grep -E "开始解包文件|文件解包成功|处理完成标记文件|传输文件打包完成"
```
### 4.1 一页文件 2 分钟的预期构成(待现场确认)
| 分段 | 预计耗时 | 性质 |
|---|---|---|
| 外网组包 | < 0.5 s | 处理 |
| 去程光闸搬运 | **未知,需实测** | 外部设备 |
| 内网轮询等待 | 平均 30 s,最坏 60 s | **纯等待** |
| 内网解包入库 | < 1 s | 处理 |
| 回程光闸搬运 | **未知,需实测** | 外部设备 |
| 外网轮询等待 | 平均 30 s,最坏 60 s | **纯等待** |
| **合计** | **约 60–120 s + 光闸两次周期** | — |
**真正干活的时间不到 2 秒,其余全是等待。** 两个 60 秒轮询窗口平均就贡献 60 秒、最坏 120 秒——这已经足以解释"一页文件 2 分钟",光闸周期还没算进去。
### 4.2 测量单次 syscall 系数(用来校准第 3.4 节的预测)
传两个大小差异明显的文件(例如 10 MB 和 50 MB),各记一次组包耗时,则:
```
每 MB 成本 = (耗时2 − 耗时1) / (大小2 − 大小1)
单次 syscall 耗时 = 每 MB 成本 / 2176
```
拿到现场系数后,把第 3.4 节的表格按现场系数重算,即为改造后的预期收益。
---
## 5. 优化项
### 5.1 目录监听周期:60 秒 → 5 秒【收益最大,改动最小】
**现状**:`TransferFileMonitorJob:29` 的 cron 为 `${file.transfer.monitor.cron:0 */1 * * * ?}`,即只在每分钟第 0 秒触发。文件落地时刻随机 → 平均白等 30 秒,最坏 60 秒。发送端和接收端两侧都有这个等待,合计平均 60 秒、最坏 120 秒。
**改法**:
- 配置项 `FILE_TRANSFER_MONITOR_CRON` 改为 `0/5 * * * * ?`(5 秒)
- 空目录时一轮的开销极小(两次 `Files.list` + 过滤),5 秒一次不会造成压力
- 若现场希望更激进,可评估改为 `java.nio.file.WatchService` 事件驱动(文件落地即触发),但 bind mount 上的 inotify 事件可靠性依赖挂载类型(FUSE/NFS 上可能收不到事件),**不建议一期就上**,先用 5 秒轮询这个确定有效的方案
**预期收益**:两侧等待从平均 60 s 降到平均 5 s,**端到端直接省掉约 55 秒**。
**风险**:低。唯一要注意的是必须保证同一时刻只有一轮在跑(见 5.2 的互斥要求)。
---
### 5.2 单轮处理上限:从「个数」改为「时间预算」
**现状**:`TransferFileMonitorServiceImpl:87-99` 与 `:109-120`
```java
.sorted(Comparator.comparing(p -> Files.getLastModifiedTime(p))) // 最老的优先
.limit(10) // 每轮最多 10 个
```
`.finish` 标记文件同样是 `limit(10)`。
**问题**:
1. **吞吐被硬顶死**:10 个/分钟 = 600 个/小时。300 人站点若同一分钟内有 25 个传输,第 11–20 个要等下一分钟、第 21–25 个等第三分钟,**高峰期排队直接叠加分钟级延迟**。
2. **按个数限流本身不合理**:包的大小跨度极大(几十 KB 到几百 MB)。10 个小文件可能 1 秒处理完(限制毫无必要),10 个 500 MB 的包可能跑十几分钟(远超调度周期,还会把下一轮挤掉)。**个数不能代表工作量,时间才能。**
3. **失败的处理不占用预算**:一个包处理失败时既不删除也不改名,它的 mtime 最老,因此每一轮都排在第一位、每一轮都消耗一次处理时间。轮次预算被无效工作占用的比例会随时间上升。
**设计方案(四条)**:
#### (1) 上限语义:个数 → 时间预算
| 配置项 | 默认值 | 含义 |
|---|---|---|
| `file.transfer.monitor.round-budget-seconds` | 4 | 单轮处理的时间预算,超时就结束本轮,剩余留给下一轮 |
| `file.transfer.monitor.round-max-files` | 500 | 兜底个数上限,防止极端场景单轮过长 |
处理循环改为:
```
deadline = now + round-budget-seconds
for (每个按 mtime 升序的候选包) {
if (now > deadline || 已处理数 >= round-max-files) break; // 预算用尽,本轮结束
processZipFile(包);
}
```
**为什么预算默认 4 秒**:与 5.1 的 5 秒周期配套,留 1 秒余量,保证上一轮结束后下一轮能准时启动,不会互相挤压。
#### (2) 认领机制:先原子改名,再处理
处理前把 `x.zip` 原子改名为 `x.zip.processing`,改名成功才算认领成功:
| 结果 | 动作 |
|---|---|
| 认领成功 + 处理成功 | 删除 `.processing`(现有行为) |
| 认领成功 + 处理失败 | 改名 `x.zip.error`,移出扫描范围并告警,不再消耗后续轮次预算 |
| 认领失败(改名报错,说明被别的线程/实例拿走) | 跳过 |
| 进程重启后发现遗留 `.processing` | 启动时清理:改回 `.zip` 重新排队,或直接标 `.error` |
作用:支持并发与多实例部署时不重复处理;失败包不再占用时间预算;`.error` 保留现场便于排查。
> 注:接收端已有的 `.tmp` 过滤(`:90`)说明原设计就有"临时名"的概念,这里与之保持一致的命名风格。
#### (3) 有限并发解包
| 配置项 | 默认值 | 含义 |
|---|---|---|
| `file.transfer.monitor.concurrency` | 2 | 并发解包线程数 |
- 仍按 mtime 升序认领,保证先到先处理(公平性不变)
- **并发度不建议超过 4**:解包同时要写数据库(Druid `max-active: 50`)、写文件存储、提交安全检测,并发过高会互相抢连接和磁盘带宽,收益递减且放大 I/O 抖动
- 若并发度 > 1,`scanAndProcess` 必须加互斥(Redisson `tryLock`,拿不到锁直接返回),避免调度周期短于单轮耗时时出现两轮重叠
#### (4) 积压可观测
每轮结束打印一行结构化日志:
```
传输监听本轮结束: 处理=8(成功=7,失败=1), 耗时=3120ms, 预算=4000ms, 目录剩余待处理=23
```
`目录剩余待处理` 持续大于阈值(建议 50)即为积压告警信号,是容量规划的直接依据。
#### `.finish` 标记文件的处理
`.finish` 只做一次状态更新,极轻。建议:
- 不受 `round-max-files` 限制(或单独放宽到 500)
- 与 zip 处理**共享同一个时间预算**,且**优先处理 `.finish`**(先让发送端尽早看到"已完成",用户体验上收益直接)
**预期收益**:高峰期吞吐从 600 个/小时提升到「预算内能处理多少就多少」;按 5.3 改造后单包耗时估算,4 秒预算 + 2 并发 ≈ 每轮可处理数十个小包,**吞吐提升一个数量级以上**;同时消除排队叠加的分钟级延迟。
---
### 5.3 读写缓冲区:512 B / 8 KB → 64 KB【大文件收益最大】
**改法**(4 处):
| 位置 | 现状 | 改为 |
|---|---|---|
| `TransferUserFileServiceImpl:125` | `new ZipOutputStream(new FileOutputStream(f))` | `new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(f), 65536))` |
| `TransferUserFileServiceImpl:139` | `byte[] buffer = new byte[8192]` | `new byte[65536]`;源流再用 `BufferedInputStream` 包一层 |
| `TransferFileMonitorServiceImpl:412` | `new ZipInputStream(new FileInputStream(f))` | `new ZipInputStream(new BufferedInputStream(new FileInputStream(f), 65536))` |
| `TransferFileMonitorServiceImpl:447` | `byte[] buffer = new byte[8192]` | `new byte[65536]` |
| `LocalFileStorage:25-26` | `IOUtils.copy(is, os)`(commons-io 默认 8 KB 缓冲) | `IOUtils.copy(is, os, 65536)`,或 `os` 包 `BufferedOutputStream(65536)` |
**关键点**:`ZipOutputStream` 的 512 字节缓冲在 JDK8 里是硬编码的(`ZipOutputStream extends DeflaterOutputStream`,构造时传入固定 BUFSIZE),**不能通过构造参数改**,所以必须用 `BufferedOutputStream` 在外层兜住——512 字节的小写先进内存缓冲,凑满 64 KB 才落一次盘。
**预期收益**:syscall 次数降为原来的 **1/68**(2176 次/MB → 32 次/MB)。按第 3.4 节模型,50 MB 组包从约 117 秒降到约 3.8 秒。
**风险**:极低。仅增加约 128 KB/流的内存占用(64 KB 读 + 64 KB 写)。需实测确认 `BufferedOutputStream` 生效(对比改前改后同一样本耗时)。
**注意事项(务必同时处理)**:解包侧把整个文件读进堆内 `ByteArrayOutputStream`(`TransferFileMonitorServiceImpl:446-451`),随后 `FileServiceImpl:374` 的 `file.getBytes()` 再拷一份、MD5 计算再一份 → **峰值堆占用约 2–3 倍单文件大小**。`-Xmx3g` 下 500 MB 以上的单文件有 OOM 风险。本项优化让 I/O 变快后,大文件更容易走到这一步,因此建议同时加一道**单文件大小上限校验**(超过阈值直接标 `.error` 并告警,而不是尝试解包后 OOM),或后续改为流式落盘。
---
### 5.4 已压缩格式不再二次压缩
**现状**:`ZipOutputStream` 默认 `Deflater.DEFAULT_COMPRESSION`(level 6),对 pptx / docx / xlsx / zip / rar / 7z / jpg / png / mp4 这类**本身已压缩**的格式,压缩率接近 0,但 CPU 照样全量跑一遍。
**改法**:按文件后缀判断,命中已压缩格式清单时用 `ZipEntry.setMethod(ZipEntry.STORED)`(原样存储,不压缩)。
> `STORED` 要求预先提供 entry 的 size 和 CRC-32。组包时源文件大小已知(`xs_file_record.file_size`),CRC 需在写入前算一遍(一次顺序读,成本远低于 level 6 压缩)。若不想引入两遍读,退一步用 `zos.setLevel(Deflater.NO_COMPRESSION)`(level 0),仍是 DEFLATE 存储块、压缩率同样接近 0,但 CPU 开销近乎为零,且无需预先提供 size/CRC。**推荐用 level 0 这个简单方案。**
**预期收益**:对 pptx/zip/mp4 类文件,去掉约 0.04 s/MB 的压缩 CPU;50 MB 样本约省 2 秒,1 GB 约省 40 秒。对文本/日志类文件不适用(应保持 level 6,压缩率收益大)。
**风险**:低。level 0 产出的仍是合法 zip,接收端解包逻辑无需改动。**注意包体积会变大**(对已压缩格式几乎无差别,但对可压缩格式若误判会明显变大),因此后缀清单要保守,只收录确定已压缩的格式。
---
### 5.5 `xs_file_record.md5_code` 加索引
**现状**:`FileServiceImpl:374-375` 在每次文件入库时做 MD5 去重查询:
```java
md5Code = DigestUtils.md5Hex(file.getBytes()).toUpperCase();
fileRecord = fileRecordService.find(FileRecord.builder().md5Code(md5Code).build());
```
而 `xs_file_record` **只有 `idx_file_code`,`md5_code` 上没有索引**(`deploy/offline/sql/01-init-table.sql:1499`、`backend/db/init-table-postgres-full.sql:1499`)→ 每个文件入库都是一次全表扫描。
**改法**(按 AGENTS.md 要求,init 与 upgrade 双写,且三个库版本都要覆盖):
```sql
CREATE INDEX idx_file_record_md5_code ON xs_file_record(md5_code);
```
- `backend/db/init-table.sql`(MySQL 版)
- `backend/db/init-table-postgres-full.sql`(PG 版)
- 新建 `backend/db/upgrade-20261103-transfer-perf.sql`(增量,供已上线环境执行)
- 同步到 `deploy/offline/sql/01-init-table.sql`(交付包用的合并版)
- 生产是 **DM8**,该语法在 DM8 通用;若现场数据量已较大,安排在业务低峰执行
**预期收益**:解包 N 个文件时的 N 次全表扫变成 N 次索引查找。数据量小(本地 23 行)时收益不可见;300 人规模下 `xs_file_record` 达数万至数十万行时,**单文件入库可省几十到几百毫秒,100 个文件的包可省数秒到数十秒**。
**风险**:极低。索引列 `varchar(64)`,空间成本可忽略;写入时多维护一个索引,单次 INSERT 增加的成本在微秒级。
---
### 5.6 (可选,二期)超大文件组包异步化
**现状**:`TransferUserFileServiceImpl.create()` 带 `@Transactional`,`packTransferFiles` 在 HTTP 请求线程内同步执行(`:59,88-94`)。
**问题**:完成 5.3/5.4 后,1 GB 组包约需 40–76 秒,仍然超过浏览器/nginx 的默认读超时(60 秒);且整个过程占用一条 Druid 连接和一个数据库事务。
**改法(二期评估)**:`create()` 只落一条传输任务记录并立即返回任务 ID,组包交给异步线程池执行,前端轮询任务状态。需要新增任务表 + 状态机 + 前端进度展示,改动面较大。
**判断标准**:若现场单文件普遍 ≤ 200 MB(改造后组包 ≤ 15 秒),**本项可以不做**;若存在 500 MB 以上的常态传输,则必须做。
---
## 6. 本次明确不做的事(附理由)
| 项 | 为什么不做 |
|---|---|
| `x_user_file` 补索引 | 实测整条链路的全部 SQL 只占 **57 ms**(见 2.2),不是瓶颈。等 300 人规模真实数据量上来后,用第 4 节的方法重测再决定 |
| `path LIKE '%...%'` 改前缀匹配 | 同上,属于数据量增长后的预防性优化,当前无收益 |
| 调度线程池扩容 | 客户环境未开启文件安全检测,`pollPendingScans` 首行即返回,不存在轮询任务抢占调度线程的问题。**若后续现场开启 MeshSafe,此项需重新评估**(`MeshSafeScanJob` 每 10 秒一轮、`listScanning()` 无条数上限、单次 HTTP 超时 120 秒,而 Spring 默认调度线程池只有 1 条线程,两者会互相阻塞) |
| 传输副本(rootType 2/3)归档清理 | 属于数据治理,与本次"慢"无关,单独立项 |
---
## 7. 验证清单
### 7.1 改造前基线(必须先采集)
- [ ] 按第 4 节采集一次完整的 T1–T6,记录 6 个分段耗时
- [ ] 按 4.2 测出现场的「每 MB 组包成本」与「单次 syscall 耗时」系数
- [ ] 记录测试样本:文件个数、总大小、格式
- [ ] 记录 `x_user_file` / `xs_file_record` 当前行数
### 7.2 单项验证
| 优化项 | 验证方法 | 通过标准 |
|---|---|---|
| 5.1 监听周期 | 传一个文件,看 T3−T2 | 从平均 30 s 降到 ≤ 5 s |
| 5.2 时间预算 | 一次性提交 30 个传输,看日志中每轮"处理=N"与"目录剩余" | 30 个在 2 轮内处理完;剩余数单调下降;无重复处理(同一包不出现两次"开始解包") |
| 5.2 认领机制 | 处理中 `kill -9` 容器后重启 | 遗留 `.processing` 被正确清理;无重复入库 |
| 5.2 失败让位 | 人为放一个损坏 zip 进 receive 目录 | 一轮后被改名 `.error`,后续轮次不再处理它,其它包不受影响 |
| 5.3 缓冲区 | 用同一个 19.6 MB 样本对比改前改后的组包耗时 | 45.9 s → ≤ 3 s(本地系数下) |
| 5.4 level 0 | 传一个 50 MB 的 pptx,对比 5.3 单独改造后的耗时 | 再降约 2 s;产出的 zip 能被接收端正常解包 |
| 5.5 md5 索引 | `EXPLAIN` 去重查询;解包一个 100 文件的包对比耗时 | 执行计划走 Index Scan;解包耗时明显下降 |
### 7.3 端到端验收
- [ ] 一页文件(≤ 100 KB)端到端:从点击发送到内网可见,**目标 ≤ 15 秒**(不含光闸搬运时间则 ≤ 5 秒)
- [ ] 一页文件端到端:到外网状态翻转为"正常",**目标 ≤ 光闸两次周期 + 15 秒**
- [ ] 50 MB 单文件:组包 ≤ 5 秒,端到端 ≤ 光闸周期 + 20 秒
- [ ] 300 并发峰值模拟(可用 30 个并发代替按比例外推):无排队叠加,无积压持续增长
- [ ] 回归:内穿外、外传内两个方向;单文件、文件夹两种形态;开启/关闭安全检测两种配置
---
## 8. 配置项汇总
| 配置项 | 类型 | 默认值 | 现状值 | 说明 |
|---|---|---|---|---|
| `FILE_TRANSFER_MONITOR_CRON` | env | `0/5 * * * * ?` | `0 */1 * * * ?` | 监听周期,5.1 |
| `file.transfer.monitor.round-budget-seconds` | yml/env | 4 | — | 单轮时间预算,5.2 |
| `file.transfer.monitor.round-max-files` | yml/env | 500 | 硬编码 10 | 单轮兜底个数上限,5.2 |
| `file.transfer.monitor.concurrency` | yml/env | 2 | 1(串行) | 并发解包线程数,5.2 |
| `file.transfer.monitor.max-unpack-size-mb` | yml/env | 建议 1024 | 无 | 单文件大小上限,超限标 `.error`,5.3 注意事项 |
涉及文件:`deploy/offline/config/application.yml`、`deploy/offline/.env.iw`、`deploy/offline/.env.ow`、`deploy/config/application-iw.yml`、`deploy/config/application-ow.yml`、`deploy/offline/docker-compose.yml`、`deploy/docker-compose.yml`。
---
## 9. 实施顺序与风险
| 顺序 | 项 | 改动量 | 风险 | 收益 |
|---|---|---|---|---|
| 1 | 5.3 缓冲区 64 KB | 5 处,各 1 行 | 极低 | 大文件组包 31× |
| 2 | 5.1 监听周期 5 秒 | 配置 | 低 | 端到端省约 55 秒 |
| 3 | 5.5 md5 索引 | 4 个 SQL 文件 | 极低 | 多文件解包明显加速 |
| 4 | 5.4 level 0 | 小 | 低 | 已压缩格式再省 CPU |
| 5 | 5.2 时间预算 + 认领 + 并发 | 中 | 中(涉及并发与文件改名状态机,需完整测试) | 高峰期吞吐一个数量级 |
| 6 | 5.6 异步组包 | 大 | 中 | 仅超大文件场景需要,二期评估 |
建议 1–4 作为一期(都是低风险、可独立验证的改动),5 作为二期,6 视现场文件大小分布决定。
**每一项都必须单独验证并记录数字**,不要合并提交,否则现场出问题时无法定位是哪一项引起的。