只读 Checksum 审计与可靠的 CLI 输出契约
本文记录 MCLI 只读 checksum 校验工作流,以及发布复核阶段发现的 non-TTY 输出缺陷 pgsty/mc#5 的设计与实现。该命令经由 pgsty/mc 的 pull request #8 与 #13 合入主干,并随 mcli 20260903 正式发布(已内置于 Server 20260903 容器镜像)。
核心概览
历史版本的 CopyObject 实现可能在压缩或加密后的底层存储字节上计算附加校验和(additional checksum),而非在 S3 GET 返回给客户端的逻辑对象字节上计算。mcli checksum verify 会筛选候选对象,独立地将逻辑对象流送入已记录的校验和算法,并把每个候选分类为 MATCH、MISMATCH、NO_CHECKSUM、WOULD_VERIFY(dry run)、十种 UNKNOWN_* 分类之一,或三种 SKIPPED_* 结果之一。
首版实现在交互式终端中工作正常,但当 stdout 被重定向到文件或管道时却没有任何输出(#5)。原因在于 MCLI 为在非终端环境中禁用进度条 UI,会自动将内部状态 globalQuiet 标记为启用;新命令误把这项内部状态当成了“用户要求隐藏审计结果”。PR #13 将语义输出与进度条抑制解耦,在不改变全局 quiet 行为、也不在 CI 中重新打开进度条的前提下恢复了非终端输出。
命令与范围
第一版支持标记为 FULL_OBJECT 的 CRC32、CRC32C、CRC64NVME、SHA1 与 SHA256 校验和。检查目标可以是单个对象、精确 VersionID、前缀下的当前对象、所有历史版本,或由 JSON Lines manifest 给出的精确条目。命令同时支持 SSE-C 密钥映射、时间与大小过滤、--dry-run 成本预估、有界并发 worker(--max-workers)、全局下载限速、--json 输出以及可选的 --report JSON Lines 报告。
该命令严格只读:不校验分段复合校验和(COMPOSITE),不从 ETag 推断校验和类型,不直接读取底层 xl.meta,也不修改任何对象元数据。服务端点必须随校验和一并返回类型头(x-amz-checksum-type);对于不返回该头的端点,所有带校验和的对象都会被归类为 UNKNOWN_CHECKSUM_TYPE,绝不猜测类型。
当前不支持 --rewind、分层恢复、服务端批处理、断点续查或修复。
只读数据路径
对每个候选对象,mcli checksum verify 执行以下五步:
- 启用 checksum mode 发送
HEAD请求,保留所有受支持的校验和摘要与ChecksumType。 - 对不支持或含糊的元数据状态直接归入
UNKNOWN_*,不做猜测。 - 将
GET返回的逻辑字节流送入有界内存哈希器,不把对象正文写入本地磁盘。 - 对不可变历史版本使用
VersionID固定目标;对可变的未版本化或null版本对象使用If-Match,并在流式读取完成后再次HEAD核验以防并发替换。 - 将独立计算的摘要与已存摘要逐一比较。
S3 传输边界仅允许 LIST、HEAD 与 GET;若 mock endpoint 收到任何写方法,自动化测试立即失败。
专用 S3 helper 通过 StatObject 保留所有校验和与类型,再用 Core.GetObject 读取逻辑字节,不启用 SDK 自动校验器。这样无需改变共享 ClientContent,也不会改变 stat、复制、镜像或 JSON 输出的既有契约。
结果与退出码契约
每个候选对象只产生一个稳定的分类结果:
| 结果 | 含义 |
|---|---|
MATCH |
所有受支持的已存校验和均匹配 GET 返回的逻辑对象字节 |
MISMATCH |
至少一个已存校验和与逻辑对象字节不一致 |
NO_CHECKSUM |
对象没有附加校验和,不读取对象正文 |
WOULD_VERIFY |
--dry-run 发现可校验的 FULL_OBJECT 校验和 |
UNKNOWN_* |
MCLI 无法给出可靠校验结论(如 COMPOSITE 或缺失 ChecksumType) |
SKIPPED_* |
过滤条件排除了该对象(SKIPPED_TOO_LARGE、时间过滤或删除标记) |
最终汇总(summary)携带 objects、verified 计数、每种结果状态的计数以及 incomplete 标志。verified 等于 MATCH 加 MISMATCH——只有这两种结果真正把对象正文流过了哈希器。枚举了大量对象却实际核验为零的运行,会在汇总中清晰体现。
--fail-on 支持 mismatch、unknown、no-checksum、any 与 none:
- 默认值
any在出现MISMATCH、UNKNOWN_*或审计不完整时返回退出码1。其中SKIPPED_TOO_LARGE会使any返回退出码1(因为大小上限使审计不完整),而时间过滤与删除标记跳过不会触发失败。 no-checksum包含any的全部失败条件,并在任一对象缺少校验和或verified == 0时返回1。--dry-run不执行--fail-on失败判定。- 命令行参数、认证、对象枚举与报告写入失败属于命令级错误,直接返回非零退出码。
输出与自动化契约
逐对象记录和最终 summary 都是命令的语义输出:
- 除非调用方显式指定
--quiet、-q或MC_QUIET=true,TTY 与 non-TTY 下的 stdout 都必须收到全部对象记录和最终 summary。 - non-TTY 下的
--json每行输出一个紧凑 JSON 值(JSON Lines);TTY 下的--json保留 MCLI 既有的缩进美化格式。 - 全局参数(
--json、--quiet)在根命令mcli、checksum与verify三层位置均生效。 --report独立于 stdout:即使显式--quiet让 stdout 静默,--report仍会完整写入对象记录和最终 summary 的 JSON Lines。- 输出通道不会改变
--fail-on的退出码判定。
这一区分非常关键:MCLI 历史上的 globalQuiet 混合了两个来源——用户显式传入的 quiet 参数,以及拿不到终端尺寸时自动启用、用于关闭进度条 UI 的 non-TTY 状态。如果直接修改全局 globalQuiet,会让 cp、get、put、mirror 等命令在 CI 日志中重新输出进度条。
因此 PR #13 的修复仅作用于 checksum 命令内部:它沿完整 CLI context 链查找显式 quiet/JSON 参数(绕开 CLI 库 GlobalBool 只检查最近一层父级 flag set 的限制),并在 verify action 内部恢复被嵌套 Before 钩子重置的 JSON Lines 模式。
报告、密钥安全与运行成本
报告文件(--report)在 POSIX 系统上以 0600 权限新建,Windows 使用账户的文件系统 ACL。若目标路径已存在则报错退出;报告中只保存对象元数据与校验结果,绝不包含对象正文或 SSE-C 客户密钥。输入清单(--manifest)同样只记录 bucket、key 与可选 versionId。
由于校验过程会完整下载每个受支持对象的逻辑正文,运维人员应组合使用 --dry-run、--max-size、时间过滤、--max-workers 与全局下载限速控制网络与存储负载。NO_CHECKSUM 与 UNKNOWN_* 计数在汇总中独立展示,绝不与 MATCH 混同。
Mismatch 能证明什么
MISMATCH 证明:校验时刻 HEAD 返回的附加校验和,与同一时刻 GET 返回的逻辑对象字节不一致。单凭 MISMATCH 本身,不能直接断定对象一定由某个历史压缩复制缺陷生成,也不能替代与外部权威数据源的比对。
排查出 MISMATCH 时,不要原地覆写底层校验和元数据。应先通过只读审计完成分类;对确认受影响且有业务消费的对象,优先写入新对象键或新版本,用 mcli checksum verify 验证新对象返回 MATCH 后再切换下游消费方。UNKNOWN_* 对象不得纳入自动化修复脚本。
验证记录与输出 Schema
验收矩阵覆盖 TTY 人类可读与 JSON 输出、non-TTY 管道、普通文件重定向、app/parent/leaf 三层 JSON 与 quiet 参数、环境变量 MC_QUIET=true、quiet 模式下的 --report、报告写入失败、MISMATCH/UNKNOWN_* 退出码,以及针对真实 SILO 服务端的 MATCH、MISMATCH 与 COMPOSITE 对象实测(详见 mc checksum verify 命令参考)。
JSON 消费方应校验 schemaVersion: 1,并通过 type: object 与 type: summary 区分逐对象记录和最终汇总。--max-workers 默认为 4(范围 1–64),用于限制并发对象校验协程数。
测试还覆盖多校验和任一不匹配、固定与 null 版本、正常完成时每个候选只输出一次,以及 worker 取消后正常关闭结果流。InvalidObjectState 优先归类为需要恢复的存储状态,不能误作通用 403;SSE-C 错误分类不依赖 URL 或对象名文本。