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
2 changes: 1 addition & 1 deletion docs/modules/core/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
** xref:in-process-microservice.adoc[]
** xref:scalability_secrets_part_1_asynchronous_architecture.adoc[]
** xref:stateless_services.adoc[]
** xref:message.adoc[]
** xref:message.adoc[]
129 changes: 129 additions & 0 deletions docs/modules/core/pages/live-primary-storage-migrate-volume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# ZSPHER-244 API 设计权衡:扩展现有 API vs 新增 API

## 方案速览(TL 确认)

- 工单:ZSPHER-244 冷迁多盘各异目标
- 前置:ZSV-12280 热迁多盘已落
- 痛点:老 API 已叠两层互斥语义
- A:老 API 加 specs 字段补丁
- B:新增独立 API(推荐)
- 理由:语义干净 校验解耦 灰度可控
- 参考:冷迁 vs 热迁 VM 早分两 API
- 待确认:仅冷迁 字段命名 上线节奏

---

# ZSPHER-244 API 设计权衡:扩展现有 API vs 新增 API

> 关联工单:[ZSPHER-244](http://jira.zstack.io/browse/ZSPHER-244) — 更改数据存储支持数据盘指定不同目标
> 关联前置:[ZSV-12280](http://jira.zstack.io/browse/ZSV-12280) — SharedBlock 多数据盘热迁移
> 状态:设计阶段
> 决策建议:方案 B(新增独立 API)

---

## 1. 背景

`APIPrimaryStorageMigrateVolumeMsg` 当前承担的语义已经叠了两层:

| 演进阶段 | 字段 | 适用场景 |
|---------|------|---------|
| 最早 | `volumeUuid`(path) + `dstPrimaryStorageUuid` | 单盘冷迁移(VM Stopped 或未挂载) |
| ZSV-12280(已落) | `volumeUuids: List<String>` + `dstPrimaryStorageUuid` | 多盘热迁移(VM Running/Paused,**统一目标 PS**) |
| ZSPHER-244(待做) | 每盘独立目标:`List<{volumeUuid, dstPsUuid}>` | 多盘**冷迁移**(VM Stopped,**每盘不同目标**) |

三种语义共用一个 API 已经出现"互斥字段"问题(`volumeUuid` 与 `volumeUuids` 必须二选一/包含关系)。ZSPHER-244 是否继续叠加,需要明确决策。

---

## 2. 方案 A:继续给 `APIPrimaryStorageMigrateVolumeMsg` 加参数

新增字段 `volumeMigrationSpecs: List<VolumeMigrationSpec{volumeUuid, dstPrimaryStorageUuid}>`。

### 优点
- 前端入口统一(一个 API 处理 volume 迁移所有场景)
- SDK 兼容:旧字段保留
- LongJob 调度复用现有 `PrimaryStorageMigrateVolumeJob`

### 缺点(多数已经显现)
1. **互斥字段爆炸**:`volumeUuid` / `volumeUuids` / `volumeMigrationSpecs` / `dstPrimaryStorageUuid` 之间需要 5+ 条互斥规则,Interceptor 校验逻辑将剧增。
2. **语义模糊**:同一 API 既是热迁移又是冷迁移,既支持单目标又支持多目标——文档和 SDK 注释难写。
3. **路径占位符 `volumeUuid` 越来越尴尬**:当请求是「按盘指定目标」时,path 上的 volumeUuid 是哪一块?需要继续打补丁说"必须是 specs 中的某一块"。
4. **审计/事件 inventory 形状不一致**:单目标返回 `VolumeInventory`,多目标返回 `List<VolumeInventory>`。老调用方期望单个,新调用方期望列表,已经在用 `inventories.get(0)` 凑活。
5. **MN 升级风险**:当前 `PrimaryStorageMigrateVolumeJob.start` 用 `JSONObjectUtil.toObject` 反序列化 jobData,加字段虽兼容但分支判断越来越复杂。

---

## 3. 方案 B:新增独立 API `APIPrimaryStorageMigrateDataVolumesMsg`

专门承载 ZSPHER-244 语义:

```java
@Action(category = VolumeConstant.ACTIONS)
public class APIPrimaryStorageMigrateDataVolumesMsg extends APIMessage {
@APIParam(resourceType = VmInstanceVO.class)
private String vmInstanceUuid;

@APIParam(nonempty = true)
private List<VolumeMigrationSpec> volumeMigrationSpecs;
// VolumeMigrationSpec: volumeUuid + dstPrimaryStorageUuid (+ optional withSnapshots)
}
```

### 优点
1. **语义干净**:一个 API 一种语义,文档/SDK/审计天然清晰。
2. **Interceptor 简单**:只校验自己的字段,与老 API 完全解耦。
3. **入口分流**:冷迁移多目标走新 Msg → 新 Job(`PrimaryStorageMigrateDataVolumesJob`);老 API 不动,回归"经典单盘迁移"角色。
4. **Inventory 形状自然**:新事件直接返回 `List<VolumeInventory>`,不用兼容老形状。
5. **演进空间**:未来 ZSPHER-244 的衍生需求(按盘选 withSnapshots、按盘选 strategy 等)只在新 Msg 上扩展,不污染老路径。
6. **可独立灰度**:新 API 出问题不影响存量。

### 缺点
1. 前端要多一个 API 调用入口(但 UI 本来就是新的高级选项面板,天然走新接口)。
2. SDK 需要重新生成(项目里本来就是必经流程)。
3. 多了一个 LongJob 类(但本来就需要新逻辑,写在哪里都一样)。

---

## 4. 同类先例参考

ZStack 自身的 API 演进经验:

- `APIMigrateVmMsg`(冷)/ `APILiveMigrateVmMsg`(热)—— **是分开的两个 API**,没有共用 `migrateVm` 加 `live: boolean`。
- `APIChangeVmHaPolicyMsg` / `APISetVmInstanceDefaultCdRomMsg` 这类语义独立的操作,从不强行复用旧 API。
- 当年 `volumeUuids` 加到 `APIPrimaryStorageMigrateVolumeMsg` 已经是历史包袱(即 ZSV-12280 当前进行中的工作),现在如果再叠一层 multi-target,会让这个 API 彻底变成"什么都能干的 god API"。

---

## 5. 推荐:方案 B(新增独立 API)

### 关键理由
1. ZSPHER-244 是 **冷迁移 + 每盘独立目标**,与 ZSV-12280 的 **热迁移 + 统一目标** 实际是两条完全独立的执行链路:
- 冷迁移:`MigrateVolumeOnPrimaryStorageMsg` fan-out(每盘可走不同目标 PS)
- 热迁移:KVM live storage migration workflow(per-volume LUN switch)

共用 API 只是表面省事,后端早晚要按字段分流。

2. **Interceptor 复杂度上限**:现在 `validateLiveMigrateMultiDataVolumes` 已经接近 80 行,再叠一种语义会到 150+ 行,可维护性崩盘。

3. **老 API 已经背了一个互斥规则补丁**(path `volumeUuid` 必须 ∈ body `volumeUuids`),再加规则会让前端联调和文档维护痛苦。

### 建议落地形态

- 新增 `APIPrimaryStorageMigrateDataVolumesMsg` + `APIPrimaryStorageMigrateDataVolumesEvent`
- 新增 `PrimaryStorageMigrateDataVolumesJob`(LongJob),内部串行/并行调用现有 `MigrateVolumeOnPrimaryStorageMsg`(每盘一个目标 PS)
- Interceptor 单独写 `validate(APIPrimaryStorageMigrateDataVolumesMsg)`,与 multi-volume 热迁移校验完全分离
- 老 `APIPrimaryStorageMigrateVolumeMsg.volumeUuids` 路径继续仅服务 ZSV-12280 热迁移场景,不改动

### 边界情况
- 如果 PD 要求"按盘选目标"也能用于**热迁移**,再单独评估是否要在新 API 上加 `live: boolean`,或者再开一个 `APILiveMigrateDataVolumesToDifferentTargets`。
- 但 ZSPHER-244 原文明确写「SAN ↔ SAN **冷迁移**」,目前不必预留。

---

## 6. 决策待办

- [ ] 与 PD 确认 ZSPHER-244 是否仅冷迁移场景
- [ ] 与前端确认是否接受新 API(UI 本身就是新面板,预计无阻力)
- [ ] 与架构组评审字段命名 `volumeMigrationSpecs` vs `volumeTargets`
- [ ] 确认审计 inventory 形状(List 返回是否需要追加 `failedVolumes` 字段)
165 changes: 165 additions & 0 deletions docs/modules/core/pages/primary-storage-migrate-volume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# APIPrimaryStorageMigrateVolumeMsg 冷迁移数据盘逻辑分析

## 方案速览(TL 确认)

- 目标:开放 Ceph/NFS 不卸盘冷迁
- 触发条件:VM 必须 Stopped
- 校验:dstPS 需共享 VM 网络集群
- 候选 PS:挂载态按 VM nic L2 过滤
- 老语义:未挂载/Root 行为不变
- SB 已支持 不变(沿用旧路径)
- 风险拆分:startVm 并发另起 MR
- 工单:code-ZSV-12280(已实现)

---

本文整理 `APIPrimaryStorageMigrateVolumeMsg` 在各存储类型下的限制与执行流程,重点说明 Ceph / NFS / SharedBlock 对数据盘冷迁移的差异化约束及其背后的设计动机。

## 一、API 概览

- **类位置**: `premium/mevoco/src/main/java/org/zstack/storage/migration/primary/APIPrimaryStorageMigrateVolumeMsg.java`
- **REST**: `PUT /primary-storage/volumes/{volumeUuid}/actions`
- **默认超时**: 72 小时
- **入参**: `volumeUuid`、`dstPrimaryStorageUuid`
- **APINoSee(拦截器自动填充)**: `srcPrimaryStorageUuid`、`type`、`vmInstanceUuid`

## 二、API 拦截校验(`StorageMigrationApiInterceptor`)

### 通用约束

1. 源/目标 PS 不能为 `Disabled` 或 `Maintenance`
2. 卷 `Status == Ready`、`State != Disabled`
3. `srcPS == dstPS` 时仅 **Ceph** 允许(支持同 PS 换 pool)

### 数据盘专属约束

```java
if (srcVolume.getVmInstanceUuid() != null) {
if (!srcPS.isSharedBlock()) {
throw "please detach it before migration"; // Ceph / NFS
} else if (state != Stopped) {
throw "vm is not stopped"; // SharedBlock
}
}
```

- **Ceph / NFS**: 数据盘只要有挂载关系就拒绝(不看 VM 状态)
- **SharedBlock**: 允许挂载,但 VM 必须 Stopped
- **Shareable 共享卷**: 一律拒绝

### 存储类型矩阵

定义于 `conf/springConfigXml/storageMigration.xml` 的 `primaryStoragePrimaryStorageMetrics`:

| 源 → 目标 | 是否允许 |
|---|---|
| Ceph → Ceph | ✅ |
| NFS → NFS | ✅ |
| SharedBlock → SharedBlock | ✅(要求共享 cluster) |
| Local → Local | ❌ 单卷 API 不支持 |
| 跨类型(如 NFS→Ceph) | ❌ |

## 三、执行流程

```
APIPrimaryStorageMigrateVolumeMsg
↓ StorageMigrationBase.handle
PrimaryStorageMigrateVolumeMsg (本地消息)
↓ handle(PrimaryStorageMigrateVolumeMsg)
↓ 数据盘分支
MigrateDataVolumeOverlayMsg (以 volumeUuid 为 sync 锁)
MigrateVolumeOnPrimaryStorageMsg
↓ 按 type 装配 FlowChain
执行迁移
```

> **注**:数据盘路径**不修改 VM 状态**。Root 盘冷迁移才有 `volumeMigrating → volumeMigrated` 状态机,并在迁移结束后通过 `setClusterAndLastHost` 重新计算 VM 可调度集群。

## 四、各存储 FlowChain 编排

| 类型 | Flow 序列 |
|---|---|
| Ceph → Ceph | `ReserveCapacityFromDstPSFlow` → `CephToCephMigrateVolumeFlow`(rbd export-diff / import-diff,按 snapshot 分 segment) → `UpdateVolumeVOFlow` → `DiscardVolumeReferenceFlow` → `CephDeleteVolumeFromSrcPSFlow` |
| NFS → NFS | `ReserveCapacityFromDstPSFlow` → `NfsToNfsMigrateVolumeFlow` → `UpdateVolumeVOFlow` → `DiscardVolumeReferenceFlow` → `NfsDeleteVolumeFromSrcPSFlow` |
| SharedBlock → SharedBlock | 同模式,由 `sharedblock` 子模块工厂提供 |

`UpdateVolumeVOFlow` 只改 `primaryStorageUuid` 和 `installPath`,**不动 `vmInstanceUuid`、设备地址、bus、cache** 等元数据。

## 五、候选 PS 计算(`getPrimaryStorageCandidatesForVolumeMigration`)

冷迁移路径:

1. 过滤 PS 状态正常(非 Disabled/Maintenance/Disconnected)
2. 按类型矩阵过滤(Ceph→Ceph / NFS→NFS / SB→SB)
3. **SharedBlock 额外约束**:`primaryStorageNotSupportCrossClusterMigrationMetrics` 要求源/目标 PS 共享 cluster
4. 是否包含源 PS 自身:仅当 `VmStorageMigrationMetric.isSupportSameStorage() == true`(Ceph 满足)

## 六、各存储限制对比

| 维度 | Ceph→Ceph | NFS→NFS | SharedBlock→SharedBlock | Local→Local |
|---|---|---|---|---|
| 单卷 API 允许 | ✅ | ✅ | ✅ | ❌ |
| 同 PS 迁移 | ✅(换 pool) | ❌ | ❌ | — |
| 跨 cluster 迁移 | ✅ | ✅ | ❌(必须共享 cluster) | — |
| 数据盘可挂载迁移 | ❌ 须 detach | ❌ 须 detach | ✅ VM 必须 Stopped | — |
| Shareable 卷 | ❌ | ❌ | ❌ | — |
| 实际主流场景 | 同 PS 不同 pool | 跨 NFS PS | 同 SB 集群内 LV 路径变更 | — |

## 七、关键问题:Ceph / NFS 为什么强制 detach?

经过逐层排查,否定了若干常见的"技术原因":

| 误解 | 否定理由 |
|---|---|
| QEMU 句柄冲突 | VM 关机时没有 QEMU 进程,不存在 fd / librbd watcher 冲突 |
| 设备号 / bus / cache 元数据需要重建 | 这些字段与 PS 类型无关,迁移流程根本不动它们 |
| 目标 PS 不在 VM 当前 cluster 导致起不来 | Ceph / NFS 是共享存储,集群所有 host 都能访问;`PrimaryStorageClusterRefVO` 仅用于调度暴露,不构成访问壁垒 |

### 真实原因:产品语义切分

ZStack 把"卷迁移"这件事在 API 层切成两条路径:

- **单卷迁移 API**(`APIPrimaryStorageMigrateVolumeMsg`)= 处理**游离卷**
- **VM 迁移 API**(`APIPrimaryStorageMigrateVmMsg with withDataVolumes`)= 处理**挂着的数据盘**

代码作者在 2017 年画了这条边界,把"挂着迁数据盘"的责任推给 VM 迁移 API,单卷 API 不去处理这种场景的复杂度(即使 Ceph / NFS 共享存储完全做得到)。

### SharedBlock 为何例外

SharedBlock 用 LVM LV 做 volume。一次 detach 涉及:

- 在 host 上 `lvchange -an` 停用 LV
- 释放 lvmlockd 锁
- 清理 multipath / device-mapper 映射

attach 时全部反向重做一遍。代价远高于 NFS 改文件路径或 Ceph 改 rbd path。如果对 SharedBlock 也强制 detach,体验会非常糟糕。

折中方案:**允许挂着迁,但要求 VM Stopped**——避开 LV 在线变更与 QEMU 的并发风险。

> **重要**:技术上 Ceph / NFS 完全可以做"挂着迁数据盘",只是没人写这段代码。这是产品边界,不是技术限制。

## 八、关键文件索引

| 文件 | 作用 |
|---|---|
| `primary/APIPrimaryStorageMigrateVolumeMsg.java` | API 消息定义 |
| `StorageMigrationApiInterceptor.java:245-372` | 入参校验 |
| `StorageMigrationBase.java:213` | API → 本地消息 |
| `StorageMigrationBase.java:893` | 数据盘 vs Root 盘分支 |
| `StorageMigrationBase.java:1402` | `migrateVolume` 装配 FlowChain |
| `StorageMigrationBase.java:1624` | 候选 PS 计算 |
| `conf/serviceConfig/storageMigration.xml` | 消息路由 |
| `conf/springConfigXml/storageMigration.xml` | 类型矩阵 + ChainFactory + Metric Bean |
| `primary/ceph/CephToCephMigrateVolumeFlow.java` | Ceph 迁移核心(segment + import-diff) |
| `primary/nfs/NfsToNfsMigrateVolumeChainFactory.java` | NFS 迁移工厂 |
| `primary/local/LocalToLocalMigrateVolumeChainFactory.java` | Local 迁移工厂(仅服务 VM 整机迁移) |
| `sharedblock/SharedBlockToSharedBlockVmStorageMigrationMetric.java` | SB 能力声明 |

## 九、一句话结论

> `APIPrimaryStorageMigrateVolumeMsg` 是**游离数据盘的冷迁移 API**,仅支持同类型存储间迁移
> (Ceph→Ceph / NFS→NFS / SharedBlock→SharedBlock)。
> Ceph 唯一允许同 PS 内换 pool(这是它真实的主流场景);
> SharedBlock 唯一允许卷处于挂载态迁移(用 VM Stopped 替代 detach,避免 LV 在线变更代价)。
> **对挂载态数据盘强制 detach 是产品边界,不是技术限制**——挂着的数据盘请走 VM 迁移 API。
Loading