Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions conf/springConfigXml/volumeSnapshot.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
</zstack:plugin>
</bean>

<bean id="VolumeSnapshotGroupCascadeExtension" class="org.zstack.storage.snapshot.group.VolumeSnapshotGroupCascadeExtension">
<zstack:plugin>
<zstack:extension interface="org.zstack.core.cascade.CascadeExtensionPoint"/>
</zstack:plugin>
</bean>

<bean id="L3NetworkMemorySnapshotGroupReference" class="org.zstack.storage.snapshot.group.L3NetworkMemorySnapshotGroupReference">
<zstack:plugin>
<zstack:extension interface="org.zstack.storage.snapshot.group.MemorySnapshotGroupReferenceFactory" />
Expand Down
50 changes: 50 additions & 0 deletions docs/snapshot-single-delete/00-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 单快照节点删除(scope=single)— 总览

> 需求:ZSV-5799 "支持删除快照不删除链"
> 关联 MR:zstack#7674 / premium#10776 / zstack-utility#5743
> 入口 API:`APIDeleteVolumeSnapshotGroupMsg`(含 `direction` + `scope` 字段)

---

## 文档索引

| 文档 | 内容 |
|---|---|
| [01-api-and-fields.md](01-api-and-fields.md) | API 入口、字段、枚举定义 |
| [02-call-chain.md](02-call-chain.md) | 处理链路总览(Group → Tree → Storage) |
| [03-direction-resolution.md](03-direction-resolution.md) | `resolveDirection()` 决策表与 fromVOs 构建 |
| [04-scope-and-stepDelete.md](04-scope-and-stepDelete.md) | scope 分支与 stepDelete 递归 |
| [05-commit-db-swap.md](05-commit-db-swap.md) | Commit 路径 DB 翻转(最关键) |
| [06-pull-db-rewrite.md](06-pull-db-rewrite.md) | Pull / pullToVolume DB 改写 |
| [07-group-passthrough.md](07-group-passthrough.md) | Group 透传与并发、失败聚合 |
| [08-hypervisor-online-commit.md](08-hypervisor-online-commit.md) | 在线 libvirt blockCommit + pivot |
| [09-agent-qemu-img.md](09-agent-qemu-img.md) | agent 端 qemu-img 三种命令对比 |
| [10-storage-backend-matrix.md](10-storage-backend-matrix.md) | Local/NFS/SMP/SharedBlock/Ceph 后端差异 |
| [11-sibling-rebase.md](11-sibling-rebase.md) | 分叉链兄弟节点 rebase |
| [12-fullrebase-and-cleanup.md](12-fullrebase-and-cleanup.md) | fullRebase 树根删除与残留清理 |
| [13-premium-and-cdp.md](13-premium-and-cdp.md) | Premium / CDP / 灾备兼容性 |
| [14-limitations-and-todos.md](14-limitations-and-todos.md) | 已知限制 / TODO / FIXME |

---

## 一图概览

```
[祖父] ── [待删节点 X] ── [子 Y] ── ...
┌──────────┴───────────┐
│ scope=single │
│ direction=commit │ 在线VM 且 X≠latest
│ → Y 差量写入 X 文件 │
│ → DB: 互换 path, Y.parent=X.parent
│ direction=pull │ 离线 或 X=latest
│ → 祖父+X 合并入 Y(rebase)
│ → DB: Y.parent = X.parent
```
Comment on lines +32 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

为代码块补充语言标识以通过 Markdown lint。

