批量报表、训练数据和工业采集文件经常通过 HTTP 或对象存储交付。下载中断之后,最危险的错误并不总是报错退出,而是把不同版本的片段拼成一个长度正常的文件,再交给下游解析。本文针对一份已生成文件的传输验收;示例为构造推演,没有声称运行过性能或故障实验。
冻结文件身份,分别回答版本、字节与业务内容问题
一个可恢复下载任务至少应记录导出任务标识、对象版本或强验证器、预期长度、表示编码、校验算法、校验范围和期望值。请求地址只是取文件的入口,签名地址更新不一定代表对象变化,地址不变也不保证内容不变。恢复时要绑定稳定文件身份,而不是只认文件名。
将完成条件拆成三层:取到同一版本的字节;取到该版本的全部正确字节;文件属于正确业务范围且内容可用。长度可以发现某些截断,却发现不了等长替换。摘要能比对字节,但若期望摘要也来自错误文件,就无法发现导出条件选错。授权范围、导出参数和数据边界必须另外核对。
源文件生成阶段若需要扫描变化中的数据,可使用分页采集的一致性快照验收方法。本文解决文件生成之后的传输层问题:即使源端已经生成正确快照,下载端仍可能因续传、压缩或落盘故障破坏它;反过来,传输校验通过也不证明源快照的业务完整性。
不要把 ETag、分段摘要与完整文件哈希混为一谈
HTTP 的 ETag 是表示的验证器,不是通用的文件摘要格式。[1] 可以把强 ETag 原样用于条件请求,不能默认去掉引号后就是本地文件的 MD5。带 W/ 前缀的弱验证器不适合本篇的逐字节续传条件。
对象存储也有专门边界。Amazon S3 官方文档说明,多段上传对象的 ETag 不等于完整对象的 MD5;部分加密方式下也不能这样解释。其完整对象校验和与组合校验和具有不同计算范围与规则。[3] 因而不能看到摘要后缀就自行裁掉,再与全文件哈希比较。
建议在任务契约中明确三元组:算法、校验类型、字节范围。例如通过可信导出清单提供整份下载文件的 SHA-256,或使用存储服务明确支持且客户端正确实现的校验机制。这里的清单方案不代表某个 S3 多段上传算法必然支持完整对象校验;应按具体服务接口确认。
期望摘要应绑定对象版本并来自可信通道。摘要本身不提供身份认证;如果文件与摘要都能被同一攻击者替换,简单相等检查没有证明来源可信。普通传输损坏检测与供应链真实性验证是不同需求,不能把增加一次哈希描述成完整安全方案。
先说清校验的是哪组字节,再处理压缩与范围响应
RFC 9530 区分 Content-Digest 与 Repr-Digest:前者针对该 HTTP 消息的内容,后者针对选定表示的数据。[2] 对范围响应而言,本次传输片段和完整表示不是同一个范围。接收一段后通过该段摘要,只能证明相应范围,不能直接把整个文件标记完成。
Content-Encoding 也会改变要校验的字节。客户端若自动解压 gzip,再对解压后的文本计算摘要,就可能与针对编码后内容的 HTTP 摘要不在同一范围。文本解码、换行转换、BOM 处理或重新序列化同样会改变字节;它们应在原始交付物验收之后进行。
一种较简单的实现契约是请求 identity 编码,并确认响应实际遵守,按原始文件字节落盘和续传。请求头表达偏好不等于服务器一定遵守;遇到意外编码要停止续传并按明确策略处理。若确需压缩表示,必须固定表示、保存其字节坐标,并明确摘要针对压缩文件还是解压产物,不能在两个坐标系之间沿用偏移。
摘要还可能位于响应尾部字段。若客户端没有读取或验证这些字段,就不能声称已完成对应校验;在校验值尚未到达前让下游消费文件,也会绕过完整性闸门。[2] 应选用能暴露所需字节与字段的客户端,并记录所用模式,而不是只记录 HTTP 状态码。
断点续传按响应分支处理,绝不盲目追加
构造一个十字节 ASCII 文件,版本 V1 为 ABCDEFGHIJ。客户端已经持久保存前四字节 ABCD,随后源端替换为同长度的 V2:abcd123456。如果直接从偏移四续读并追加,结果成为 ABCD123456,长度仍是十,却既不是 V1 也不是 V2。这是字节推演,不是网络实测。
对可变地址,应随 Range 请求携带原强 ETag 对应的 If-Range。RFC 9110 规定条件不匹配时忽略范围请求、返回完整表示,而不是继续提供旧断点后的片段。[1] 如果服务提供不可变对象版本地址,也应把该版本与清单绑定。缺少可靠版本机制时,宁可重新获取完整文件并校验,也不能把跨版本拼接称作安全续传。
收到 206 时,检查 Content-Range 的起点与请求断点一致、终点和总长度合理,并核对实际片段字节数与期望范围。对本例剩余六字节,范围为 bytes 4-9/10,端点包含在范围内。还应确认响应表示与任务身份一致;发现版本或编码变化就停止合并。此处流程只接受单范围响应,不实现 multipart/byteranges。
收到 200 时,不把完整响应接到旧文件后。将旧暂存内容隔离,按新的完整下载处理;如果任务必须交付原版本,则先确认是否仍可获取原版本,不能悄悄改为最新文件。收到 416 只说明请求范围不可满足,不自动证明本地文件完成;仍须核对身份、总长度及整文件摘要,不能仅因断点等于某个长度就放行。
断点值应来自已确认持久写入的进度,不是网络层已接收的计数。重试同一片段时按指定偏移写入或确认片段已提交,不能重复追加。并行下载需要记录各片段的起止、长度、版本和完成状态,最后确认覆盖整个区间且无缺口、无不期望的重叠;文件预分配到目标大小也不代表这些字节已经写入。
完整校验与正式交付必须是两个有序阶段
建议将下载状态分为已定位版本、下载中、字节已核验、格式已核验、已交付。所有片段先进入独立暂存对象。收齐后,从实际落盘文件重新计算约定的整文件摘要,并检查长度;不能只对网络流算一遍后默认磁盘写入也一定正确。
字节通过之后再解析格式,检查编码、必要字段、记录数和源端清单中的业务合计。若这些业务核验失败,保留字节校验成功的事实,同时将交付标记为失败,便于区分传输与源数据问题。不要把两种失败都归因于网络重试。
下游只读取明确标记为完成的版本。在支持相应语义的同一文件系统中,可以先准备临时文件,再通过原子重命名切换可见名称;崩溃持久性仍需遵循该文件系统的同步约定。对象存储不能默认具有文件系统重命名语义,应上传独立完成对象,再按服务支持的条件更新发布清单或指针。
下载完成后若还要导入数据库,应把导入批次标识与文件身份绑定,使重复通知不会重复计入数据。文件已验收、业务已导入和已通知消费者分别记录。跨存储没有共同事务时,需要可恢复的提交记录;不能靠一个“成功”布尔值掩盖中间阶段。
上线前用故障矩阵验证恢复,不以正常下载一次作为验收
测试一:连接在响应中途关闭。预期文件保持未交付,恢复只从已提交片段继续。测试二:同长度替换源版本。预期拒绝混合,并按原版本契约决定重新下载或失败。测试三:服务器忽略 Range 返回 200。预期进入完整下载分支,不能产生旧前缀加完整文件。
测试四:注入错误 Content-Range、重复片段或缺失片段。预期定位具体范围并停止交付。测试五:客户端自动解压或响应带意外编码。预期发现字节范围不一致,而非通过改摘要算法掩盖。测试六:文件长度相同但改动一个字节。预期整文件摘要失败。
还应覆盖空文件、尾部摘要未读、416 响应、落盘后进度未更新的崩溃,以及已交付后重复执行。每项都要预先写出期望状态和允许恢复动作。本文给出验收设计,没有实际执行这些故障注入,因此不提供通过率、性能倍数或通用分片大小。
最终回执至少包含文件身份、表示编码、预期与实际字节数、算法与校验范围、摘要比对结果、格式与业务检查结果、交付版本及未解决异常。完整重读会增加磁盘 I/O,分片并行也会增加状态管理;应按文件大小与恢复要求选择。成本可以测量和权衡,但不能用 HTTP 成功代替明确的完整性交付证据。
