传统 VCS 使用固定大小分片,存在严重缺陷:
文件版本 1: [AAAAA][BBBBB][CCCCC][DDDDD]
↑ 插入一个字节
文件版本 2: [AXAAAA][BBBBB][CCCCC] ← 所有分片边界偏移!
结果: 仅仅插入 1 字节,导致所有后续分片都改变,去重率接近 0%。
CDC 通过内容决定边界,而不是固定偏移:
文件版本 1: [AAAAA][BBBBB][CCCCC][DDDDD]
↑ 插入一个字节
文件版本 2: [AX][AAAAA][BBBBB][CCCCC][DDDDD]
↑ 只有这一个分片改变
结果: 局部修改只影响附近的 1-2 个分片,其他分片保持不变。
我们使用 FastCDC 算法,这是工业级的 CDC 实现:
// FastCDC 滚动哈希 (Gear Hash)
hash = (hash << 1) + gearTable[byte]FastCDC 的核心创新:根据当前分片大小动态调整切割概率
阶段 1 (0 ~ minSize): 不切割
阶段 2 (minSize ~ normalSize): 使用 maskShort (更多 bits → 更难切割,让小分片继续增长)
阶段 3 (normalSize ~ normalSize+normalSpan): 使用 maskNormal (标准切割概率)
阶段 4 (normalSize+normalSpan ~ maxSize): 使用 maskLong (更少 bits → 更容易切割,关闭过大分片)
阶段 5 (maxSize+): 强制切割
三 mask 策略:
maskShort = 2^(maskBits+1) - 1: 更多 bits,更难切割 (让小分片继续增长到 normalSize)maskNormal = 2^maskBits - 1: 标准切割概率maskLong = 2^(maskBits-2) - 1: 更少 bits,更容易切割 (防止分片过大)
其中 maskBits = clamp(log2(targetSize) - 1, 10, 24)。
默认参数:
targetSize = Fragment.Size() // 默认 1GB (可通过配置覆盖)
minSize = max(target/4, 64KiB) // 最小分片,下限 64KiB
maxSize = min(target*8, 64MiB) // 最大分片,上限 64MiB
normalSize = target // short/normal 阶段分界
normalSpan = target // normal 阶段长度AI 模型场景推荐配置 (targetSize = 4MB):
AI 模型文件的特点:
- 典型张量大小: 几 MB 到几百 MB
- Fine-tuning 更新: 通常是整个张量或较大区域
- Checkpoint 文件: 10GB - 100GB
当配置 fragment.size = "4m" 时:
| 指标 | 1MB 分片 | 4MB 分片 | 改进 |
|---|---|---|---|
| 10GB 模型 | ~10000 fragments | ~2500 fragments | 减少 75% 元数据 |
| 去重效果 | 优秀 | 优秀 (相近) | 保持高去重率 |
| CPU 开销 | 高 | 低 | 减少 hash 计算次数 |
| 传输协商 | 慢 | 快 | metadata 传输更快 |
CDC 需要检测边界后才能处理分片,无法实现真正的纯 streaming:
1. 读取字节计算 rolling hash
2. 检测到边界
3. 然后才能哈希分片数据
问题:流一旦读过去就无法"回退"。
解决方案:使用滚动缓冲区 (Rolling Buffer)
// 缓冲区大小 = maxSize
chunkBuf := make([]byte, 0, c.maxSize)
// 检测到边界后通过回调传递数据
onChunk(span Span, data io.Reader)内存占用: O(maxSize)
- 典型值: 64MiB (maxSize 上限)
- 与文件大小无关,只与分片大小有关
这是工业标准做法:
- restic: 使用滚动缓冲区
- borg: 使用滚动缓冲区
- rsync: 使用滚动缓冲区
单遍扫描,零临时文件:
func (r *Repository) writeCDCFragments(ctx context.Context, reader io.Reader, size int64) (oid plumbing.Hash, fragments bool, err error) {
// 1. 计算完整文件哈希
h := plumbing.NewHasher()
tr := io.TeeReader(reader, h)
// 2. 创建 CDC 分片器
chunker := NewChunker(r.Fragment.Size())
// 3. 单遍流式分片 + 哈希计算
index := uint32(0)
walkErr := chunker.Walk(tr, func(span Span, data io.Reader) error {
chunkHash, hashErr := r.odb.HashTo(ctx, data, span.Size)
if hashErr != nil {
return hashErr
}
ff.Entries = append(ff.Entries, &object.Fragment{
Index: index,
Hash: chunkHash,
Size: uint64(span.Size),
})
index++
return nil
})
// 4. 保存 Fragments 对象
ff.Origin = h.Sum()
oid, err = r.odb.WriteEncoded(ff)
return
}优点:
- 单 pass
- full hash + chunk hash 同时算
- 无临时文件
Walk 方法实现了尾部合并逻辑:如果最后一个分片小于 minSize,会将其合并到前一个分片中,避免产生过小的 fragment 元数据。实现方式是延迟一个 chunk 的 emit,直到确认下一个 chunk 不需要合并。
在 .zeta/zeta.toml 文件中添加:
[fragment]
enable_cdc = true # 启用 CDC 分片 (Boolean 类型,支持配置 merge)配置说明:
enable_cdc是Boolean类型,支持true/false值- 支持配置层级 merge (Local > Global > System)
- 默认值:
false(使用固定大小分片)
[fragment]
threshold = "1GB" # 文件大小阈值,超过此值才进行分片 (默认 1GB,最小 1MiB)
size = "4m" # 目标分片大小 (默认 1GB,最小 1MiB)
enable_cdc = true # 启用 CDC 分片注意: threshold 和 size 的最小有效值为 1MiB,低于此值将使用默认值。
Zeta 的配置系统有三个层级 (优先级从低到高):
- System config (
<prefix>/etc/zeta.toml) - 系统级配置 - Global config (
~/.zeta.toml) - 用户全局配置 - Local config (
.zeta/zeta.toml) - 仓库本地配置 (最高优先级)
Merge 语义: 高优先级配置覆盖低优先级配置
// Boolean.Merge() 实现
func (b *Boolean) Merge(other *Boolean) {
// If other has a definite value, it should override b (higher priority)
if other.val != BOOLEAN_UNSET {
b.val = other.val
}
}// HashTo 是分片的主入口,根据文件大小和配置决定分片策略
func (r *Repository) HashTo(ctx context.Context, reader io.Reader, size int64) (oid plumbing.Hash, fragments bool, err error)决策逻辑:
size < Fragment.Threshold()→ 作为单个 blob 存储,不分片Fragment.EnableCDC.True()→ 调用writeCDCFragments(CDC 分片)- 否则 → 调用
writeFixedFragments(固定大小分片)
| 文件 | 说明 |
|---|---|
pkg/zeta/cdc.go |
FastCDC 分片器核心实现 (Chunker、NewChunker、Walk、Split) |
pkg/zeta/safetensors.go |
SafeTensors 格式解析器 (未来优化) |
pkg/zeta/objects.go |
HashTo、writeCDCFragments、writeFixedFragments |
modules/zeta/config/config.go |
Fragment 配置项定义 (Fragment struct) |
modules/zeta/config/type.go |
Boolean 类型实现 |
A: 不会。读取时只根据 Fragments.Entries 中的偏移和大小读取,分片策略对读取透明。
A: 可以! CDC 只影响新上传的文件。已有文件保持原有分片方式,两种方式可以共存。
A: CDC 分片大小在 [minSize, maxSize] 范围内波动,平均大小接近 targetSize。实际测试表明存储开销与固定分片相当。
A: CDC 的本质决定了它需要缓冲:
- CDC 需要读取字节 → 计算 hash → 检测边界
- 检测到边界后,才能哈希分片
- 但流已经读过去了,无法"回退"
工业标准: restic, borg, rsync 都使用 rolling buffer
A: 默认 targetSize 为 1GB (FragmentSize 常量)。对于 AI 模型场景,建议配置为更小的值如 4m,以获得更好的去重效果和更快的增量同步。
- FastCDC 算法: Xia, W., et al. "FastCDC: A Fast and Efficient Content-Defined Chunking Approach for Data Deduplication." USENIX ATC 2016
- Gear Hash: 比传统 Rabin Fingerprint 快 2-3 倍
- CDC 原理: "Content-Defined Chunking" (joshleeb.com)
- SafeTensors 格式: https://huggingface.co/docs/safetensors
- 工业实现参考: restic, borg, rsync
文档版本: v3.0 最后更新: 2026-06-03 维护者: Zeta Team