Longhorn 异步拉取远程备份目标:BackupTarget/BackupVolume/Backup CRD 与控制器架构详解
Longhorn 异步拉取远程备份目标:BackupTarget/BackupVolume/Backup CRD 与控制器架构详解
本指南基于 Longhorn 仓库中的增强设计文档 enhancements/20210525-async-pull-backups.md,讲解 Longhorn 如何将"与远程备份目标(S3/NFS)的同步阻塞式通信"重构为"异步拉取 + 集群 CRD 缓存"的最终一致模型。你将掌握三个核心 CRD(BackupTarget、BackupVolume、Backup)的字段语义、backup 命令行(list / inspect-volume / head)的职责拆分、四个控制器(setting / backup_target / backup_volume / backup)的协作循环,以及相关 HTTP API 的前后行为变化,可用于排查或二次开发 Longhorn 备份链路。
背景与问题:阻塞式通信带来的备份列表超时
在 Longhorn 引入异步拉取机制之前,Longhorn manager 与远程备份目标(S3/NFS)之间的通信是阻塞式的:列出备份卷、列出备份等操作需要实时访问远端备份目标并读取其配置文件(volume.cfg、backup_backup_<backup-hash>.cfg)。这种设计在以下场景会产生明显的可靠性问题:
- 远程备份目标中积累了大量备份卷或备份;
- Longhorn 集群与远程备份目标之间的网络延迟较高(例如跨地域 S3);
- 备份目标操作引发的级联故障(如 NFS 短暂不可用)会影响依赖远程备份目标的功能。
其中最直接的痛点是:当用户在 Longhorn GUI 上点击 Backup 页面时,列表请求的默认超时时间为 1 分钟。若备份目标数据量大或延迟高,用户会直接遭遇列表超时。设计文档特别说明,之所以不提供"增大列表超时时间"的设置项,是因为浏览器本身也有超时限制(例如 Google Chrome 不允许用户修改默认超时值)——即使 Longhorn manager 侧放宽超时,浏览器侧依然会中断请求,因此从架构上消除同步阻塞才是正解。
该设计对应的原始问题包括 issue #1761、#1955、#2536、#2543,最终在 Longhorn 1.4 及后续版本中落地(CHANGELOG 中亦有相关后续修复记录,例如 CHANGELOG/CHANGELOG-1.4.0.md 中关于"无法从远程备份目标拉取由另一 Longhorn 系统创建的备份(#4637)"的修复,即属于该机制落地后的边界问题)。
目标与边界(Goals / Non-goals)
目标:降低"列出备份卷"或"列出备份"时的查询延迟,覆盖三种典型场景——备份卷数量庞大、备份数量庞大、集群到远程备份目标的网络延迟高。
非目标(明确不做的事):
- 不自动调整备份目标轮询间隔;
- 不支持多个备份目标(后续由独立的增强 enhancements/20240926-multiple-backup-targets-support.md 演进);
- 不支持备份卷/备份列表的 API 分页。
总体方案:异步拉取并把结果持久化为集群 CR
核心思路是"同步阻塞查询 → 后台轮询同步 + CRD 缓存":
- 后台按轮询间隔异步查询远程备份目标,将备份卷、备份的元数据以 Kubernetes 自定义资源(CR)的形式持久化保存到集群内;
- 用户的列表请求不再直连远程备份目标,而是读取集群内的 CR;
- 集群内 CR 与远程备份目标之间通过
spec.syncRequestAt/status.lastSyncedAt的时间戳比较驱动增量同步,实现最终一致; - 删除操作同样异步化:HTTP 端点只负责打标/删除 CR,由控制器在后台真正删除远端备份数据。
方案的五个组成部分:
- 修改
longhorn/backupstore的list命令行为,新增inspect-volume与head命令(把"列名"与"读配置"分离); - 新增
BackupTargetCRD(保存备份目标 URL、凭证 Secret、轮询间隔); - 新增
BackupVolumeCRD(保存备份卷配置); - 新增
BackupCRD(保存单个备份配置); - 改造既有
setting_controller并新增backup_target_controller、backup_volume_controller、backup_controller三个控制器; - 备份卷/备份相关的 HTTP 端点改为与 CR 交互,不再直连远端。
backupstore 命令行改造:把 list、read、head 彻底分离
原设计中 backup ls 在列出的同时会读取配置,这正是一次列出大量备份时延迟高的根源。改造后三条命令职责分明:
改造前(阻塞式 + 读取配置)
backup ls --volume-only:列出所有备份卷并读取其配置(volume.cfg):
$ backup ls s3://backupbucket@minio/ --volume-only
{
"pvc-004d8edb-3a8c-4596-a659-3d00122d3f07": {
"Name": "pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"Size": "2147483648",
"Labels": {},
"Created": "2021-05-12T00:52:01Z",
"LastBackupName": "backup-c5f548b7e86b4b56",
"LastBackupAt": "2021-05-17T05:31:01Z",
"DataStored": "121634816",
"Messages": {}
},
"pvc-7a8ded68-862d-4abb-a08c-8cf9664dab10": {
"Name": "pvc-7a8ded68-862d-4abb-a08c-8cf9664dab10",
"Size": "10737418240",
"Labels": {},
"Created": "2021-05-10T02:43:02Z",
"LastBackupName": "backup-432f4d6afa31481f",
"LastBackupAt": "2021-05-10T06:04:02Z",
"DataStored": "140509184",
"Messages": {}
}
}
backup ls --volume <volume-name>:列出指定卷下所有备份并读取每个备份的配置:
$ backup ls s3://backupbucket@minio/ --volume pvc-004d8edb-3a8c-4596-a659-3d00122d3f07
{
"pvc-004d8edb-3a8c-4596-a659-3d00122d3f07": {
"Name": "pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"Size": "2147483648",
"Labels": {},
"Created": "2021-05-12T00:52:01Z",
"LastBackupName": "backup-c5f548b7e86b4b56",
"LastBackupAt": "2021-05-17T05:31:01Z",
"DataStored": "121634816",
"Messages": {},
"Backups": {
"s3://backupbucket@minio/?backup=backup-02224cb26b794e73&volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07": {
"Name": "backup-02224cb26b794e73",
"URL": "s3://backupbucket@minio/?backup=backup-02224cb26b794e73&volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"SnapshotName": "backup-23c4fd9a",
"SnapshotCreated": "2021-05-17T05:23:01Z",
"Created": "2021-05-17T05:23:04Z",
"Size": "115343360",
"Labels": {},
"IsIncremental": true,
"Messages": null
},
...
"s3://backupbucket@minio/?backup=backup-fa78d89827664840&volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07": {
"Name": "backup-fa78d89827664840",
"URL": "s3://backupbucket@minio/?backup=backup-fa78d89827664840&volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"SnapshotName": "backup-ac364071",
"SnapshotCreated": "2021-05-17T04:42:01Z",
"Created": "2021-05-17T04:42:03Z",
"Size": "115343360",
"Labels": {},
"IsIncremental": true,
"Messages": null
}
}
}
}
backup inspect <backup>:读取单个备份配置(backup_backup_<backup-hash>.cfg):
$ backup inspect "s3://backupbucket@minio/?backup=backup-fa78d89827664840&volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07"
{
"Name": "backup-fa78d89827664840",
"URL": "s3://backupbucket@minio/?backup=backup-fa78d89827664840&volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"SnapshotName": "backup-ac364071",
"SnapshotCreated": "2021-05-17T04:42:01Z",
"Created": "2021-05-17T04:42:03Z",
"Size": "115343360",
"Labels": {},
"IsIncremental": true,
"VolumeName": "pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"VolumeSize": "2147483648",
"VolumeCreated": "2021-05-12T00:52:01Z",
"Messages": null
}
改造后(只列名,配置单独读取)
backup ls --volume-only:仅列出备份卷名:
$ backup ls s3://backupbucket@minio/ --volume-only
{
"pvc-004d8edb-3a8c-4596-a659-3d00122d3f07": {},
"pvc-7a8ded68-862d-4abb-a08c-8cf9664dab10": {}
}
backup ls --volume <volume-name>:仅列出备份名:
$ backup ls s3://backupbucket@minio/ --volume pvc-004d8edb-3a8c-4596-a659-3d00122d3f07
{
"pvc-004d8edb-3a8c-4596-a659-3d00122d3f07": {
"Backups": {
"backup-02224cb26b794e73": {},
...
"backup-fa78d89827664840": {}
}
}
}
backup inspect-volume <volume>:读取单个备份卷配置(volume.cfg):
$ backup inspect-volume "s3://backupbucket@minio/?volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07"
{
"Name": "pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"Size": "2147483648",
"Labels": {},
"Created": "2021-05-12T00:52:01Z",
"LastBackupName": "backup-c5f548b7e86b4b56",
"LastBackupAt": "2021-05-17T05:31:01Z",
"DataStored": "121634816",
"Messages": {}
}
backup inspect <backup>:语义不变,仍读取单个备份配置:
$ backup inspect "s3://backupbucket@minio/?backup=backup-fa78d89827664840&volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07"
{
"Name": "backup-fa78d89827664840",
"URL": "s3://backupbucket@minio/?backup=backup-fa78d89827664840&volume=pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"SnapshotName": "backup-ac364071",
"SnapshotCreated": "2021-05-17T04:42:01Z",
"Created": "2021-05-17T04:42:03Z",
"Size": "115343360",
"Labels": {},
"IsIncremental": true,
"VolumeName": "pvc-004d8edb-3a8c-4596-a659-3d00122d3f07",
"VolumeSize": "2147483648",
"VolumeCreated": "2021-05-12T00:52:01Z",
"Messages": null
}
新增 backup head <config>:只获取配置元数据(修改时间),用于判断配置是否变化:
{
"ModificationTime": "2021-05-17T04:42:03Z"
}
三个核心 CRD:把远端状态缓存进集群
设计文档规划了三个 CRD,当前仓库的 Helm Chart 中已将它们落地为 longhorn.io 组、v1beta2 版本的正式 CRD(见 chart/templates/crds.yaml),并提供了 CLI 短名:lhbt(BackupTarget)、lhbv(BackupVolume)、lhb(Backup)。
BackupTarget CRD(backuptargets.longhorn.io,短名 lhbt)
保存备份目标的连接信息与轮询状态,对应 CRD 定义位于 chart/templates/crds.yaml,当前 CRD 中 backupTargetName 已被移除,说明该 CRD 落地时已随多备份目标演进调整。设计文档中的核心结构:
metadata:
name: "default" # 备份目标名,最初仅支持单一备份目标
spec:
backupTargetURL: "" # 备份目标 URL(string)
credentialSecret: "" # 备份目标凭证 Secret(string)
pollInterval: 0s # 备份目标轮询间隔(metav1.Duration)
syncRequestAt: null # 请求同步远端备份目标的时间(*metav1.Time)
status:
ownerID: "" # 负责运行备份目标控制器操作的节点 ID
available: false # 远端备份目标是否可用
lastSyncedAt: null # 备份目标最近一次执行 reconcile 的时间
当前 CRD 中保留了 spec.backupTargetURL、spec.credentialSecret、spec.pollInterval、spec.syncRequestedAt,状态中除 available、lastSyncedAt、ownerID 外还扩展了 conditions(记录备份目标不可用的原因),并在 additionalPrinterColumns 中暴露 URL、Credential、Available、LastSyncedAt 等列,便于 kubectl get lhbt 直接观察。
BackupVolume CRD(backupvolumes.longhorn.io,短名 lhbv)
缓存单个备份卷的配置,对应 CRD 定义位于 chart/templates/crds.yaml。设计文档结构:
metadata:
name: "<backup-volume-name>" # 备份卷名称
spec:
syncRequestAt: null # 请求同步远端备份卷的时间(*metav1.Time)
fileCleanupRequired: false # 是否删除远端备份卷配置(bool)
status:
ownerID: "" # 负责运行备份卷控制器操作的节点 ID
lastModificationTime: null # 备份卷配置的最后修改时间(Time)
size: "" # 备份卷大小(string)
labels: {} # 备份卷标签(map[string]string)
createAt: "" # 备份卷创建时间(string)
lastBackupName: "" # 最新备份名(string)
lastBackupAt: "" # 最新备份时间(string)
dataStored: "" # 备份卷已存块数(string)
messages: {} # 调用 longhorn engine 列/查备份卷时的错误信息(map[string]string)
lastSyncedAt: null # 备份卷最近一次同步进集群的时间(*metav1.Time)
当前 CRD 的状态字段基本沿用此设计(createdAt、dataStored、lastBackupAt、lastBackupName、lastModificationTime、lastSyncedAt、messages、ownerID、size、labels),并随功能演进补充了 backingImageName、backingImageChecksum、storageClassName、linkedCloneSourceVolume、linkedCloneSourceSnapshot 等字段(后者用于快速克隆场景)。kubectl get lhbv 可看到 BackupTarget、CreatedAt、LastBackupName、LastBackupAt、LastSyncedAt 等列。
Backup CRD(backups.longhorn.io,短名 lhb)
缓存单个备份的配置,并通过标签关联所属备份卷,对应 CRD 定义位于 chart/templates/crds.yaml。设计文档结构:
metadata:
name: "<backup-name>"
labels:
longhornvolume: "<backup-volume-name>" # 标记该备份所属的备份卷
spec:
fileCleanupRequired: false # 是否删除远端备份配置及关联块文件(bool)
snapshotName: "" # 快照名(string)
labels: {} # 快照备份标签(map[string]string)
backingImage: "" # 备份映像(string)
backingImageURL: "" # 备份映像 URL(string)
status:
ownerID: "" # 负责运行备份控制器操作的节点 ID
backupCreationIsStart: false # 快照备份创建是否已开始(bool)
url: "" # 快照备份 URL(string)
snapshotName: "" # 快照名(string)
snapshotCreateAt: "" # 快照创建时间(string)
backupCreateAt: "" # 快照备份创建时间(string)
size: "" # 快照大小(string)
labels: {} # 快照备份标签(map[string]string)
messages: {} # 调用 longhorn engine 列/查备份时的错误信息(map[string]string)
lastSyncedAt: null # 备份最近一次同步进集群的时间(*metav1.Time)
当前 CRD 中 spec 保留了 snapshotName、labels、backupMode(full/incremental)、backupBlockSize(0 表示沿用默认 2MiB,-1 表示无效,2097152/16777216 为可选块大小)、syncRequestedAt;status 保留了 backupCreatedAt、labels、backupTargetName、compressionMethod、error、lastSyncedAt 等,并在 additionalPrinterColumns 中暴露 SnapshotName、SnapshotSize、SnapshotCreatedAt、BackupTarget、State、LastSyncedAt 列。
控制器架构:四级协调实现最终一致
1. setting_controller(既有控制器改造)
监听 Setting CR settings.longhorn.io 的 backup-target、backup-target-credential-secret、backupstore-poll-interval 三个字段,负责创建/更新默认的 BackupTarget CR;同时按 backupstore-poll-interval 启动一个定时器 goroutine,定时把 BackupTarget CR 的 spec.syncRequestAt 更新为 time.Now()。若轮询间隔为 0,则不更新 spec.syncRequestAt(即关闭自动轮询)。
2. backup_target_controller(新增)
监听 BackupTarget CR 的变化,负责创建/更新/删除 BackupVolume CR 的 metadata 与 spec。Reconcile 步骤:
- 若当前节点 ID ≠ BackupTarget CR 的
spec.responsibleNodeID,跳过; - 若
status.lastSyncedAt ≥ spec.syncRequestAt,跳过(无需同步); - 调用 longhorn engine 执行
backup ls --volume-only列出远端备份卷backupStoreBackupVolumes;若远端不可用:- 置
status.available=false、status.lastSyncedAt=time.Now(); - 跳过本次 reconcile;
- 置
- 列出集群内 BackupVolume CR
clusterBackupVolumes; - 计算差集
backupVolumesToPull = backupStoreBackupVolumes - clusterBackupVolumes,为缺失的备份卷创建 BackupVolume CR(metadata.name); - 计算差集
backupVolumesToDelete = clusterBackupVolumes - backupStoreBackupVolumes,删除多余的 BackupVolume CR; - 重新列出集群内 BackupVolume CR,更新其
spec.syncRequestAt = time.Now()(触发下一层同步); - 更新 BackupTarget CR 状态:
status.available=true、status.lastSyncedAt=time.Now()。
3. backup_volume_controller(新增)
监听 BackupVolume CR,负责删除场景(远端清理)、状态更新与 Backup CR 的创建/删除。Reconcile 步骤:
- 校验当前节点 ID(同前);
- 若收到删除 BackupVolume CR 事件:
- 若 BackupVolume CR
spec.fileCleanupRequired=true,则把所有 Backup CR 的spec.fileCleanupRequired置为true; - 删除对应 Backup CR;
- 若
spec.fileCleanupRequired=true,执行backup rm --volume <volume-name> <url>删除远端备份卷; - 移除 finalizer;
- 若 BackupVolume CR
- 若
status.lastSyncedAt ≥ spec.syncRequestAt,跳过; - 执行
backup ls --volume <volume-name>列出远端备份backupStoreBackups; - 列出集群内 Backup CR
clusterBackups; - 差集
backupsToPull = backupStoreBackups - clusterBackups:创建 Backup CR(metadata.name+metadata.labels["longhornvolume"]=<backup-volume-name>); - 差集
backupsToDelete = clusterBackups - backupStoreBackups:删除 Backup CR; - 执行
backup head <volume-config>获取备份卷配置的最后修改时间,与status.lastModificationTime比较;若未变化,仅更新status.lastSyncedAt并返回(避免无谓重读); - 执行
backup inspect-volume <volume-name>读取备份卷配置; - 依据配置更新 BackupVolume CR 状态,并更新
status.lastModificationTime与status.lastSyncedAt; - 同步更新 Volume CR 的
status.lastBackup与status.lastBackupAt。
4. backup_controller(新增)
监听 Backup CR,负责向远端备份目标创建/删除备份并更新状态。Reconcile 步骤:
- 校验当前节点 ID(同前);
- 若收到删除 Backup CR 事件:
- 若
spec.fileCleanupRequired=true,执行backup rm <url>删除远端备份; - 更新对应 BackupVolume CR 的
spec.syncRequestAt=time.Now(); - 移除 finalizer;
- 若
- 若
spec.snapshotName != ""且status.backupCreationIsStart == false:- 调用 longhorn engine/replica 执行备份创建;
- 置
status.backupCreationIsStart = true; - 派生 goroutine 监控创建进度,当进度达到 100% 时:若 BackupVolume CR 存在则更新其
spec.syncRequestAt = time.Now(),否则创建该 BackupVolume CR(metadata.name);
- 若
status.lastSyncedAt != nil,说明备份配置已同步过,跳过; - 执行
backup inspect <backup-url>读取备份配置; - 按配置更新 Backup CR 状态;
- 更新
status.lastSyncedAt。
从实现上看,"差集同步"是三个新控制器的共同模式:pull 差集(远端有、集群没有 → 创建 CR)、delete 差集(集群有、远端没有 → 删除 CR),从而保证集群内 CR 集合与远端备份目标最终一致。
HTTP API 前后行为对照
设计文档给出了完整的端点行为对照表,改造后所有备份卷/备份相关的读操作都变成读集群内 CR,写/删操作则转为打标 + 控制器异步执行:
| HTTP Endpoint | Before(直连远端) | After(CR 驱动) |
|---|---|---|
GET /v1/backupvolumes |
从远端备份目标读取所有备份卷 | 读取所有 BackupVolume CR |
GET /v1/backupvolumes/{volName} |
从远端读取单个备份卷 | 按卷名读取 BackupVolume CR |
DELETE /v1/backupvolumes/{volName} |
从远端删除备份卷 | 删除 BackupVolume CR,由 backup_volume_controller 协调删除远端备份卷 |
POST /v1/volumes/{volName}?action=snapshotBackup |
直接向远端创建备份 | 创建新 Backup CR,由 backup_controller 协调创建远端备份 |
GET /v1/backupvolumes/{volName}?action=backupList |
从远端读取备份列表 | 按标签过滤 volume=<backup-volume-name> 读取 Backup CR 列表 |
GET /v1/backupvolumes/{volName}?action=backupGet |
从远端读取单个备份 | 按备份名读取 Backup CR |
DELETE /v1/backupvolumes/{volName}?action=backupDelete |
从远端删除备份 | 删除 Backup CR,由 backup_controller 协调删除远端备份 |
两个删除端点的具体流程:
DELETE /v1/backupvolumes/{volName}:先将 BackupVolume CR 的spec.fileCleanupRequired置为true,再删除该 CR,最终由控制器执行远端清理并移除 finalizer;DELETE /v1/backupvolumes/{volName}?action=backupDelete:先将 Backup CR 的spec.fileCleanupRequired置为true,再删除该 CR,最终由backup_controller执行backup rm <url>。
POST /v1/volumes/{volName}?action=snapshotBackup 会先生成备份名,再创建如下 Backup CR:
metadata:
name: <backup-name>
labels:
longhornvolume: <backup-volume-name>
spec:
snapshotName: <snapshot-name>
labels: <snapshot-backup-labels>
backingImage: <backing-image>
backingImageURL: <backing-image-URL>
用户故事与体验提升
设计文档给出了三个典型用户故事,用于验证改造效果:
- Story 1:远端备份目标有大量备份卷且延迟高 → 用户仍能在 GUI 上列出全部备份卷;
- Story 2:远端备份目标有大量备份且延迟高 → 用户仍能在 GUI 上列出全部备份;
- Story 3:用户在 GUI 上创建备份 → 系统先创建 Backup CR,
backup_controller再协调 longhorn engine/replica 真正执行远端备份。
改造后,GUI 的列表请求不再受"远端查询耗时"的直接影响,超时问题从架构上消除。
配置与部署:轮询间隔与默认备份存储
异步拉取的轮询行为由设置 backupstore-poll-interval 控制。在 Helm Chart 中可通过 chart/values.yaml 的 defaultBackupStore 段配置默认值:
defaultBackupStore:
# 默认备份存储端点(可选:"NFS"、"CIFS"、"AWS"、"GCP"、"AZURE")
backupTarget: ~
# 与默认备份目标关联的 Kubernetes Secret 名称
backupTargetCredentialSecret: ~
# Longhorn 等待多久再检查默认备份存储是否有新备份(秒)。
# 默认值为 "300";值为 "0" 时禁用轮询。
pollInterval: ~
这些配置经 chart/templates/default-resource.yaml 写入默认 Setting CR:
backup-target: {{ .Values.defaultBackupStore.backupTarget }}
backup-target-credential-secret: {{ .Values.defaultBackupStore.backupTargetCredentialSecret }}
backupstore-poll-interval: {{ .Values.defaultBackupStore.pollInterval }}
setting_controller 正是监听这三个 Setting 字段来创建/更新默认 BackupTarget CR,并按 backupstore-poll-interval(默认 300 秒,即 5 分钟;设为 0 则完全禁用自动轮询)触发 spec.syncRequestAt 更新。
设计文档还建议提供"立即同步"的能力:可在 Backup 页面提供按钮更新 BackupTarget CR 的 spec.syncRequestAt = time.Now(),或在 Backup → Backup Volume 页面提供按钮更新 BackupVolume CR 的 spec.syncRequestAt = time.Now(),让用户在配置变更后不必等待整个轮询周期。
测试计划与验证要点
设计文档给出的测试围绕高负载、高延迟场景(超过 1000 个备份卷、超过 1000 个备份,且从 longhorn manager 到远端备份目标每次操作延迟 700–800ms):
基础备份/恢复:配置备份目标与 5 分钟轮询 → 在 vol-A、vol-B 各创建两个备份 → GUI 可见对应 BackupVolume 与备份 → 删除单个备份后远端数据随之删除 → 删除 vol-A 备份卷后远端备份卷与其全部备份被删除 → 切换到另一备份目标后看不到 vol-B → 将 backupstore-poll-interval 改为 1 分钟并切回原目标,1 分钟后恢复可见 → 从 vol-B 备份创建卷。
DR 卷操作(双集群共享同一备份目标):集群 A 创建卷并周期性备份 → 集群 B 在轮询周期后能列出备份卷/备份 → 从备份卷创建 DR 卷 → 校验 DR 卷 status.LastBackup/status.LastBackupAt 周期性更新 → 集群 A 删除备份卷 → 集群 B 在轮询周期后该备份卷消失且 DR 卷状态不再更新。
备份目标 URL 清空:配置目标并创建备份 → 将备份目标设置置空 → 轮询触发后:默认 BackupTarget CR status.available=false、status.lastSyncedAt 更新、所有 BackupVolume/Backup CR 被删除、vol-A CR 的 status.lastBackup/status.lastBackupAt 被清理、GUI 显示目标不可用。
切换备份目标 URL:S3 备份 → 切到 NFS 备份 → 再切回 S3 → 轮询触发后 BackupVolume/Backup CR 与 vol-A 状态按 S3 数据重新同步。
凭证 Secret 变更:将备份目标凭证 Secret 置空 → 轮询触发后 status.available=false,GUI 显示目标不可用。
后续演进与现状对照
该设计落地后 Longhorn 持续演进,仓库中可看到与本主题直接相关的后续增强:
- enhancements/20241003-improve-pulling-backups-from-the-backup-target.md:针对 NFS 短时宕机恢复后空响应导致备份数据被误删的问题,引入
DeleteCustomResourceOnly标签——当远端已无对应备份数据时,只删除集群内的BackupVolume/Backup/BackupBackingImage/SystemBackup资源,不再删除远端数据; - enhancements/20240926-multiple-backup-targets-support.md:解除"仅支持单一备份目标"的限制,支撑多备份目标(这也是当前 CRD 中 BackupVolume/Backup 增加
backupTargetName字段的原因); - 当前 CRD 的
Backup还扩展了backupMode(full/incremental)、backupBlockSize(对应 enhancements/20250701-configurable-backup-block-size.md 的可配置备份块大小)等字段; - CHANGELOG/CHANGELOG-1.8.0.md 记录有"使备份删除异步化,并强制备份创建等待直到没有备份正在删除"的改进(issue #8746)。
由此可见,异步拉取架构从 2021 年的增强提案起步,逐步成为 Longhorn 备份链路的基础设施,并围绕可靠性(误删防护)、多目标支持、备份模式等方向持续完善。
小结
本增强的核心价值可以概括为三点:列表与读配置分离(backupstore 命令职责拆分)、远端状态集群化(三个 CRD 缓存备份目标/备份卷/备份元数据)、控制器异步协调(差集同步 + 时间戳驱动 + finalizer 清理),最终让"备份列表超时"从架构层面消失,并为后续的多备份目标、备份块大小可配置等能力打下基础。若需深入源码,可重点研读 chart/templates/crds.yaml 中三个 CRD 的字段定义,以及 chart/values.yaml 的默认备份存储配置,结合本文的 reconcile 步骤理解数据流。