文件秒传与去重背后的 hash 选型:MD5 够吗、分块怎么切、碰撞要不要怕

· 约 6 分钟 #️⃣ Hash

百度网盘的”秒传”、Git 的 commit、Dropbox 的同步、Docker 镜像的 layer 缓存——背后都是同一件事:用文件内容的 hash 当地址,相同 hash 视作相同内容。理解这套机制要回答三个问题:用哪种 hash?整文件还是分块?碰撞概率到底多大?

秒传到底”秒”什么

传统上传流程:

本地文件 ──读取 1GB──→ 网络 ──上传 1GB──→ 服务器存盘
                       (受带宽限制,几分钟到几小时)

秒传流程:

本地文件 ──读取 1GB──→ 算 hash ──上传 32 字节──→ 服务器查表

                                          命中 → link 现有数据 (秒级)
                                          未命中 → 退化回真上传

“秒传”一词有误导性——它没真正传完整文件,传的是文件指纹。慢硬盘的瓶颈反而不是带宽是 I/O——你得本地完整读一遍 1GB 才能算出 hash。SSD 上 BLAKE3 能跑到 6GB/s,机械硬盘读取速度上限就是 hash 速度。

百度网盘的真实秒传指纹

公开资料和抓包分析得到的秒传指纹:

file_size       (文件总字节数)
slice_md5       (前 256KB 的 MD5,快速预筛)
content_md5     (整文件 MD5)
content_crc32   (整文件 CRC32,校验完整性)

为什么不是单一 hash

  1. slice_md5 算得快,先用它做服务器侧第一轮筛——256KB 的 MD5 < 10ms,能过滤掉 99.9% 的不命中
  2. content_md5 才是真实命中判定,但要客户端完整读一遍文件
  3. file_size 双重保险——同 MD5 不同 size 几乎不可能(攻击者要同时碰撞 MD5 + 控制 size)
  4. content_crc32 是后续传输纠错,不参与去重 key

这套设计的安全模型:抗自然碰撞 + 弱抗主动碰撞。MD5 已经被构造碰撞攻破(2004 年王小云论文),但攻击者要同时让 size 一致、前 256KB 内容一致、整文件 MD5 一致,难度抬高了几个量级——但不是不可能

整文件 hash vs 分块 hash

整文件 hash 简单但脆——改一个比特,完全不同:

file_v1.bin  → SHA-256: a3f2c8...
file_v2.bin  → SHA-256: 9b1e4d...   (改一字节)

雪崩效应是设计目标——hash 函数的核心安全性质就是输入微变 → 输出剧变。对完整性校验是好事,对增量去重是灾难

分块 hash 解决”小修改重传”问题:

file (12 MB)  →  Block 0 (4MB) → SHA-256: a1...
                 Block 1 (4MB) → SHA-256: b2...
                 Block 2 (4MB) → SHA-256: c3...

修改第 5MB 处的一个字节 → 只有 Block 1 变化
                          → 上传只发 Block 1(4MB)
                          → 服务器替换该块

Merkle 树把分块 hash 再 hash 一层,得到根 hash 作为整体指纹:

        Root = SHA-256(H_AB || H_CD)
       /                    \
   H_AB = SHA-256(A||B)   H_CD = SHA-256(C||D)
   /        \             /       \
 A         B            C        D    (4MB blocks)

Git、IPFS、BitTorrent、ZFS、btrfs 全部用 Merkle 树。改 1 个块只用更新该路径上的 log₂(N) 个节点。

固定分块的”插入 1 字节”问题

固定 4MB 切块对”在中间插入”完全失败:

原文件:[Block 0][Block 1][Block 2][Block 3]
                  ↓ 在中间插入 1 字节
新文件:[Block 0'][Block 1'][Block 2'][Block 3']

Block 0 之后所有块全部向后偏移 1 字节,hash 全部改变——明明只改了 1 字节,要重传从插入点到文件尾的所有内容。

内容定义切块 (CDC) 是 rsync 的灵魂

