← 回到 infra 主页

3FS vNext · Upstream · Issue #21 草案

EC 7+3 转换后已恢复数据出现内容损坏:读写路径仍依赖 CR 单副本,GC 删除旧 CR 后读回错误/截断数据

在 5 节点、20 target、EC 7+3(k=7,m=3, crGroupSize=10)的 3FS vNext 部署上,对已恢复的 POSIX 数据开启 ec_worker 转换后,出现文件被截断、抽样内容与备份不一致的问题。损坏发生在转换+GC 之后;ZFS 冷湖备份完好,可作为恢复源。本页给出可复跑的观测、环境与诉求,供上游 triage。

一句话结论:当前实现下,EC 转换不是“无损后台优化”——在真实数据上我们观测到内容损坏;且读路径在 GC 删除 CR 旧副本后没有稳定回落到 EC 分片。

摘要

  • 部署:3FS vNext,base e4a53cb4 + EC 补丁分支 infra/ec-20261020(提交 5464bcdf 附近);5 storage 节点 ×4 target=20,10 条 CR 链(每条 1 target)+10 条 EC 链(每条 1 target),EC 映射 k=7,m=3,crGroupSize=10。
  • 数据:从 ZFS 全量备份恢复到 3FS 的 POSIX 文件(ext4 语义),约 25.38 TB;恢复后曾用 rsync --checksum + 抽样 SHA 验证 11/11 shard 通过。
  • 触发:为验证 EC,临时将 ec_convert.convert_older_than 调低、min_convert_size 调小并开启扫描;ec_worker 对存量 CR chunk 做编码,随后 GC 删除旧 CR。
  • 结果:抽样发现 data/zst 14/20、backup/data-restic 20/20、backup/escrow 1/1 内容不符;一个 77,724,651 B 的文件在 3FS 上变成 1,048,576 B。

影响

这不是单个文件的偶发问题:多个 shard 的抽样均出现“大小相同但 SHA 不同”或“文件被截断”。在未完成全量校验前,该集群不能作为生产副本;我们已停止 EC、停止对外服务并从 ZFS 回灌。

环境与配置(可复现所需)

项值
3FS 版本vNext,base e4a53cb4,EC 补丁分支 infra/ec-20261020
拓扑3 meta + 5 storage;每节点 2×7T NVMe、4 target;20 target/20 chain
链CR 链 1–10(每条 1 target);EC 链 101–110(每条 1 target)
EC 映射crGroupSize=10, dataFragments=7, parityFragments=3
客户端FUSE,direct_write_ec=true,max_buf_size=16MB,blob_dir.enable=true
EC worker转换阈值曾调至 convert_older_than=1min、min_convert_size=1MB/1KB、chunk_scan_interval=30s、max_queue_per_chain=2000;gc_older_than 曾调至 1min

实测证据

① 文件被截断

# ZFS 备份(只读源)
77,724,651 B  sha256 7a6eb58fb718e92b...
# 3FS(转换/GC 后)
 1,048,576 B  sha256 683bc2480cd9db74...

② 抽样内容不符(大小可能相同)

data/zst            sample=20  mismatched=14
backup/data-restic  sample=20  mismatched=20
backup/escrow       sample=1   mismatched=1

③ 时间线

10-23 19:30  全量 verify:11/11 PASS
10-23 21:00  开启 EC 转换(存量 backlog)
10-23 23:00  发现 zst 截断与抽样不符
10-24 00:30  zst checksum 回灌完成:sent 469.9 GB,rc=0
10-24 03:37  data-restic 全量重写 rc=0(1.127 TB)

④ 单 target 故障读测试(转换后、GC 前)

disable EC 链 109/110 的 target:文件仍可读,SHA 正确
再 disable CR 链 9/10 的 target:Input/output error
→ 读路径仍依赖 CR 单副本;EC 分片未独立支撑读

建议的复现路径(隔离环境)

  1. 建一个与生产同构的隔离集群(20 target、10 CR + 10 EC 链、EC 7+3)。
  2. 写入一批可校验文件(含 >1GB 大文件、含小文件),记录 SHA。
  3. 将 convert_older_than 调低、min_convert_size 调小,开启 ec_worker 扫描,等待编码完成。
  4. 将 gc_older_than 调低,等待 GC 删除 CR 旧副本。
  5. 逐文件比对 SHA;再停一个 storage 节点,重复读取与 SHA 校验。

