
CubeS3lvol 三层测试体系完全指南从离线单测到 S3 数据面端到端回归【免费下载链接】CubeSandboxInstant, Concurrent, Secure Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox本篇指南以 CubeS3lvol 存储组件SPDK 驱动层自带的 test/README.md 为核心骨架系统讲解该仓库test/目录下从integration/、dataplane/到tools/的三层测试架构。你将掌握如何用make check一键运行 24 个测试套件、理解每个套件对应的真实缺陷类型WAL 并发写、快照隔离、跨 lvstore 导出、文件系统 FLUSH 等并学会用s3lvol_rpc.py、s3_prefix_rm.py等工具进行失败诊断与残留清理。三层测试结构与设计动机CubeS3lvol 是一个基于 S3 的块存储组件通过 NVMe/TCP 目标s3lvol_tgt将卷数据保存在对象存储中本地磁盘只用于 WAL 与元数据日志。其核心思路可参见 CubeS3lvol/README.md。正因数据路径贯穿 DPDK/SPDK、NVMe-oF、真实 S3 后端测试被刻意设计成依赖递增的三层改动代码后应按由快到慢、由廉价到侵入的顺序执行目录内容依赖耗时integration/按模块划分的断言式测试直接链接libs3bsdev.amake check批量无需凭据、无需 root约 1 分钟dataplane/端到端target nvmf 真实 S3 真实块设备root、真实 bucket 与凭据、nvme-cli、fio每个 3–6 分钟tools/上述两层共用的小工具及手工调试工具——这种分层的直接好处写在 test/run_all.sh 的注释里10 个 integration 测试可在任何环境运行2 个额外需要真实 S3 凭据10 个 dataplane 脚本则需要 root、凭据、可写的/data以及整台机器 nvme 栈的独占使用权。若没有统一入口运行测试就退化为记住 22 条手工调用命令。一条命令跑全部make check 与 run_all.sh顶层 Makefile 提供了三条快捷入口make check # 24 个套件等价于 test/run_all.shdataplane 需要 root S3 make check-offline # 无需凭据和 root 的套件test/run_all.sh --offline make check-rules # 仅运行分层规则检查test/tools/check_layering.sh更细粒度地控制运行范围直接使用 run_all.shtest/run_all.sh --list # 仅列出将运行什么、环境具备什么不执行 test/run_all.sh --no-dataplane # 两层 integration含需要 S3 的不含 dataplane test/run_all.sh --offline # 只需离线 integration 与离线工具 test/run_all.sh # 环境允许的一切run_all.sh存在的根本原因见 test/README.md 与 run_all.sh各套件的前置条件曾经各自漂移——哪些需要凭据、哪些需要 root、哪些需要可写/data现在统一收敛到run_all.sh --list的输出与它打印的 skip 原因里而不是放在一个每新增一个套件就会过时的计数中。--list会同时打印本机探测结果root 是否有、凭据是否有、endpoint/bucket/region 是什么从/data/cubelet/s3.cfg读取可用环境变量S3LVOL_TEST_BUCKET覆盖。运行前的环境探测逻辑run_all.sh对环境的判定直接决定哪些套件会变成 SKIPrun_all.shrootid -u是否为 0凭据已导出的AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY优先否则回退读取s3.cfgS3 可用凭据 endpoint bucket 三者齐全才算HAVE_S31dataplane 阻塞器若目标进程s3lvol_tgt已在运行、或/data/cubelet/rcow/active_lvols中仍有活动卷记录或该文件损坏无法解析则整组 dataplane 套件被 SKIP——这是防止上次残留把本次运行搅乱的第一道防线。此外run_all.sh会读取s3.cfg中的path_style/no_tls标志并通过S3LVOL_TEST_PATH_STYLE/S3LVOL_TEST_NO_TLS/S3LVOL_TEST_S3FLAGS导出给 dataplane 脚本使其对 MinIOpath-style 明文 HTTP与云 COSvirtual-hosted HTTPS两类后端一视同仁。五个防止假绿的设计决策test/README.md明确列出每一条决策都对应一种看起来全绿、实际什么都没测的运行方式这也是整个测试体系最值得借鉴的部分Skipped 不等于 Passed。无法运行的套件以 SKIP 形式上报并附原因汇总时单独计数退出码2专门留给全部通过但部分套件因环境无法运行与0严格区分——二者共用退出码正是 CI 假绿的机制。实现见 run_all.sh只有因显式标志而跳过才允许与通过共用0因环境被迫跳过的会输出PASSED, but some suites could not run并返回 2。先构建。dataplane 脚本拒绝针对比源码更旧的二进制运行见下文check_binary_fresh.sh不先构建陈旧代码树的整个后半段都会变成 skip。串行。每个 dataplane 脚本都假定独占本机固定 RPC socket、nvme 连接、以及编译进模块的bstore.json与rcow_active_lvols路径。两个同时跑不会干净地失败而是互相穿插。失败不停跑。完整一轮需数分钟在第一个红点就停会丢掉坏了一个还是坏了十个——这是最有价值的信息。失败被收集并在最后统一重放。从廉价到侵入排序激活与控制放最后。它们对遗留全局状态最敏感因此也最适合作为前面所有套件是否清理干净的最终裁决。一个真实踩过的坑失败后故意保留残留文档记录了一个在第二次运行中真实命中过的陷阱test/README.md失败的 dataplane 脚本会故意保留其 S3 前缀与bstore.json条目便于诊断因此同一脚本的下一次运行会发现 lvstore 已被记录、走 attach 路径而非 create 路径从而以与原问题毫无关系的方式失败没有走 create 路径、无法创建 ctl-a。run_all.sh现在会在失败汇总后直接打印清理命令run_all.shtest/tools/s3_prefix_rm.py -e ep -b bucket -r region -p lvs/ # 并手动从 /data/cubelet/rcow/bstore.json 中移除 lvs 条目integration/无凭据的断言式单测层make -C test/integration check运行 10 个不需要 S3 凭据的套件spawner / thread_bounce / journal / wal / cache / flush / export / statefile / local_dev / checkpoint当前共 491 个断言。journal与wal在/tmp下创建 aio 镜像cache与local_dev在/data下全部不触碰网络。这层的构建规则在 test/integration/Makefile 中有非常细致的说明其中两条最关键两种链接方式不可混用s3_client_test/s3_spawner_test链接spdk_thread_stub.c从普通 pthread 提交、owner_thread恒为 NULLbounce 分支运行时不可达s3_thread_bounce_test必须链接真实 spdk_thread spdk_env_dpdk 并传--no-huge才能真正驱动线程弹跳——它也因此成为整棵树中第一个到达spdk_env_init()的链接是Dropped DPDK constructor这类链接错误最廉价的探针40 秒内跑完。DPDK 始终静态且--whole-archivebdev_aio、accel 等模块靠构造函数自注册、无显式符号引用普通链接会直接丢弃它们这正是s3_journal_test/s3_wal_test/s3_cache_test/s3_local_dev_test必须--whole-archive的原因。各套件对应的真实缺陷每个 integration 套件都锚定一类真实缺陷而非泛泛的写了能读回cache读缓存允许 miss但绝不允许返回错误数据、绝不允许复用他人仍在使用的槽位。旧 uuid 必须 miss、淘汰必须放过 pinned 槽位、populate 在旧版本被读时必须让出。它用墙钟时间等待 I/O 而非轮询计数原因见 HANDOFF 5.20。[11]–[13]节是驻留位图——槽位只持有对象的一部分未持有的块保留着前一租户的字节三节分别钉死未填充区间必须 miss含仅部分重叠的读、短对象尾部的整块/半块边界、uuid 更换或槽位复用后旧区间不可读。journal[12]节用仅 4 块4×64256 条记录即可环绕构造第二个 journal因为默认 16 MiB journal 需要 262144 条记录才能环绕一次——真实机器根本无法被推进到那条路径。export只测 manifest 且不启动 DPDK——重点是可解析但错误的 manifest 必须被拒绝因为这是唯一没有警示音的失败模式坏 uuid 仍能 GET 回内容读到的却是别人的数据。其中一条用例直接使用从活机上抓取的真实 manifest而非人工构造。statefile只测文件 I/O。两个崩溃形态残留的部分.tmp不得泄漏进读取模拟 rename 前崩溃目标文件仍是完好的旧内容过短文件就地写入崩溃的经典残留必须被拒绝而非作为空对象交给 JSON 解析器。外加环境覆盖策略绝对路径生效、相对路径被拒、解析结果被缓存。local_dev测试超级块的四个拒绝路径及各自精确 errno——坏 magic / 坏 CRC 为-EILSEQ未知版本为-EPROTO双 bdev 布局缺少 cache bdev 重开为-EINVAL。损坏通过直接重写后备文件的前 4 KiB植入损坏是外部的不是模块自身会走的路径。这里还踩过一个陷阱struct s3_super_block约 200 字节而磁盘块是整整 4 KiB用结构体当读写缓冲区会栈溢出。checkpoint测试加载 checkpoint 的拒绝路径。它 stub 掉s3_head/s3_get_range/s3_put/s3_delete使加载同步完成并彻底排除s3_client_aws.o的链接但仍链接 bdev/thread 库——因为s3_chunk_map.o的 insert/remove 引用s3_journal_append_*会拖入s3_journal.o与s3_local_dev.o。对象被手工构造且两个 CRC 都正确然后逐字段破坏坏 magic / 坏版本 / 头 CRC / 几何不匹配 / 尺寸不匹配 / 条目 CRC各配精确 errno而合法对象则以正确的 LSN 与 generation 加载。AWS_INSTALL_DIR 的探测AWS_INSTALL_DIR通常无需显式给出mk/s3lvol.common.mk 会探测/usr/local/aws、/opt/aws、/usr/local判据是include/aws/s3/s3_client.h是否存在。显式传空值仍表示使用系统库。两个需要真实 S3 的套件export AWS_ACCESS_KEY_ID... AWS_SECRET_ACCESS_KEY... ./test/integration/s3_client_test --endpoint host --bucket name ./test/integration/s3_bs_dev_test --endpoint host --bucket names3_client_test有 1 个xfailCOS 忽略If-None-Match: *HANDOFF 5.5。它仍会运行并打印结果只是不计为失败——否则该套件将永远红而一个永远红的测试就不是测试了。若某天真通过会以XPASS单独列出意味着后端长出了新能力create-once 可以转而依赖服务端而非 uuid 唯一性这是值得跟进的事件而不是需要刷新的数字。run_all.sh中对该 xfail 的注释run_all.sh补充说明MinIO 后端会遵守该头并产生 XPASS被套件的结果统计接受。dataplane/端到端数据面回归export AWS_ACCESS_KEY_ID... AWS_SECRET_ACCESS_KEY... ./test/dataplane/run_dataplane_test.sh -e cos.ap-nanjing.myqcloud.com -b bucket -r ap-nanjing ./test/dataplane/run_recovery_test.sh -e cos.ap-nanjing.myqcloud.com -b bucket -r ap-nanjing ./test/dataplane/run_snapshot_test.sh -e cos.ap-nanjing.myqcloud.com -b bucket -r ap-nanjing ./test/dataplane/run_export_test.sh -e cos.ap-nanjing.myqcloud.com -b bucket -r ap-nanjing ./test/dataplane/run_selfimport_test.sh # 读取 /data/cubelet/s3.cfg无参数 ./test/dataplane/run_snapdelete_test.sh # 同上 ./test/dataplane/run_cubecow_client_test.sh # 同上cubecow/Cubelet RPC 顺序 ./test/dataplane/run_activation_test.sh # 同上 ./test/dataplane/run_fs_test.sh # 同上真的执行 mkfs.xfs mount ./test/dataplane/run_guards_test.sh # 同上两道误删防线 ./test/dataplane/run_control_test.sh # 同上驱动 scripts/ 下的脚本run_all.sh的 dataplane 段run_all.sh按依赖关系精心排序export 系export → srcdel → selfimport → derived → decouple_queue → snapshot_cancel → snapshot_converge → agent_template共享 manifest 故紧挨在一起snapdelete 与 pending_delete其拒绝正是延迟删除队列的输入相邻fs 排在块级套件之后两者同时失败时说 dd 语言的那个更容易读guards 因其不扰动主机状态的本性可置于任意位置activation 与 control 殿后。run_dataplane_test.sh单进程打通读写路径单进程链路s3lvol_tgt - lvstore on S3 - lvol - bdev - nvmf tcp - kernel nvme - /dev/nvmeXnY - dd/fiorun_dataplane_test.sh流程create → write → flush → disconnect/reconnect → 从 S3 读回 → fio verify → resize → delete lvol。它特意针对五类缺陷写入数据逐字节读回dd sha256及 fio 自带 verifymax_num_segments 1是否真正生效——bs_dev 对iovcnt 1拒绝-ENOTSUP若 bdev 层不拆分iodepth 1 大块 fio 必然暴露且事后 grep 日志确认是哪一层的问题从未写入区域的读返回零blobstore 对未分配 chunk 的假设同一 chunk 内的并发写存活——这就是 WAL 与 flusher 存在的理由步骤 8 用 1 MiB 块、iodepth 8nvmf 拆成 8 个并发命令落进同一 chunk关停顺序正确步骤 10unload 必须排干 flusher、让 blobstore 写最终元数据、再 flush、关日志最后才释放 journal 与本地设备。还有一个重要的方法细节关键读取全部使用iflagdirect页缓存会掩盖 S3 里的垃圾而 WAL 路径下进程内 overlay 会先应答读因此步骤 6b 先 flush lvstore 排空 overlay步骤 7 才断开、重连并重读——此时数据只能来自 S3。run_recovery_test.sh三进程崩溃恢复attach 于干净卸载之后、attach 于 SIGKILL 之后、校验两个数据段。覆盖 owner-marker 契约与 checkpoint含定时器触发。最后一步专门追猎 unload 与 checkpoint 轮询器之间的竞争卸载前写入数十 MiB 数据把 flusher 排空拉伸到秒级使 destroy 后的窗口必然跨越 1 秒轮询周期——断言日志在最终 Destroying s3_bs_dev 之后没有 checkpoint 开始。这个套件曾经复现过一次 use-after-freeHANDOFF 7.16。run_snapshot_test.sh快照与克隆的隔离性原卷、快照、克隆各自拥有独立的 NVMe namespace三者互不影响。核心断言即隔离写 A → 快照 → 用 B 覆盖原卷快照必须仍读到 A克隆快照后克隆初始读 A直通父层向克隆写 C 后三者读 C/A/B。另验证对快照的写被拒绝、删除有克隆的快照被拒绝-EBUSY。run_export_test.sh跨 lvstore 导出/导入沙箱 pause/resume两个 lvstore 共享同一进程同一 bucket 下的两个前缀、各自独立本地设备。断言按重要性排序导入卷读源数据 → 写它不影响源 →unload attach 目标 lvstore 后克隆仍可打开且数据不变测试 imports 注册表与 esnap 回调blobstore 在加载期间同步询问父层→ decouple然后 release 导出release 后再 unload attach 仍能打开卷专门守卫decouple 必须改元数据——只换内存中的 back_bs_dev 能通过此前所有断言恰恰在这里死掉。还验证零拷贝导出除 manifest 外不产生任何东西稀疏性成为 manifest 的属性64 MiB 卷中的 8 MiB 数据必须命名 8 个 chunk 而非 64 个以及导入进行中拒绝 release。两条容易被忽略的路径也在其中快照继承 esnap 依赖——给导入的克隆拍快照后删掉克隆release 仍必须被拒绝否则对象会被从活快照脚下删除以及import --decouple的后台形式——验证 import 在拷贝完成前就返回、完成后卷无需父层即可读数据。步骤 7 重启前会显式打一次 checkpoint。此前每次运行日志都是No checkpoint加ckpt lsn 0checkpoint 轮询器间隔 60 秒而一次运行约 40 秒所以带 checkpoint 重放从未被 export/import 场景测过。它测的东西很具体克隆必须在加载期间打开checkpoint 后其映射来自 checkpoint加journal 尾部而非一整本 journal——断言要求skipped与ckpt lsn同时非零只看其一也会被空 journal 路径满足。步骤 11 专门测克隆链lvol - snap1 - snap2是真实用法也是零拷贝最安静地崩掉的地方——后续快照只持增量父层仍持有大部分数据。三条断言(a) 仍为零拷贝(b) manifest 命名的 chunk 数多于 snap2 自己拥有的(c) B 侧两个区域都能读回。(b) 是真正的哨兵链断开时 (c) 也会失败但表现为导入成功、读着正常、继承的那半是零——而 (b) 直指父层未被解析。步骤 13 打印时延。零拷贝的意义是这些数字不随卷大小增长所以全绿时也打印——一个其实还在拷数据的交接不会失败任何断言只会在这里露馅毫秒变秒。RPC 客户端的 python 启动开销本机约 37 ms先作为基线测出并扣除否则会淹没被测量对象。它还打印本次导出是否触发了 drain——唯一随脏数据变化的时延分量。rcow_export_snapshot现在立即返回导出 uuid不代表导出完成manifest 异步发布时延只有在rcow_get_snapshot_status轮询到DONE后才算落定——仍然测量发起导出到真正完成的全过程。rcow_get_snapshot_status接受export_uuid或snapshot_name二选一都传会被拒绝成功时返回export_statusINPROGRESS / DONE / NONE与deletableYES / NO当场计算导出进行中、活动/未知租约或本地 esnap 读者仍引用零拷贝导出、或克隆数多于一时为 NO。空闲的 REF 导出不钉住快照删除快照即释放该导出。两种查询形式仅区别不存在的含义按 uuid 查不到走失败路径按 snapshot_name 查只有快照本身缺失才失败而存在却从未导出的快照返回export_status: NONE——这正是快照名查询形式存在的原因。run_selfimport_test.sh导出-导入退化为本地克隆export_snapshotimport_lvol在同一 lvstore 内现在退化为本地克隆RPC 回复的mode字段区分local_clone与esnap。export_snapshot本身完全没改manifest 写时不可能知道会被本地消费还是跨机消费决策只能放在导入侧。退化条件要求 endpoint、bucket、prefix、快照名与 lvol uuid 全部匹配。套件还验证生命周期不变量删除源快照释放其导出复用快照名于另一个 blob 或可写 lvol 不会复活该导出。为什么值得退化本地克隆的父层被 blobstore 钉住有克隆的快照不可删而 esnap 克隆的父层靠导出租约保护直到克隆 decouple 或删除——所以它更安全不只是更快。run_cubecow_client_test.shCubelet 实际发出的 RPC 组合Cubelet 从不直接调 JSON-RPCcubecow 始终按固定顺序使用同样的 11 个rcow_*方法本套件驱动的正是这个顺序。既有套件覆盖机制导出、克隆隔离、双进程导入但不覆盖 Cubelet 真正发出的组合sealext4、umount、inactive 快照、删除工作卷、从同一模板克隆 N 个并 resize 其一、同时导出三个快照并按snapshot_name轮询、被拒的模板删除其错误不得含not found、失败的导入不得留下命名 lvol、以及import(decoupletrue)后不等 decouple 直接rcow_active_bdev。它读s3.cfg使用自己的 lvstore / WAL / 注册表。run_snapdelete_test.sh23 个断言的删除语义源卷仍存活时删除其快照及该操作的边界。关键事实快照化把原 blob 变成新快照的克隆再次快照则插入链中所以对 lvol L 有 s0/s1/s2 时形状是s0 - s1 - s2 - L每个快照的 clone_count 恰为 1。bs_is_blob_deletable()blobstore.c:8704只在1时无条件拒绝恰好为 1 时它把快照合并进那个克隆spdk_lvol_destroy()配合先查克隆的 lvol 以便修复它。本套件存在的原因pre-check 曾把阈值写成0使最普通的删旧快照报 EBUSY且该错误被误认为 blobstore 限制HANDOFF 7.22。它断言数据而非返回码——删除是合并只看状态码对返回 0 但丢掉了被合并的 cluster视而不见而该故障要数周后才浮出。布局刻意安排得让错误合并无法显得正确卷分四个 4 MiB 区域区域 N 只在快照 N 之前写入故每区域只存在于链的一环区域 0 要遍历整条链才能读到丢 cluster 的合并会先坏区域 0/1 而非卷自己写过的区域 3。每次删除后四个区域全部重读。还覆盖存活快照仍读到其捕获的历史区域 0-2 存在、区域 3 为零2 个克隆的快照必须被拒且拒绝发生在任何回退之前数据不变 卷仍可写lvstore 仍可 unload——这是 pre-check 存在的全部理由来得太晚的拒绝会留下无法关闭的 lvol。run_fs_test.sh把卷当磁盘用的唯一套件rcow_create_lvol-rcow_active_bdev-mkfs.xfs- mount写 47 个文件 → freeze → 快照 → 挂载快照并按文件逐个 md5 比对。其他套件用 dd/fio/blkdiscard对齐、direct、已知偏移说话文件系统则完全不同小型未对齐元数据写、每请求数百段、每次 journal 提交一次 NVMe FLUSH。最后一条绝非假设——SPDK_BDEV_IO_TYPE_FLUSH最初实现为spdk_blob_sync_md()宿主的mkfs.xfs曾使 target 中止HANDOFF 7.17而彼时 15 个套件 / 643 个断言全绿只因没有套件像块磁盘那样用卷。第 [12] 节现在按名 grepblob_verify_md_op。核心断言在文件粒度而非块粒度从冻结快照挂载别处读到的文件集与内容必须与 freeze 瞬间的原卷完全一致后续变更一个都不可见。完全一致是两个 manifest各文件 md5排序后的diff而非抽查且先断言 manifest 非空——比较两个空目录同样会成功却什么也没证明。三类变更刻意设计因为它们的传播路径不同创建文件占用快照从未有过的 cluster删除释放快照仍引用的 cluster就地覆盖必须走 CoW 且不得修改共享 cluster。另一半价值在于怎么挂载快照——曾两次量错猜错快照在 bdev 层拒绝写但 nvmf 无处上报blockdev --getro恒为 0run_snapshot_test.sh第 [8] 节从另一侧钉住该值。XFS 在挂载时看块设备的只读标志决定能否写——xlog_find_tail()里的xlog_clear_stale_blocks()会写只有xfs_readonly_buftarg()为真时才跳过。所以默认标志下那次写总发生且总被拒mount(8) 报出极具误导性的cant read superblock内核侧是log recovery write I/O error。注意 recovery 与日志脏不脏无关——日志干净时也会发生前两版正是栽在这里。三行结论被断言且两个失败都匹配特定内核消息只断言它失败了毫无价值——任何无关故障也会失败快照取自blockdev --setromount -o ro,nouuid未挂载文件系统否EIOcant read superblock未挂载文件系统是可挂载Ending clean mount已挂载、冻结的文件系统是诚实拒绝recovery required on read-only device需-o norecovery操作规则因此是挂载快照前先blockdev --setro快照来自活文件系统时再加-o norecovery——xfs_freeze不覆盖日志xfs_quiesce_attr()只强制日志并回收 inode不写卸载记录只读设备无法重放。两种情况都需要nouuid快照与原卷同 UUIDXFS 拒绝在原卷挂载时重复挂载。xfs_repair -n只对干净日志的快照运行——文件都能正确读取时元数据仍可能不一致这是数周后才会浮出的故障脏日志下-n拒绝重放并报出无法对平的可用块数那是在抱怨日志而非快照。run_guards_test.sh24 个断言的两道误删防线防线 1状态文件路径可配置。/var/tmp/bstore.json与/var/tmp/rcow_active_lvols曾是整台机器共享的编译期常量——包括测试套件在内其中两个在清理时会rm -f活动注册表。这并非假设它真的一次删掉了运行中实例的注册表。现在模块通过S3LVOL_ACTIVE_FILE/S3LVOL_BSTORE_FILEvbdev_s3lvol_statefile.c解析它们rcow_common.sh从RCOW_*变量映射导出。套件断言覆盖生效条目写入私有路径/var/tmp下什么都没有、变量未设时历史默认仍成立生产依赖此点、运行后宿主的两个文件逐字节不变。外加一条每节点单 blobstore 策略不得失败开放。它曾是pick_one() ! NULL而pick_one()对 0 和 1 匹配都返回 NULL所以只在恰好一个时拦截两个就放行——而两个可达attach刻意不受限跨 lvstore 搬卷需要两个同时加载。现在改为计数。注意限制在 create不在同时加载几个上——不要给attach加同样的检查run_export_test.sh需要两个同时加载来构建克隆链。防线 2create 拒绝已有 blobstore 的前缀。lvstore 名即 bucket 中的键前缀s3_bs_dev_create中prefix lvs_name前缀下meta/checkpoint名字固定在其上再建 blobstore 会覆盖前者的 chunk map——静默地因为数据对象以 uuid 命名、永不相撞只是不再可达。owner marker 挡不住这个它回答此刻是否有人在写干净卸载会释放它正常关停的 lvstore 在那里不留痕迹。两条普通入口本地bstore.json丢失、rcow_start.sh看不到前缀被占而回退到 create或两节点派生同名而第一个已干净关停。create 现在先 HEADprefix/meta/checkpoint。套件钉住的是该防线的边界有 checkpoint 时拒绝干净卸载后仍拒绝恰是 owner marker 看不到、也是防线存在的全部理由forcetrue放行接管前缀必须仍可能日志会说明检查被跳过attach 不受影响——否则create 被拒将与此 lvstore 已死无法区分这是此类防线最常见的次级故障。防线看不到的窗口写在代码注释里create 后、任何 checkpoint 前的干净关停会留下一个只含 uuid 命名对象的前缀而s3_list_objects()仍无法列出它-ENOTSUP。run_control_test.sh驱动控制脚本、测顺序而非单个 RPC驱动scripts/rcow_{start,stop,recovery}.sh九个节 48 个断言一次 start/stop 往返连接后热插拔 namespaceAEN宿主须自行发现干净重启后卷回到同一 subsystem/nsid 且数据不变冷读进行中移除 namespaceSIGKILL 后 recovery 先确认 owner marker 已死再强制 attach指向已不存在卷的注册表条目被拒并从注册表移除进程已加载注册表但未挂载任何 namespace被报为不一致而非健康。第 [5b] 节带进行中读的去激活对应一次真实崩溃spdk_nvmf_subsystem_pause()中 nsid0 意为一个 namespace 都不暂停nvmf.h的措辞模块曾传 0——于是 remove_ns 前什么都没静默resume 释放 bdev channel 时读仍在飞行bdev 层的assert(TAILQ_EMPTY(ch-io_submitted))中止了进程。去激活空闲卷永远不会命中它——第一次命中它的是 udev 探测新设备。因此本节故意跑一个 32 MiB 冷读数据在 S3、overlay 答不了、I/O 保证在飞行。最后一节同时是另一次崩溃的回归哨兵spdk_json_next()在对象末尾返回NULL注册表解析循环在解引用前测试it endNULL 满足它所以重启后第一次rcow_get_bdev每次都段错误。没有其他路径到那行代码——注册表惰性加载首次激活式 RPC 才读它其他测试要么从空文件开始、要么如 replay在首次调用前删除它。它的隔离与其他脚本不同lvstore 名、WAL 镜像、运行目录都是测试专属但bstore.json与rcow_active_lvols路径编译进模块、整机共享。因此脚本在开始时若rcow_active_lvols非空本机正有东西在提供卷就拒绝运行结束时只删自己的条目。运行目录每轮开始整体删除——target 日志是 append-only 且最终检查要 grep 它上一轮留下的 assert 会让下一轮变红而真正产生它的那一轮全绿两端都错。dataplane 公共约定环境变量环境变量作用S3LVOL_KEEP_S3成功时也保留 S3 对象S3LVOL_KEEP_LOGS成功时也保留 target 日志S3LVOL_TGT_CPUMASKtarget 的-m默认0x3S3LVOL_WAL_FILE/S3LVOL_WAL_MB/S3LVOL_JOURNAL_MB本地设备镜像的位置与布局S3LVOL_SRC_WAL_FILE/S3LVOL_DST_WAL_FILE仅run_export_test.sh两个 lvstore 各自的设备镜像S3LVOL_CKPT_INTERVAL_SECrecovery 测试第三次 attach 的 checkpoint 间隔默认 5 秒成功与失败的清理语义成功时脚本自己清理 S3 对象与本地镜像失败时一切都保留并打印删除命令——失败时这些对象与 WAL 镜像是唯一证据不要急着删。bucket 必须是专用的。清理按前缀lvs_name/删除而-p 可以删掉整个 bucket不要指向存有其他数据的 bucket。tools/三层共用的调试工具s3lvol_rpc.py发送一次 JSON-RPCs3lvol 的 RPC 方法定义在本仓库SPDK 的scripts/rpc.py不认识它们因此自定义方法一律走这个工具。成功时result打到 stdout失败时error打到 stderr 且退出码为 1。test/tools/s3lvol_rpc.py rcow_get_lvstores test/tools/s3lvol_rpc.py rcow_checkpoint_lvstore {lvs_name:dpvs} test/tools/s3lvol_rpc.py --sock /var/run/s3lvol.sock --timeout 60 bdev_get_bdevs两种无需自带方法的模式test/tools/s3lvol_rpc.py --ls # 以表格列出 rcow_get_lvstores # 含 DEL / PEND 列 test/tools/s3lvol_rpc.py --retry-pending # 重新发起被拒、现已可删的快照删除s3lvol_rpc.py有一个值得注意的契约细节见 s3lvol_rpc.py每个外部 RPC 无论成败都以{bool_value, string_value}信封应答且两者都是 JSON-RPC成功——若原样透传所有失败都会被报告为退出码 0。该工具负责解包bool_value为真则string_value上 stdout、退出 0为假则上 stderr、退出 1从而在一个地方恢复契约而非在测试套件与脚本中约 200 个调用点各做一遍。--raw关闭解包用于查看 target 实际发出的线上内容。--retry-pending作用于被拒快照删除留下的 pending-delete 标记--ls中的PEND。约 60 秒的轮询器已经能完成租约导出、多余克隆与已结束的 decouple--retry-pending针对的是无租约导出、失败销毁、以及不想等轮询器的情形。标记持久化到prefix/meta/pending-deletes.json那次 PUT 前的崩溃会遗忘意图。用rcow_cancel_pending_delete撤销标记完整契约见主 README 的 Retrying a refused snapshot delete 一节包括轮询器已提交 destroy 时的竞争。run_pending_delete_test.sh第 [11] 步用rcow_pending_load_hold延迟该注册表的 HEAD 与 GET 跨越 unload仅测试用生产从不停放 attach。调试挂死 target 时它尤其有用被测试脚本遗留的进程可以直接询问其状态。s3_prefix_rm.py按前缀删除 S3 对象rcow_unload_lvstore刻意不删对象下次 attach 需要它们所以每轮测试都留些残留崩溃的一轮还会留下 owner marker使下一次 create 直接报 -EBUSY。这个脚本就是清理工具。纯标准库手写 SigV4——测试机既没有 aws-cli 也没有 boto3而需要清理的场合往往正是没有 target 在跑的时候进程已崩溃。凭据只从环境读取。test/tools/s3_prefix_rm.py -e cos.ap-nanjing.myqcloud.com -b bucket -r ap-nanjing -p rcvs/ --list test/tools/s3_prefix_rm.py -e cos.ap-nanjing.myqcloud.com -b bucket -r ap-nanjing -p rcvs/ test/tools/s3_prefix_rm.py -e minio-host -b bucket --path-style -p check_binary_fresh.sh拒绝针对陈旧二进制跑测试启动 target 的五个 dataplane 脚本dataplane/recovery/snapshot/export/activation都会先调用它。若源码比app/s3lvol_tgt/s3lvol_tgt新立即失败。检查存在的原因是一次真实事故check_binary_fresh.sh顶层 Makefile 默认目标当时不含app/make只重建库、二进制原地不动于是一整轮 39/39 32/32 32/32 55/55 全绿——用的却是三小时前、甚至不含待验证修复的二进制。绿跑中没有任何线索唯一痕迹是日志里一个早已改名的函数名。陈旧二进制的绿比红更糟它是指向错误方向的证据而人们会把它当作结论。Makefile 已修复但通往该状态的路不止一条make shared、中断的构建、重建了另一棵树所以检查放在测试侧——放错结论会被得出的那一侧。它只比较 mtime。若确实需要旧二进制比如二分定位设S3LVOL_SKIP_FRESH_CHECK1。check_layering.sh三条分层硬规则integration 测试假设lib/可独立链接本检查强制维持该假设的三条分层规则HANDOFF 第 8 节作为make check/make check-rules的一部分运行。调试失败时的已知陷阱test/README.md收尾部分浓缩了实测踩过的四个坑target 无法启动、报Cannot create lock on core 0上一轮的 target 还活着。pkill -f s3lvol_tgt。pgrep -x s3lvol_tgt找不到明显在跑的目标SPDK 启动 reactor 后会把主线程改名为reactor_0/proc/pid/comm里不再有s3lvol_tgt。应匹配/proc/pid/exercow_target_instances()就是这么做的。这不是琐事rcow_start.sh曾用pgrep -x判断target 是否已在运行检查静默失效后放进了第二个 target新进程接管了/var/run/s3lvol.sockRPC server 绑定前会 unlink 它——第一个进程仍活着握着 WAL 镜像却再也够不着。create 报-EBUSYS3 里还留着别人的 owner marker通常是上一轮被 kill 的。确认无进程运行后用s3_prefix_rm.py清掉或带force: trueattach。scripts/rcow_start.sh在 marker 指向本节点且该 pid 未运行时自动确认并重试。改了头文件但行为看起来没变曾经可能Makefile 不追踪头依赖同一 struct 在不同.o里有不同布局。-MMD -MP已修复仍存疑就make clean一次排除。改了代码但行为没变且脚本报陈旧二进制执行它让你做的make。注意顶层默认目标现在也会构建二进制make shared只构建库、不更新s3lvol_tgt。总结这套测试体系可以带走什么CubeS3lvol 的test/目录是一份可复用的存储组件回归测试设计样本。值得带走的实践包括用退出码 2 隔离环境不允许与真实失败把套件前置条件集中在单一入口而非散布各处对合并类操作快照删除、导出导入断言数据而非返回码用墙钟时间而非轮询计数等待 I/O把陈旧二进制当作一等公民拒绝而非提醒以及让最敏感于全局状态的套件殿后作为整轮清理是否彻底的最终裁决。配合 CubeS3lvol/README.md 与 run_all.sh任何开发者都可以在三分钟内在自己的机器上复现这套从离线单测到 S3 数据面端到端的完整回归。【免费下载链接】CubeSandboxInstant, Concurrent, Secure Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考