Content-Defined Chunking 用滚动 hash 找”自然边界”——比如 hash 末尾连续 13 个 0 bit 就切一刀。这样:

  • 在文件中间插入 1 字节 → 只影响当前块
  • 后面的块边界仍由”内容找边界”决定,不受偏移影响
  • 平均块大小 = 2^(0 的位数),13 位 0 → 平均 8KB

历史上的 CDC 算法:

算法年代用在哪
Rabin fingerprint1981rsync (1996), LBFS, Plan 9
Buzhashbup (Git-like backup)
Gear2014restic (~2015 起)
FastCDC2016borgbackup, restic 新版

FastCDC 比 Rabin 快 3×、比 Gear 快 1.5×——已成为现代去重备份系统的事实标准。

实战工具:

  • rsync —— 经典文件同步,Rabin 滚动 hash,1996 年就有
  • restic / borgbackup —— 加密增量备份,FastCDC + BLAKE2/SHA-256
  • Dropbox —— 块级增量同步,公开论文里写了 4MB 固定块(早期)→ 后来改 CDC
  • ZFS / btrfs —— 文件系统级去重,块级
  • Docker image layers —— layer 级(粗粒度),每个 layer 是 tar.gz 的 SHA-256

碰撞概率的真实数字

生日悖论:N 位 hash,期望第一次碰撞在约 √(π/2 × 2^N) ≈ 1.25 × 2^(N/2) 次:

hash 位数期望碰撞所需文件数类比
32 (CRC32)~ 8 万一个家庭照片库就撞
64~ 50 亿Google 全球用户数级别
128 (MD5)~ 2.2 × 10¹⁹全人类一辈子产文件量级别
160 (SHA-1)~ 1.5 × 10²⁴沙子级别
256 (SHA-256/BLAKE3)~ 4 × 10³⁸宇宙原子数级别

关键观察

  • CRC32 绝不能做去重 key——10 万张图就期望撞一次
  • MD5 在非对抗场景仍可用——自家网盘库 PB 级也不会自然碰撞
  • SHA-256 已经远超物理需求——选它纯粹是为了抗”人为构造”

人为构造碰撞自然碰撞完全是两件事。MD5 已被工程化攻破——王小云 2004 / Stevens 2009 给出秒级生成”两个 MD5 相同的不同文件”的算法。但秒传场景里这个攻击需要攻击者在受害者之前上传,门槛实际较高。SHA-1 在 2017 年被 Google SHAttered 攻击破掉,2020 年成本降到 $11k——生产环境从 2020 年起一律应迁 SHA-256

加密 hash vs 非加密 hash

加密 hash(SHA-256、BLAKE3、MD5)的设计目标是抗主动构造——在没有密钥的情况下,攻击者无法在合理算力内找到碰撞或原像。

非加密 hash(xxHash、Murmur、CityHash、SipHash)只保证均匀分布——做哈希表的桶分配、Bloom filter 的指纹够用,但攻击者能秒级构造碰撞。它们的速度优势:

hash速度(单核)类型
MD5600 MB/s加密(已破)
SHA-1800 MB/s加密(已破)
SHA-256400 MB/s加密
BLAKE36,800 MB/s加密(最快的)
xxHash30,000 MB/s非加密
Murmur35,000 MB/s非加密

BLAKE3 是过去十年最大的工程进步——树形结构天然支持并行(多核可线性加速),速度还把 SHA-256 甩开 10×,安全性等同 SHA-256。Cargo(Rust)、IPFS、syncthing 都已切到 BLAKE3。

选型决策树

你需要 hash 来做什么?

├─ 文件完整性校验(信任源)        → SHA-256 或 BLAKE3
├─ 自家备份/网盘去重                → BLAKE3(速度优先)
├─ 公开网盘"秒传"防恶意构造          → SHA-256 + 文件大小双校验
├─ Git 内容寻址                    → 跟 Git 走(默认 SHA-1,已开始迁 SHA-256)
├─ rsync 类增量同步                 → FastCDC + BLAKE3
├─ 哈希表桶分配(无外部输入)       → xxHash(最快)
├─ 哈希表桶分配(有外部输入,防 DoS)→ SipHash(HashDoS 防御)
├─ Bloom filter / LRU 缓存 key      → xxHash 或 Murmur
└─ 密码哈希(用户密码)             → 不是 hash 范畴!用 Argon2/bcrypt/scrypt