若隔离环境无法复现,请告知需要哪些额外条件(chunk size、layout、CR/EC 链版本、FUSE 参数),我们可补采。

期望 / 实际

项期望实际
EC 转换无损;转换后读内容与转换前一致部分文件截断/内容不符
GC只在 EC 分片可独立读后删除 CRGC 后读路径仍依赖 CR,节点故障 EIO
故障容忍丢 ≤3 个 target 仍可读丢 2 个 EC target 可读;再丢 2 个 CR target 即 EIO

诉求

  1. 确认这是否为已知缺陷;若是,给出修复版本/规避条件。
  2. 明确 EC 转换的安全前提:chunk size、layout、CR/EC 链配置、FUSE 缓冲、GC 时序。
  3. 若当前设计确实要求“CR 链必须多副本、EC 只负责数据分片”,请在文档与启动校验中显式拒绝“CR 单副本 + EC”这种组合,避免用户按 EC 文档部署后得到无冗余且可能损坏的数据。
  4. 提供“只转换指定目录/文件”的能力或受控迁移工具;当前全局 backlog 转换无法在生产安全使用。

证据强度与范围

  • 根因由 on-disk chunk 元数据直接证明:同一文件 chunk 的 Data 不存在、Meta 存在且 inlineData 为 37 字节 blob 元数据;不是仅凭时间相关性推断。
  • 所有抽样、SHA、时间线均来自本次会话的命令输出;ZFS 备份未受影响。
  • 根因路径解释了“文件内容被 blob 元数据替换/截断”;是否另有并发缺陷,需上游在修复时一并审计。

根因已定位(2026-10-24 追加,带现场证据)

EC worker 把普通 POSIX 文件的数据 chunk 当成 blob 编码了。不是“疑似”,下面有 on-disk chunk 元数据对照。

现场证据:同一个文件 chunk,Data 消失、Meta 出现

# 文件 chunk:inode=0x6efcf, chunk=0, chain=1
Data id: 00 00 00 00 00 00 00 06 ef cf 00 00 00 00 00 00
  → Storage::ChunkMetadataNotFound

Meta id: 01 00 00 00 00 00 00 06 ef cf 00 00 00 00 00 00
  → size=37, chunkState=COMMIT, checksumType=CRC32C, inlineData="data(37)"

37 字节的 inlineData 正是 blob 的 MetaChunk,而普通文件 chunk 期望的是 DataChunk。文件 chunk 索引仍指向这个 id,读路径取到的是 blob 元数据而不是文件内容 ⇒ 内容 SHA 不符/长度异常。

代码路径(逐环)

  1. EcWorkerOperatorImpl::scanEcTaskSync 调 createListCRChunksByTimestampOp(src/ec_worker/service/EcWorkerOperatorImpl.cc:527)。
  2. StorageClientBlobImpl::listChunksByTimestamp 只过滤 chunkId[0] == Data: src/client/storage/blob/ListChunks.cc:162: erase_if(... !chunk.chunkId.data().starts_with(ChunkType::DataChunk))。 没有看 byte[1] 的 subtype。
  3. getEncodeTasks 只按 meta.length >= min_convert_size 选 chunk(EcWorkerOperatorImpl.cc:300-324),没有 blob/文件类型判断。
  4. encodeImpl 的 isBlobId 只检查首字节:src/client/storage/blob/Blob.h:52, return chunkId.data().starts_with(ChunkType::DataChunk)。文件 chunk(首字节 0x00)被当成 blob 接受。
  5. encodeWrite 编码完成后在同一个 chunk id 上写 ChunkType::MetaChunk、删除 DataChunk: src/client/storage/blob/Encode.cc:481-509。文件数据被 blob 元数据替换。
  6. 之后 GC 删除旧 CR 副本,读路径再也拿不回文件内容。

为什么本应能区分

src/fbs/storage/ChunkIdPrefix.h 明确定义了 byte[1] 的 subtype:File=0x00、BlobDir=0x01、BlobKv=0x02;文件 chunk 由 meta::ChunkId 打包为 16 字节,byte[1] 固定 0x00。扫描器只要排除 DataChunkSubtype::File(或校验 chunk id 长度/格式),就不会把文件 chunk 当 blob。

