diff --git a/backend/docs/传输链路性能优化方案.md b/backend/docs/传输链路性能优化方案.md index b6106b9..9d833b3 100644 --- a/backend/docs/传输链路性能优化方案.md +++ b/backend/docs/传输链路性能优化方案.md @@ -1,8 +1,8 @@ # 跨网传输链路性能优化方案 -> 版本:v1.2(2026-11-03) +> 版本:v1.3(2026-11-03) > 适用后端:s-file(alita-bjy-uygur) -> 状态:5.1/5.2/5.3/5.4/5.5 已实施并本地验证 → 待现场验证;5.6 视现场文件大小分布决定 +> 状态:5.1/5.2/5.3/5.4/5.5 已实施,本地已完成一次真实端到端传输实测(§10.5)→ 待现场验证;5.6 视现场文件大小分布决定 > 实施与实测记录见第 10 节 > 关联代码:`TransferUserFileServiceImpl`(发送/组包)、`TransferFileMonitorServiceImpl`(接收/解包)、`TransferFileMonitorJob`(调度)、`FileServiceImpl` / `LocalFileStorage`(物理文件读写) @@ -526,9 +526,59 @@ CREATE INDEX idx_file_record_md5_code ON xs_file_record(md5_code); 本地没有这个数据量。现场验收时按 §7.2 的方法:一次性提交 30 个传输,观察日志中每轮「传输包处理=N」 与「目录剩余待处理」是否单调下降。 -### 10.5 尚未验证的部分 +### 10.5 真实端到端传输实测(2026-11-03 23:13,本地内外网双实例) -- [ ] **应用内真实组包**:需要登录后发送一次同一个文件夹,从 `ControllerLogAspect` 的 `useTime` 与「传输文件打包完成」日志确认应用侧数字(基准只覆盖 I/O 环节,未含 SQL 与元数据序列化) -- [ ] **解包侧**:64KB `ZipInputStream` 的改动只做了编译验证,未跑通一次真实解包(同样受登录限制) -- [ ] **level 0 产出的 zip 能否被接收端正常解包**:需一次内穿外 + 一次外传内的完整往返 +用内网 `admin` 账号把文件夹 `test`(userFileId=31,13 个子文件)传外网,走完整链路:组包 → 摆渡目录 → 外网解包入库 → 回写 `.finish` → 内网状态翻正。 + +实际发出 9 个文件(4 个被安全检测拦下:id 32 `manual_review`、38 `reject`、39 `reject`、41 `pass_with_flag`),原始合计 19,647,211 B ≈ 18.7 MiB,产出包体 19,631,241 B。 + +#### 四段耗时 + +| 段 | 观测点 | 耗时 | +|---|---|---| +| ① 内网组包 | `ControllerLogAspect` 的 `useTime`(含 SQL、读原文件、deflate、落盘) | **881 ms** | +| ② 去程摆渡 | 包落盘 23:13:41.246 → 外网轮询发现 23:13:45.111 | 3.87 s(本机同一目录,真实搬运≈0,全是轮询等待) | +| ③ 外网解包入库 | `开始解包文件` 23:13:45.198 → `文件解包成功` 23:13:45.983 | **785 ms** | +| ③′ 外网整轮 | 含大小校验 87 ms + 解包入库 785 ms + 写 `.finish` + 删包 | **1001 ms**(`TransferFileMonitorJob.execute(),use(ms)`) | +| ④ 回程状态翻转 | `.finish` 落盘 23:13:45.992 → 内网处理完 23:13:50.027 | 4.04 s(同样是轮询等待),处理本身 **21 ms** | + +**端到端 9.9 s,其中真正干活 1.90 s(881 + 1001 + 21),其余 ~8.0 s 是两次轮询等待。** +两侧日志 0 ERROR;外网侧收到文件夹 id=40 + 9 个文件,状态全为正常,字节数与内网逐一对应;内网侧传输副本文件夹 id=55 状态已翻回正常。 + +#### 与基线对比 + +组包 45,934 ms(§10.3 记录的改造前应用侧基线,同一文件夹)→ **881 ms,52×**。 + +这个 52× 高于 §10.3 容器内基准测到的 17.6×,原因是**本次样本对 5.4 极度有利**:9 个文件里 pptx/xlsx/png 占 19,635,943 / 19,647,211 = **99.94%**,全部命中 level 0,deflate 几乎不消耗 CPU;包体 19,631,241 B 对原始 19,647,211 B 只有 0.08% 的缩减,正是「存原样」的预期表现。 + +**汇报口径**:现场若以 pptx/图片/压缩包为主,可引用 50× 量级;若以 txt/doc/源码为主,只有 5.3 的缓冲区收益,应按 8× 量级说。不要用 52× 做通用承诺。 + +解包侧无改造前基线(§10.3 的基准只覆盖组包),本次 785 ms 是改后的绝对值。18.7 MiB / 9 文件 → 约 **42 ms/MB**,与组包侧 881 ms(47 ms/MB)同量级,符合对称预期。 + +#### 本次实测暴露的一个既有缺陷:包完整性校验空转 + +外网侧日志出现 9 条 `元数据中未找到文件大小信息: userFileId=56..64`,随后仍是 `文件大小校验通过: 1_55.zip (共 0 个文件)`。 + +根因:`x_user_file` 表**没有 `file_size` 列**(见 `backend/db/init-table.sql` 的建表语句)。`UserFileDTO.fileSize` 只在**查询路径**上由 `UserFileMapper.xml:7` 的 `SELECT t.*, fr.file_size ... JOIN x_file_record fr` 带出来。 + +而 `packTransferFiles` 写元数据用的是 `userFileService.create(ExpandUserFileCreateDTO)` 的**返回值**:`UserFileServiceImpl:171` 拿到的是 `create(UserFileCreateDTO)` 里 `UserFileConvert.toDto(userFile)` 转出的 DTO,`userFile` 来自刚 insert 的实体,插入路径上从来没有 size。于是 `files[].fileSize` 全为 null → `transferMetadata.json` 里没有 size → 接收侧 `expectedSizeMap` 是空 map。 + +注意:在 `UserFileCreateDTO` 上补 `.fileSize(...)` **没用**——`toEntity` 会丢弃这个字段,表里也存不下。 + +而 `verifyFileMD5`(`TransferFileMonitorServiceImpl:450`)的放行判据是 `fileCount == 0 || successCount != expectedSizeMap.size()`,此时 `fileCount=9`、`successCount=0`、`expectedSizeMap.size()=0`,`0 != 0` 为假 → **直接放行**。 + +后果:这道校验从上线起就没真正比对过任何一个字节数。一个被截断的包(光闸搬到一半停了、mtime 已静止超过 `stale-after-seconds`)会被当成完整包解包,**静默丢文件**,且不会进 `.error`。5.2 引入的 mtime 静止判据只解决「坏包反复重试」,解决不了「不完整的包看起来是好的」。 + +修法两处: +1. **补齐 size**:`UserFileServiceImpl.create(ExpandUserFileCreateDTO)` 返回前用带 JOIN 的查询路径重查一次子文件(`this.list(UserFileQueryDTO.builder().userId(userId).rootType(rootType).parentId(folderId)...)`),把带 `fileSize` 的列表作为 `files` 返回;或在 `packTransferFiles` 里按 `fileCode` 批量查 `x_file_record` 补齐。另一条重载 `create(UserFileDTO folder, List files)`(外网解包入库走它,`TransferFileMonitorServiceImpl:579`)返回值同样缺 `fileSize`,但目前没有下游读它,属潜在问题,可一并修也可先放着。 +2. **改掉空转判据**:`verifyFileMD5` 的 `successCount != expectedSizeMap.size()` 改成 `successCount != fileCount`(拿实际遍历到的条目数比,而不是拿可能为空的期望表比),保留 `fileCount == 0` 的在途判断。 + +第 2 条即使第 1 条不修也必须改——它是「静默丢文件」的直接闸门;第 1 条不修的话,改了判据会让所有包都校验不过,所以**两条必须一起上**。 + +改完后 §10.5 表格里 ③′ 的 87 ms 校验段才会真正开始比对字节数,届时需重测该段(预期增量很小:size 来自 ZIP 中央目录,`ZipFile` 已经打开过一次)。 + +### 10.6 尚未验证的部分 + +- [ ] **外传内方向**:本次只跑了内穿外,回程方向(外网组包 → 内网解包)未实测 - [ ] **现场系数**:客户环境是 Linux + 光闸监控目录,syscall 单价与本地 Docker Desktop 不同,必须按第 4 节重测 +- [ ] **时间预算耗尽分支**:需单轮塞进足够多的大包才能耗尽 4 秒,本地没有这个数据量(见 §10.4)