最后一条特别要警告:密码存储绝对不能用 SHA-256 这类——它们设计目标是”快”,密码哈希要的是”慢”(让暴力破解不可行)。Argon2id 是当前推荐,bcrypt 仍可接受,scrypt 已淘汰。

一句话总结

文件秒传 = 传 hash 不传文件;分块去重要用 CDC 不要用固定切块;自家场景 MD5 还能撑、对抗场景一律 SHA-256 / BLAKE3、密码场景用 Argon2id 不用任何普通 hash。

❓ 常见问题

百度网盘的"秒传"原理是什么?

客户端先算文件 hash 给服务器查表,命中就跳过上传。具体是文件 SHA-1(早期 MD5)+ 文件大小 + 前 256KB 的 MD5 三件套:服务器只要这三项命中库里的某个文件,就把那个文件 link 到你账户名下,零字节真实上传。这就是为什么大文件能"秒传"——上传的不是文件,是 hash。代价:算 hash 要本地完整读一遍文件,所以"秒传"对慢硬盘并不真"秒",瓶颈从带宽变成磁盘 I/O。安全代价:MD5/SHA-1 已知碰撞构造,攻击者可以预先放一个"良性"文件占位 hash,后来真用户上传同 hash 的恶意文件时被秒传成良性版本——但反向更危险:恶意者投毒后"秒传"给所有上传同 hash 的人。

文件去重为什么不能直接拿"文件大小"当 key?

因为同尺寸文件碰撞秒发生。10 万个 JPG 里很多都恰好是 4.0MB 整。即使加上 mtime 也不行——同一段视频在不同设备上时间戳不同。正确做法是内容 hash:MD5(128 bit)在自家库里 PB 级别都不会自然碰撞(生日悖论下大约 2^64 ≈ 1.8×10¹⁹ 个文件后才期望首次碰撞)。自然碰撞人工构造碰撞是两码事——MD5 在"非对抗、防自然碰撞"场景仍然可用(如个人备份去重、媒体库管理);但只要场景里有外部攻击者能控制部分文件内容,就必须升 SHA-256 或 BLAKE3。

同一个 1GB 文件改了一个字节,秒传还能命中吗?

整文件 hash 完全不命中——SHA-256 在改一个比特后 hash 完全不同(雪崩效应是 hash 函数的核心性质)。这就是为什么 rsync、Dropbox、restic、borgbackup 不用整文件 hash,而是分块 hash:文件切成 4MB(或更小)的块,每块独立算 hash,上传时只发改过的那几块。进一步:固定 4MB 切块对"在文件中间插入 1 字节"也会失败——后面所有块向后偏移 1 字节,全部 hash 改变。FastCDC 这类内容定义切块(Content-Defined Chunking)用滚动 hash 找"自然边界"——插入只影响当前块,前后块不受影响,rsync 算法的灵魂。

选 MD5、SHA-256、BLAKE3、xxHash 该看什么?

两个轴:是否对抗、是否需要安全。(1) 加密 hash——MD5/SHA-1(已破,仅自家场景)、SHA-256(仍安全)、BLAKE3(最快的安全 hash,比 SHA-256 快 5-10×)。(2) 非加密 hash——xxHash、Murmur、CityHash,速度比 SHA-256 快 10-50×,但完全不抗主动构造,只能用在"绝对没人攻击"的场景(哈希表、Bloom filter、缓存 key)。选型:自家备份去重 → BLAKE3;公开网盘秒传防恶意 → SHA-256(带文件大小双校验);内存哈希表 → xxHash。绝对不要用 CRC32 做去重——32 bit 在 6 万文件后就开始撞。

#️⃣ 打开 Hash MD5/SHA · HMAC · 文件校验

📖 同一工具的其他教程