From f817bf070192017917b017cad1cf380b5d2ab304 Mon Sep 17 00:00:00 2001 From: KodouWZF <3092554823@qq.com> Date: Tue, 3 Nov 2026 21:57:15 +0800 Subject: [PATCH] =?UTF-8?q?docs(=E4=BC=A0=E8=BE=93):=20=E8=B7=A8=E7=BD=91?= =?UTF-8?q?=E4=BC=A0=E8=BE=93=E9=93=BE=E8=B7=AF=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=96=B9=E6=A1=88=E4=B8=8E=E7=8E=B0=E5=9C=BA=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现场反馈一页文件要 2 分钟才进内网。实测组包 19.6MB 耗时 45.9s, 其中 SQL 仅 57ms,其余全在文件 I/O,折算 2.33 秒/MB,比正常 deflate 慢 50-100 倍,根因是 512B/8KB 小缓冲在 bind mount 上放大 syscall 次数。 沉淀耗时归因模型、不改代码即可切分端到端 6 段的现场测量方法、 6 项优化的改法与验收标准,以及本次明确不做的事及理由。 --- backend/docs/传输链路性能优化方案.md | 410 +++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 backend/docs/传输链路性能优化方案.md diff --git a/backend/docs/传输链路性能优化方案.md b/backend/docs/传输链路性能优化方案.md new file mode 100644 index 0000000..16a0cc6 --- /dev/null +++ b/backend/docs/传输链路性能优化方案.md @@ -0,0 +1,410 @@ +# 跨网传输链路性能优化方案 + +> 版本: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 +# T2 +ls -la --time-style=full-iso "$GATE_RECV_DIR" | grep +# T3 / T4 / T6 +docker logs --since 30m 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 视现场文件大小分布决定。 + +**每一项都必须单独验证并记录数字**,不要合并提交,否则现场出问题时无法定位是哪一项引起的。