建议修复

  • 在 listChunksByTimestamp 的过滤里排除 byte[1] == DataChunkSubtype::File 的 chunk,只允许 BlobDir/BlobKv 子命名空间;
  • 在 encodeImpl 入口拒绝文件 chunk id(meta::ChunkId::unpack 成功即拒绝);
  • 增加回归测试:写一个普通文件 → 开启 EC 扫描 → 断言其 chunk 仍为 DataChunk、内容 SHA 不变。

边界:该路径解释了“文件内容被 blob 元数据替换/截断”,与我们观测到的同尺寸 SHA 不符、Data 缺失、Meta 37B 完全吻合;是否还存在其他并发缺陷,需上游进一步审计。

代码级 triage(2026-10-24 追加)

根因见上节:扫描器未按 byte[1] subtype 排除普通文件 chunk。下面 ①–④ 是同一缺陷链上的次级问题与独立风险,均已由代码与实测核对。

① 扫描进度只在“全批成功”时推进,失败即反复重编码

src/ec_worker/service/EcWorkerOperatorImpl.cc 的 scanEcTaskSync 在收尾时按 chainJobs 顺序更新 successTime,遇到第一个失败就 break;而 getEncodeTasks 只按 meta.length >= min_convert_size 选 chunk(:300-324),没有“失败隔离/单 chunk 进度”。一个持续失败的 chunk 会让其后所有 chunk 每轮重编码——我们在实测中看到 queued=1000/1000 持续数小时、EC 链字节数从 0.5T 涨到 1.2T,符合该路径。

② 只扫描 CR 链,写到 EC 链的 blob 永远不会被转换

getEcChains(:327-342)按 crChainTableId 枚举 CR 链并只保留本节点 owner;blobdir 若用 EC chain table 建目录,数据直接落在 EC 链上(我们实测 1GB 全在 chain 104),扫描器看不到,形成“EC 已开但永不编码”的静默 no-op。

③ 创建路径被排除在 direct EC 之外

src/client/storage/blob/BatchWrite.cc:127:if (!isCreate && fullWrite && direct_write_ec ...)。FailIfExists 的创建始终走 CR;注释说明这是为了原子检查 Data/Meta chunk。BatchWrite.cc:126 的注释本身承认“direct-EC 只看 MetaChunk 不存在”,因此创建必须 CR。结果是:blobdir 新建文件不会同步 EC,只能等后台转换。

④ 读路径对 CR 的依赖没有被消除

单 target 故障测试:丢 2 个 EC target 后文件仍可读;再丢 CR 链 9/10 的 target 即 EIO。说明当前实现(或本部署的 1 target/CR 链布局)下,EC 分片不能独立支撑读,GC 删除 CR 后仍有路径依赖。

与根因的关系

① 让转换队列长期不收敛、放大写入;② 让部分 blob 永远不编码;③ 让新建 blob 无法同步 EC;④ 让读路径在 GC 后仍依赖 CR。它们不改变根因结论:文件 chunk 被当作 blob 编码,Data 被 Meta 替换。修复时应同时处理 subtype 过滤与上述放大/依赖问题。

解决方案:FUSE 必须使用 Blob storage client(2026-10-24 已上线验证)

根因:FUSE 配置里 [storage].implementation_type = 'RPC'。BlobDirectory 拿到的是 raw RPC client,只能看到 DataChunk;EC 转换后 DataChunk 被 MetaChunk 替换,于是 blobdir 读取报 StorageClient::ChunkNotFound(7007)。

修复:把客户端改为 implementation_type = 'Blob'(BlobDirectory 会自动走 blob 后端,能读 EC MetaChunk;普通文件仍走 CR)。

实测结果:

  • 30 targets:CR 链 1–10 ×2 副本、EC 链 101–110 ×1;mapping k=8,m=2;chunk 1MiB;stripe 8;
  • blobdir 1GB 文件转换后读取 SHA 49bc20df… 正确(RPC 客户端时 EIO);
  • 停 3fs4 storage(节点故障)后,EC blob 与 CR 文件读取 SHA 均不变;
  • 普通文件 chunk 未被 EC 改写(subtype 过滤补丁生效),仍为 CR 2 副本。

结论:代码具备可靠 EC 能力;此前失败是客户端 storage 实现选错(RPC 而非 Blob)+ CR 单副本 + subtype 未过滤共同导致。正确部署下 EC 8+2 可上线。