Line 32 的 fenced code block 缺少语言类型,当前会触发 MD040。建议改为 ```text(或 mermaid,若后续改成可渲染图)。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 32-32: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/snapshot-single-delete/00-overview.md` around lines 32 - 44, The fenced
code block in docs/snapshot-single-delete/00-overview.md (the ASCII diagram
showing "[祖父] ── [待删节点 X] ── [子 Y] ...") is missing a language tag and triggers
MD040; update the opening fence from ``` to ```text (or ```mermaid if you
convert it to a renderable diagram) so the block is annotated and the linter
passes, ensuring the diagram remains unchanged.


## 仓库根

- `/d/0zw/zw/zstack/` —— 开源主库
- `/d/0zw/zw/premium/` —— Premium(独立 git)
- `/d/0zw/zw/zstack-utility/` —— Python agent
56 changes: 56 additions & 0 deletions docs/snapshot-single-delete/01-api-and-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 01 — API 入口与字段定义

## 1.1 `APIDeleteVolumeSnapshotGroupMsg`(快照组删除)

**文件**:`header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java:24`

```java
@APIParam(required = false, validValues = {"pull", "commit", "auto"})
private String direction = "auto";

@APIParam(required = false, validValues = {"single", "chain", "auto"})
private String scope = "chain"; // 默认保留旧行为
```

REST 路径:`DELETE /volume-snapshots/group/{uuid}`

## 1.2 `APIDeleteVolumeSnapshotMsg`(单快照删除)

**文件**:`header/.../APIDeleteVolumeSnapshotMsg.java:49`

```java
@APIParam(required = false, validValues = {"pull", "commit", "auto"})
private String direction = "auto";

@APIParam(required = false, validValues = {"single", "chain", "auto"})
private String scope = "chain"; // 默认 chain,向后兼容
```

REST 路径:`DELETE /volume-snapshots/{uuid}`

## 1.3 枚举类

### `DeleteVolumeSnapshotDirection` — `header/.../DeleteVolumeSnapshotDirection.java:3`

| 值 | 语义 |
|---|---|
| `Pull("pull")` | 下拉方向:父快照内容合入子快照 |
| `Commit("commit")` | 上提方向:子快照内容合入父快照 |
| `Auto("auto")` | 系统自动判断 |

### `DeleteVolumeSnapshotScope` — `header/.../DeleteVolumeSnapshotScope.java:3`

| 值 | 语义 |
|---|---|
| `Single("single")` | 只删除当前单节点,保留整条链 |
| `Chain("chain")` | 删除当前节点及所有后代(旧默认) |
| `Auto("auto")` | 系统自动判断(实际等同 single) |

## 1.4 传递结构体

`VolumeSnapshotDeletionStructs` — `header/.../VolumeSnapshotDeletionStructs.java:5`
跨层透传 `direction + scope + 快照列表`。

## 1.5 兼容性

API 默认值 `scope = "chain"` 保持向后兼容;**必须显式传 `scope=single`** 才会触发新功能。
47 changes: 47 additions & 0 deletions docs/snapshot-single-delete/02-call-chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 02 — 处理链路总览

## 2.1 快照组删除链路

```
APIDeleteVolumeSnapshotGroupMsg
└─ VolumeSnapshotGroupBase.handle() GroupBase.java:163
└─ handleDelete() GroupBase.java:187
└─ DeleteVolumeSnapshotGroupInnerMsg (携带 scope/direction)
└─ While 循环每个 VolumeSnapshotVO GroupBase.java:212
└─ DeleteVolumeSnapshotMsg(scope,direction)
└─ VolumeSnapshotTreeBase
└─ deletion() TreeBase.java:358
├─ scope=chain → deleteChainFlows() :487
└─ scope=single → deleteSingleFlows() :828
└─ stepDelete() :875
├─ 叶节点 → deleteVolumeSnapshotAndSyncVolumeSize
├─ 单子节点 → resolveDirection → commit() / pull()
└─ 多子节点 → pull() (强制)
```
Comment on lines +5 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

处理链路代码块缺少语言声明(MD040)。

Line 5 的代码围栏建议改成 ```text,避免 markdownlint 报警并提升渲染一致性。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/snapshot-single-delete/02-call-chain.md` around lines 5 - 20, The code
block in the call chain snippet lacks a language tag which triggers markdownlint
MD040; update the opening fence from ``` to ```text so the block is explicitly
marked as plain text (affecting the snippet that shows
APIDeleteVolumeSnapshotGroupMsg, VolumeSnapshotGroupBase.handle(),
handleDelete(), deleteChainFlows(), deleteSingleFlows(), stepDelete(), etc.),
ensuring consistent rendering and silencing the MD040 warning.


## 2.2 关键透传点

`VolumeSnapshotGroupBase.java:221-228`:
```java
DeleteVolumeSnapshotMsg rmsg = new DeleteVolumeSnapshotMsg();
rmsg.setScope(msg.getScope());
rmsg.setDirection(msg.getDirection());
bus.makeTargetServiceIdByResourceUuid(rmsg, VolumeSnapshotConstant.SERVICE_ID, ...);
```

## 2.3 关键类索引

| 文件 | 作用 |
|---|---|
| `header/.../APIDeleteVolumeSnapshotMsg.java:49` | 单快照 API 入口 |
| `header/.../APIDeleteVolumeSnapshotGroupMsg.java:24` | 快照组 API 入口 |
| `storage/.../group/VolumeSnapshotGroupBase.java:212` | Group → 单快照消息分发 |
| `storage/.../VolumeSnapshotTreeBase.java:473` | scope 分支点 |
| `storage/.../VolumeSnapshotTreeBase.java:875` | stepDelete 递归 |
| `storage/.../VolumeSnapshotTreeBase.java:921` | commit() 流程 |
| `storage/.../VolumeSnapshotTreeBase.java:1097` | pull() 流程 |
| `storage/.../VolumeTree.java:364` | resolveDirection 决策 |
| `storage/.../VolumeTree.java:418/471` | updateDatabaseAfter Pull/Commit |
| `plugin/kvm/.../KVMHost.java:1043/1159` | 在线 commit/pull |
| `kvmagent/plugins/vm_plugin.py:3915` | libvirt blockCommit 核心 |
| `zstacklib/utils/linux.py:1389` | qcow2 工具函数 |
101 changes: 101 additions & 0 deletions docs/snapshot-single-delete/03-direction-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 03 — direction 决策(resolveDirection)

## 3.1 核心代码

**文件**:`storage/src/main/java/org/zstack/storage/snapshot/VolumeTree.java:364`

```java
public DeleteVolumeSnapshotDirection resolveDirection(
String targetSnapshotUuid, // 待删节点(dst, 老节点)
String childSnapshotUuid, // 待删节点的子节点(src, 新节点)
String initialDirection, // 用户传入的 direction
boolean targetSnapshotIsLatest, // 待删节点是否 latest
VmInstanceState vmState) {

boolean online =
(vmState == VmInstanceState.Running || vmState == VmInstanceState.Paused)
&& getAliveChainSnapshotUuids().contains(targetSnapshotUuid)
&& getAliveChainSnapshotUuids().contains(childSnapshotUuid);

boolean shouldUseCommitStrategy = current && !targetSnapshotIsLatest && online;

if (Objects.equals(initialDirection, DeleteVolumeSnapshotDirection.Pull.toString())
&& shouldUseCommitStrategy) {
throw new IllegalArgumentException(
"the snapshot will be deleted by block 'commit', but the direction is 'pull', " +
"change the direction to 'commit' or 'auto'.");
}

if (initialDirection == null) return DeleteVolumeSnapshotDirection.Commit;

if (Objects.equals(initialDirection, DeleteVolumeSnapshotDirection.Auto.toString())) {
return shouldUseCommitStrategy
? DeleteVolumeSnapshotDirection.Commit
: DeleteVolumeSnapshotDirection.Pull;
}

return DeleteVolumeSnapshotDirection.fromString(initialDirection);
}
```

## 3.2 决策表

| current | targetIsLatest | online | initialDirection | 结果 |
|---|---|---|---|---|
| 任意 | 任意 | 任意 | `null` | **Commit**(兜底) |
| true | false | true | `pull` | **抛 IllegalArgumentException** |
| true | false | true | `auto` | **Commit** |
| 其它组合 | — | — | `auto` | **Pull** |
| 任意 | 任意 | 任意 | `commit` | **Commit** |
| 任意 | 任意 | 任意 | `pull`(合法) | **Pull** |

## 3.3 关键字段含义

| 字段 | 含义 |
|---|---|
| `current` (`VolumeTree.current`,第38行) | 来自 `VolumeSnapshotTreeVO.current`,true 表示快照链尾连着活跃 volume |
| `targetSnapshotIsLatest` | 来自 `VolumeSnapshotVO.latest = 1`,调用方传 `currentRoot.isLatest()` |
| `aliveChain` | volume 沿 backing chain 上溯到根的所有节点,代表"qemu 当前持有的文件链" |

## 3.4 调用方

`VolumeSnapshotTreeBase.java:904`:
```java
DeleteVolumeSnapshotDirection direction = volumeTree.resolveDirection(
currentRoot.getUuid(), // 待删节点
child.getUuid(), // 子节点
msg.getDirection(), // 用户传入
currentRoot.isLatest(), // 来自 DB
vmState);
```

## 3.5 `VolumeTree.fromVOs()` 构建过程

`VolumeTree.java:260-327`:

1. 校验:至多一个根(`parentUuid == null`)、至多一个 latest
2. 若 `current && 有 latest`,把 **volume 自身作为虚拟叶节点** 挂到 latest 之后(uuid = volume uuid)
3. HashMap 还原 parent/children
4. 从 volume 虚拟节点向上收集 `aliveChain`

```java
// 步骤 3:构建树
Map<String, VolumeSnapshotLeaf> map = new HashMap<>();
for (VolumeSnapshotInventory inv : invs) {
VolumeSnapshotLeaf leaf = map.computeIfAbsent(inv.getUuid(), k -> new VolumeSnapshotLeaf());
leaf.inventory = inv;
if (inv.getParentUuid() != null) {
VolumeSnapshotLeaf parent = map.computeIfAbsent(inv.getParentUuid(), k -> new VolumeSnapshotLeaf());
parent.children.add(leaf);
leaf.parent = parent;
} else {
tree.root = leaf;
}
}

// 步骤 4:计算 aliveChain
if (tree.current) {
VolumeSnapshotLeaf leaf = tree.getSnapshotLeaf(volumeInv.getUuid());
tree.aliveChain = leaf != null ? leaf.getAncestors() : new ArrayList<>();
}
```
102 changes: 102 additions & 0 deletions docs/snapshot-single-delete/04-scope-and-stepDelete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 04 — scope 分支与 stepDelete 递归

## 4.1 scope 分支点

**文件**:`VolumeSnapshotTreeBase.java:473`

```java
if (Objects.equals(msg.getScope(), DeleteVolumeSnapshotScope.Chain.toString())) {
deleteChainFlows(); // 旧行为:删当前 + 所有后代
} else {
deleteSingleFlows(); // single/auto:只删当前节点
}
```

注意:`scope=auto` 也走 `deleteSingleFlows()` 分支;只有显式 `chain` 走级联删除。

## 4.2 stepDelete 完整代码

**文件**:`VolumeSnapshotTreeBase.java:875-918`

```java
private void stepDelete(Completion completion) {
// 1) 从 DB 拉取整棵树最新状态
List<VolumeSnapshotVO> vos = Q.New(VolumeSnapshotVO.class)
.eq(VolumeSnapshotVO_.treeUuid, currentRoot.getTreeUuid()).list();
boolean current = Q.New(VolumeSnapshotTreeVO.class)
.eq(VolumeSnapshotTreeVO_.uuid, currentRoot.getTreeUuid())
.select(VolumeSnapshotTreeVO_.current).findValue();

// 2) 重建内存树
VolumeTree volumeTree = VolumeTree.fromVOs(vos, current, VolumeInventory.valueOf(volume));
List<VolumeSnapshotLeaf> children =
volumeTree.getSnapshotLeaf(currentRoot.getUuid()).getChildren();

// 3) 终止条件:无子节点
if (children.isEmpty()) {
deleteVolumeSnapshotAndSyncVolumeSize(completion);
return;
}

// 4) 递归 completion
Completion comp = new Completion(completion) {
@Override public void success() { stepDelete(completion); }
@Override public void fail(ErrorCode e) { completion.fail(e); }
};

// 5) 找 online 子节点(vm running/paused 且在 aliveChain)
VolumeSnapshotLeaf onlineChild = children.stream()
.filter(c -> volumeTree.isOnline(current, currentRoot.getUuid(), c.getUuid(), vmState))
.findFirst().orElse(null);

VolumeSnapshotLeaf child = children.get(0);

if (children.size() == 1) {
DeleteVolumeSnapshotDirection direction = volumeTree.resolveDirection(
currentRoot.getUuid(), child.getUuid(),
msg.getDirection(), currentRoot.isLatest(), vmState);
boolean online = volumeTree.isOnline(current, currentRoot.getUuid(), child.getUuid(), vmState);
if (direction == Commit) commit(child, volumeTree, online, comp);
else pull(child, volumeTree, online, comp);
} else {
// 多子节点(分叉链)
if (onlineChild != null && child.getUuid().equals(onlineChild.getUuid())) {
child = children.get(1); // 优先处理非 online 子节点
}
boolean online = volumeTree.isOnline(current, currentRoot.getUuid(), child.getUuid(), vmState);
pull(child, volumeTree, online, comp); // 多子节点统一 pull
}
}
```

## 4.3 递归特性

| 维度 | 说明 |
|---|---|
| 终止条件 | `children.isEmpty()` |
| 每次递归 | 处理一个子节点;commit/pull 后子节点数 -1 |
| 最坏深度 | 子节点总数(**不是链深度**) |
| 多子节点策略 | 强制 pull;优先非 online 子节点 |
| 失败处理 | `comp.fail()` 直接上抛,**已完成的中间步骤不回滚**,依赖存储幂等 |

## 4.4 多子节点优先非 online 原因

online 子节点的 backing file 正在被 qemu 持有写 I/O,修改它有风险;
先处理非 online 子节点,把它们逐个 pull 掉;最后 online 子节点剩一个,落入"单子节点"分支正常处理。

## 4.5 特殊短路

`VolumeSnapshotTreeBase.java:836`:
```java
if (VolumeSnapshotConstant.STORAGE_SNAPSHOT_TYPE.toString().equals(currentRoot.getType())
|| Objects.equals(currentRoot.getVolumeType(), VolumeType.Memory.toString())) {
deleteVolumeSnapshotAndSyncVolumeSize(completion);
return;
}
```

CDP / 存储快照 / 内存快照绕过 commit/pull,直接调用存储删除。

## 4.6 VmState 限制

`:854` 仅允许 `Running / Paused / Destroyed / Stopped / Destroying`,其它(如 Migrating / Unknown)直接失败。
Loading