必须用内容定义切分(如Rabin-Karp滚动哈希+阈值触发),首块从offset 0开始,最后一块完整计算sha256.Sum256;阈值建议0xFFFFF(约1MB),禁用固定大小切分,避免错位导致去重失效。

块切分必须用内容定义切分,不能按固定字节切
固定大小切分(比如每 4MB 切一刀)会导致相同内容在不同文件中因偏移差异生成不同块哈希,彻底废掉去重能力。真实场景中,哪怕只是插入一个字节,后续所有块都会错位。
- 必须用内容定义切分(content-defined chunking),主流是 Rabin-Karp 滚动哈希 + 阈值触发;
github.com/minio/minio/pkg/hash提供可用实现,github.com/klauspost/cpuid不适用 - 阈值建议设为
0xFFFFF(约 1MB),太小导致块碎片化、索引膨胀;太大削弱去重率 - 首块必须从 offset 0 开始——否则无法对齐已有块索引;最后一块允许小于阈值,但需完整读取后计算
sha256.Sum256,不能截断补零 - 切分过程必须流式:用
bufio.NewReader+ 手动维护滑动窗口 buffer;禁止os.ReadFile加载整块到内存
块哈希必须统一用 sha256.Sum256,禁用 md5.Sum
块级去重对哈希碰撞零容忍。两个不同块若哈希相等,后续写入会覆盖错误数据,且无法检测——这不是“概率低”,而是“一旦发生即数据损坏”。
-
md5.Sum返回 16 字节,sha256.Sum256是 32 字节,混用会导致 map key 长度不一致、查不到索引 - 流式哈希模板固定:
h := sha256.New(); io.Copy(h, chunkReader); sum := h.Sum(nil),别用sum[:]直接当 map key(是指针,比较地址) - 空块(如末尾 padding)必须显式返回
sha256.Sum256{}零值,不能跳过或 panic - 性能慢 10%–20% 是可接受代价,这是块级去重的底线,不是优化项
引用计数必须支持原子检查+递增,sync.Map 不够用
多个 goroutine 并发上传同一文件时,可能同时发现某块不存在、同时写入、同时初始化引用计数为 1,最终变成 1 而非 2,导致该块被提前回收。
- 推荐组合:
map[[32]byte]int+sync.RWMutex,锁粒度控制在单个 block key 上(可用shardKey := sum[0] % 64分片) - 写入前必须原子判断:先
RLock查是否存在;存在则Unlock后Lock递增;不存在则Lock后写入并初始化为 1 - 引用计数减为 0 时不能直接删块——必须确保无任何 goroutine 正在读该块(需额外读屏障或延迟删除机制)
备份路径必须分层存储,不能平铺所有块哈希
把几百万个 sha256.Sum256 哈希平铺在一个目录下,Linux ext4 或 XFS 会严重拖慢 stat 和 open,甚至触发内核目录 hash 冲突退化为线性扫描。
立即学习“go语言免费学习笔记(深入)”;
- 标准做法是取哈希前 2 字节做一级目录,再取接下来 2 字节做二级目录,例如
ab/cd/abcdef...,保证单目录文件数 - 路径生成必须用
hex.EncodeToString(sum[:]),不可用sprintf("%x", sum)(后者对前导零处理不一致) - 写块文件前,必须先
os.MkdirAll(dirPath, 0755);注意os.MkdirAll不是原子的,但并发创建同一目录无害 - 块文件本身应设权限
0444(只读),防止被意外覆盖;引用计数变更走独立元数据文件或数据库,不改块文件内容


















