3FS vNext · Upstream · Issue #21 草案
在 5 节点、20 target、EC 7+3(k=7,m=3, crGroupSize=10)的 3FS vNext 部署上,对已恢复的 POSIX 数据开启 ec_worker 转换后,出现文件被截断、抽样内容与备份不一致的问题。损坏发生在转换+GC 之后;ZFS 冷湖备份完好,可作为恢复源。本页给出可复跑的观测、环境与诉求,供上游 triage。
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。rsync --checksum + 抽样 SHA 验证 11/11 shard 通过。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)
disable EC 链 109/110 的 target:文件仍可读,SHA 正确
再 disable CR 链 9/10 的 target:Input/output error
→ 读路径仍依赖 CR 单副本;EC 分片未独立支撑读
convert_older_than 调低、min_convert_size 调小,开启 ec_worker 扫描,等待编码完成。gc_older_than 调低,等待 GC 删除 CR 旧副本。若隔离环境无法复现,请告知需要哪些额外条件(chunk size、layout、CR/EC 链版本、FUSE 参数),我们可补采。
| 项 | 期望 | 实际 |
|---|---|---|
| EC 转换 | 无损;转换后读内容与转换前一致 | 部分文件截断/内容不符 |
| GC | 只在 EC 分片可独立读后删除 CR | GC 后读路径仍依赖 CR,节点故障 EIO |
| 故障容忍 | 丢 ≤3 个 target 仍可读 | 丢 2 个 EC target 可读;再丢 2 个 CR target 即 EIO |
EC worker 把普通 POSIX 文件的数据 chunk 当成 blob 编码了。不是“疑似”,下面有 on-disk chunk 元数据对照。
# 文件 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 不符/长度异常。
EcWorkerOperatorImpl::scanEcTaskSync 调 createListCRChunksByTimestampOp(src/ec_worker/service/EcWorkerOperatorImpl.cc:527)。StorageClientBlobImpl::listChunksByTimestamp 只过滤 chunkId[0] == Data:
src/client/storage/blob/ListChunks.cc:162:
erase_if(... !chunk.chunkId.data().starts_with(ChunkType::DataChunk))。
没有看 byte[1] 的 subtype。getEncodeTasks 只按 meta.length >= min_convert_size 选 chunk(EcWorkerOperatorImpl.cc:300-324),没有 blob/文件类型判断。encodeImpl 的 isBlobId 只检查首字节:src/client/storage/blob/Blob.h:52,
return chunkId.data().starts_with(ChunkType::DataChunk)。文件 chunk(首字节 0x00)被当成 blob 接受。encodeWrite 编码完成后在同一个 chunk id 上写 ChunkType::MetaChunk、删除 DataChunk:
src/client/storage/blob/Encode.cc:481-509。文件数据被 blob 元数据替换。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 成功即拒绝);边界:该路径解释了“文件内容被 blob 元数据替换/截断”,与我们观测到的同尺寸 SHA 不符、Data 缺失、Meta 37B 完全吻合;是否还存在其他并发缺陷,需上游进一步审计。
根因见上节:扫描器未按 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,符合该路径。
getEcChains(:327-342)按 crChainTableId 枚举 CR 链并只保留本节点 owner;blobdir 若用 EC chain table 建目录,数据直接落在 EC 链上(我们实测 1GB 全在 chain 104),扫描器看不到,形成“EC 已开但永不编码”的静默 no-op。
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,只能等后台转换。
单 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 配置里 [storage].implementation_type = 'RPC'。BlobDirectory 拿到的是 raw RPC client,只能看到 DataChunk;EC 转换后 DataChunk 被 MetaChunk 替换,于是 blobdir 读取报 StorageClient::ChunkNotFound(7007)。
修复:把客户端改为 implementation_type = 'Blob'(BlobDirectory 会自动走 blob 后端,能读 EC MetaChunk;普通文件仍走 CR)。
实测结果:
49bc20df… 正确(RPC 客户端时 EIO);结论:代码具备可靠 EC 能力;此前失败是客户端 storage 实现选错(RPC 而非 Blob)+ CR 单副本 + subtype 未过滤共同导致。正确部署下 EC 8+2 可上线。