☰
Cinder备份自动化脚本助手:从不可控到可观测的运维实践
2026/10/14 12:04:03 网站建设 项目流程

简介:cinderback 是一份面向 OpenStack 运维与云平台开发者的 Python 参考脚本,用于简化 Cinder 卷备份与还原流程,并针对 Cinder Backup Service 的现有局限提供变通方案。它支持按租户或全部租户批量备份、还原卷,管理员可在完成备份后对属主租户隐藏备份,也能灵活恢复并控制原始租户的可见性;同时提供备份轮换、按原始卷 ID 或备份 ID 还原、保留卷名称与描述、借助临时快照处理使用中卷,以及自动备份元数据的批量导出与导入。脚本仅依赖 Python 2.7 与 cinderclient v1.1.1 及以上版本,旧版本客户端会因缺少多租户选项支持而损失部分功能。资源包共 4 个文件,以 py 主脚本为核心,辅以 txt 依赖说明、md 使用文档与 gitignore 配置,压缩后约 11KB,结构轻量、便于直接阅读与二次改造。目前已有 313 人学习,适合需要快速搭建卷备份还原工作流、理解 Cinder 备份限制与规避思路的读者参考。

1. 当 Cinder 备份开始“玄学”:为什么需要一个脚本助手

凌晨两点,某公司私有云平台的运维群里弹出一条告警:生产卷的备份任务连续三天失败,日志里只有一句Backup driver reported an error。值班的 A 同学把 Cinder 的备份服务重启了一遍,任务恢复了,但没人说得清刚才到底发生了什么。这种场景在 OpenStack 环境里并不罕见——Cinder 备份本身能跑,但一旦涉及增量、多后端、跨存储迁移,排查成本就会指数级上升。

cinderback这个方向要解决的,正是这类“备份能跑但不可控”的问题。它不是 Cinder 的替代品,而是一层脚本助手:把 Cinder 原生的 backup API、快照链路、后端驱动状态串起来,用可重复的脚本完成备份创建、状态巡检、失败重试和元数据归档。适合两类人:一是手里管着几十到几百个卷、靠手工点 Dashboard 已经忙不过来的云运维;二是想给 Cinder 备份做二次封装、但又不想改 Cinder 源码的开发者。核心诉求就一个——让备份这件事从“碰运气”变成“可观测、可复现”。

2. 拆开 Cinder 备份链路:脚本助手到底该接在哪一层

2.1 Cinder 备份的三个关键对象:卷、快照、备份记录

要写脚本助手,先得把 Cinder 里三个容易混淆的对象分清楚。卷(volume)是实际挂给虚机的块设备;快照(snapshot)是卷在某个时间点的只读副本,通常留在同一后端;备份(backup)才是落到独立备份存储(如对象存储或 NFS)的持久化数据。Cinder 的备份 API 允许直接对卷做备份,也允许先打快照再备份,两条路径的底层行为差别很大。

直接对在用卷做全量备份时,Cinder 会创建一个临时快照来保证数据一致性,备份完成后销毁。如果卷正在被高频写入,这个临时快照的创建可能因为后端驱动不支持而失败。常见做法是:对数据库类卷先做应用层静默,再调 Cinder 快照,最后基于快照做备份。脚本助手要做的第一件事,就是根据卷的metadata或volume_type判断走哪条路径,而不是无脑调openstack volume backup create。

另一个关键点是备份的container和availability_zone参数。container决定备份数据写到哪个对象存储容器,availability_zone决定由哪个 cinder-backup 服务处理。如果这两个参数和实际后端不匹配,任务会卡在creating状态直到超时。脚本里必须显式指定,不能依赖默认值。

2.2 用 openstackclient 还是直接调 REST API

写脚本助手时第一个选型问题:用openstack命令行还是直接请求 Cinder REST API。openstack命令上手快,但每次调用都要走一遍认证和端点发现,批量操作时开销明显;而且它的输出格式在不同版本间有差异,解析起来容易翻车。直接调 REST API 更可控,但需要自己处理 token 刷新和分页。

我一般会这样分层:巡检类、低频操作用openstack命令,因为可读性好、调试方便;批量创建、状态轮询用 Python 的keystoneauth1+requests直接打 Cinder API。下面是一个最小可用的 REST 调用示例,完成 token 获取和备份列表查询:

import requests from keystoneauth1.identity import v3 from keystoneauth1 import session # 认证信息从环境变量读取,避免硬编码 auth = v3.Password( auth_url="http://controller:5000/v3", username="backup_svc", password="__from_env__", project_name="service", user_domain_name="Default", project_domain_name="Default" ) sess = session.Session(auth=auth) # 获取 Cinder 端点,service_type 固定为 volumev3 endpoint = sess.get_endpoint(service_type="volumev3", interface="internal", region_name="RegionOne") # 查询所有处于 error 状态的备份,limit 控制单页大小 resp = requests.get( f"{endpoint}/backups/detail", headers={"X-Auth-Token": sess.get_token()}, params={"status": "error", "limit": 50}, timeout=30 ) resp.raise_for_status() for bk in resp.json()["backups"]: print(bk["id"], bk["volume_id"], bk["status"], bk["fail_reason"])

这段代码的逻辑说明:keystoneauth1负责认证和 token 生命周期,get_endpoint拿到的是内部端点,避免走公网。/backups/detail比/backups多返回fail_reason和volume_id,排查时必须用 detail 接口。limit参数建议设 50 到 100,太大容易在备份量大时拖慢响应。参数上,interface选internal还是public取决于脚本运行位置;region_name在多 region 环境里必须显式指定,否则可能查到别的 region 的备份。

注意:fail_reason字段在部分 Cinder 版本里可能为空,此时需要结合 cinder-backup 服务的日志一起看,不能只依赖 API 返回。

2.3 备份状态机:哪些状态可以重试,哪些必须人工介入

Cinder 备份的状态流转大致是creating→available或error,删除时经过deleting→deleted。脚本助手最核心的判断逻辑就在这里:不是所有 error 都能重试。如果fail_reason里出现InsufficientFreeSpace或BackendNotFound,重试一百次也没用,必须先处理存储空间或后端注册;如果是ServiceUnavailable或ConnectionError,则适合加入重试队列。

我通常会在脚本里维护一张状态处置表:

状态典型 fail_reason 关键词处置动作
errorServiceUnavailable延迟 5 分钟后重试,最多 3 次
errorConnectionError检查 cinder-backup 服务,重试
errorInsufficientFreeSpace停止重试,告警人工扩容
errorBackendNotFound停止重试,检查后端配置
creating超过 2 小时未变标记为疑似卡死,人工确认

这张表是脚本的“决策大脑”。没有它,重试逻辑就会变成盲目循环,既浪费资源又掩盖真实故障。参数上,重试间隔建议用指数退避:第一次 1 分钟,第二次 5 分钟,第三次 15 分钟。超过三次仍失败就转人工,不要无限重试。

3. 把脚本助手跑起来:从环境准备到批量备份的完整步骤

3.1 环境准备与依赖安装

脚本助手不需要装在控制节点上,任意能访问 Cinder API 和消息队列的机器都可以。依赖只有三个:Python 3.8 以上、keystoneauth1、requests。如果要用消息队列做异步通知,再加一个kombu。安装命令如下:

# 建议用 venv 隔离,避免污染系统 Python python3 -m venv /opt/cinderback/venv source /opt/cinderback/venv/bin/activate # 固定版本,避免不同环境行为不一致 pip install keystoneauth1==5.6.0 requests==2.31.0 kombu==5.3.7

逻辑说明:keystoneauth1的 5.x 版本对 v3 认证支持最稳定,requests用 2.31 是因为它修复了若干连接池相关的 bug。kombu只在需要监听备份完成事件时才装,不是必选。参数上,venv 路径建议放在/opt下,方便 systemd 单元引用。如果公司内网有 PyPI 镜像,记得在 pip 命令里加-i指向内部源,否则安装可能超时。

3.2 备份配置文件的字段与取值

脚本助手需要一个配置文件来定义“哪些卷要备份、备份到哪、保留多久”。我一般用 YAML,字段如下:

# /etc/cinderback/backup_policy.yaml defaults: container: backup_container_01 # 对象存储容器名 availability_zone: az1 # 由哪个 cinder-backup 处理 incremental: true # 是否增量备份 retention_days: 7 # 保留天数 volumes: - id: "a1b2c3d4-xxxx" # 卷 ID,必填 policy: daily # 引用下面的策略 - id: "e5f6g7h8-yyyy" policy: weekly container: backup_container_02 # 覆盖默认容器 policies: daily: interval_hours: 24 max_retry: 3 weekly: interval_hours: 168 max_retry: 2

逻辑说明:defaults提供兜底值,volumes里每个卷可以覆盖。incremental: true表示在已有全量备份基础上做增量,但要注意——增量备份依赖上一次备份的parent_id,如果上一次备份被删除,增量会退化成全量。retention_days是脚本自己维护的清理依据,不是 Cinder 原生功能,需要脚本定期调删除接口。参数上,container必须和 cinder-backup 配置里的backup_ceph_container或backup_swift_container一致,否则任务会直接失败。

3.3 批量创建备份的脚本实现

下面是一个批量创建备份的核心函数,包含状态轮询和简单重试:

import time import yaml import requests from keystoneauth1.identity import v3 from keystoneauth1 import session def load_policy(path="/etc/cinderback/backup_policy.yaml"): with open(path) as f: return yaml.safe_load(f) def create_backup(endpoint, token, vol_id, container, az, incremental): """创建单个备份,返回 backup id""" body = { "backup": { "volume_id": vol_id, "container": container, "availability_zone": az, "incremental": incremental, "name": f"auto-{vol_id[:8]}-{int(time.time())}" } } resp = requests.post( f"{endpoint}/backups", headers={"X-Auth-Token": token}, json=body, timeout=60 ) if resp.status_code >= 400: # 把 Cinder 返回的错误信息原样抛出,方便定位 raise RuntimeError(f"create failed: {resp.text}") return resp.json()["backup"]["id"] def wait_backup(endpoint, token, backup_id, timeout=7200): """轮询直到备份进入终态,返回最终状态""" deadline = time.time() + timeout while time.time() < deadline: resp = requests.get( f"{endpoint}/backups/{backup_id}", headers={"X-Auth-Token": token}, timeout=30 ) status = resp.json()["backup"]["status"] if status in ("available", "error"): return status time.sleep(30) # 30 秒轮询一次,避免打爆 API return "timeout"

逻辑说明:create_backup里把name拼上时间戳,方便在 Dashboard 里区分。wait_backup的轮询间隔设 30 秒,是因为备份任务通常持续几分钟到几十分钟,太频繁没意义。timeout默认 7200 秒,对应大卷的全量备份。参数上,incremental为 true 时,Cinder 会自动找该卷最近一次可用的全量备份作为父备份;如果找不到,会报错而不是自动转全量,这一点必须在脚本里捕获并处理。

3.4 备份完成后的元数据归档

备份成功后,脚本还应该把关键信息写一份到本地或数据库,方便审计和快速检索。我一般会记录:备份 ID、卷 ID、创建时间、大小、容器、父备份 ID。用 SQLite 就够,不需要上重型数据库。

import sqlite3 def archive_backup(db_path, bk): conn = sqlite3.connect(db_path) conn.execute(""" CREATE TABLE IF NOT EXISTS backup_log ( backup_id TEXT PRIMARY KEY, volume_id TEXT, created_at TEXT, size_gb REAL, container TEXT, parent_id TEXT ) """) conn.execute( "INSERT OR REPLACE INTO backup_log VALUES (?,?,?,?,?,?)", (bk["id"], bk["volume_id"], bk["created_at"], bk.get("size", 0) / 1024**3, bk["container"], bk.get("parent_id")) ) conn.commit() conn.close()

逻辑说明:INSERT OR REPLACE保证重复归档不会报错。size字段 Cinder 返回的是字节,除以 1024 的三次方转成 GB,方便人看。参数上,db_path建议放在/var/lib/cinderback/下,和配置文件分开。这张表后续可以用来做保留策略:查created_at超过retention_days的记录,调删除接口。

4. 避坑与排查:备份脚本最容易翻车的五个地方

4.1 增量备份突然变成全量,存储空间一夜爆满

现象:某天早上发现备份存储使用量翻了三倍,检查发现多个卷的增量备份都变成了全量。原因:增量备份依赖父备份的parent_id,如果父备份因为保留策略被删除,下一次增量找不到父备份,Cinder 会直接做全量而不是报错。解决:在删除旧备份前,先检查是否有子备份引用它;脚本里维护一张父子关系表,删除时先删子再删父,或者干脆对重要卷禁用增量、只用全量加保留策略。

4.2 备份卡在 creating 超过两小时

现象:openstack volume backup list里某条记录状态一直是creating,日志里没有明显报错。原因:通常是 cinder-backup 服务与后端存储之间的连接中断,或者备份数据量太大导致超时。解决:先看 cinder-backup 服务日志里有没有Timeout或Connection refused;如果是连接问题,重启服务后任务可能自动恢复;如果超过四小时仍无变化,需要手动把状态置为 error 再重新发起,不要直接删记录。

4.3 备份成功但恢复时找不到卷类型

现象:备份状态是available,但恢复出来的卷挂给虚机后无法启动。原因:备份时没有记录原卷的volume_type,恢复时用了默认类型,导致 IOPS 或后端不匹配。解决:脚本在创建备份前,先把卷的volume_type和metadata写进归档表;恢复时显式指定类型。Cinder 备份本身不保存卷类型信息,这一步必须由脚本补上。

4.4 多后端环境下备份写错了容器

现象:备份任务成功,但数据写到了非预期的对象存储容器,审计时对不上。原因:container参数没有显式指定,Cinder 用了 cinder-backup 配置文件里的默认值,而默认值可能和策略不符。解决:脚本里强制每个卷的备份请求都带container字段,不允许省略;同时在归档表里记录实际容器名,定期和策略比对。

4.5 并发备份把消息队列打满

现象:批量发起备份后,Cinder 的 RabbitMQ 队列堆积,其他操作变慢。原因:脚本一次性发起了几百个备份请求,每个请求都会产生消息。解决:在脚本里加并发控制,用threading.Semaphore限制同时进行的备份数,建议不超过 10;或者分批执行,每批之间 sleep 几秒。参数上,并发数取决于 cinder-backup 服务的 worker 数量,一般设为 worker 数的两倍以内。

5. 让脚本助手更耐用:状态校验与保留策略的进阶写法

脚本助手跑通之后,真正决定它能不能长期用的是两件事:状态校验的严谨度和保留策略的安全性。我见过太多脚本在测试环境跑得好好的,一到生产就因为一个边界条件把备份删多了。下面说两个我实际用过的技巧。

第一个是双人校验式的删除逻辑。删除旧备份前,不只看created_at,还要确认三个条件同时满足:该备份没有子备份、该备份不是所在卷的唯一可用备份、该备份的status是available。用 SQL 表达就是:

-- 找出可以安全删除的备份:超期、无子备份、且该卷还有其他可用备份 SELECT b.backup_id FROM backup_log b WHERE b.created_at < datetime('now', '-7 days') AND NOT EXISTS ( SELECT 1 FROM backup_log c WHERE c.parent_id = b.backup_id ) AND EXISTS ( SELECT 1 FROM backup_log o WHERE o.volume_id = b.volume_id AND o.backup_id != b.backup_id AND o.created_at > b.created_at );

这段 SQL 的逻辑是:先筛出超过保留期的备份,再排除掉有子备份的,最后要求同一卷还有更新的备份存在。三个条件缺一不可。参数上,-7 days对应策略里的retention_days,实际使用时用变量替换。这个查询在备份量到几万条时仍然很快,因为volume_id和parent_id上都建了索引。

第二个是定期做恢复演练。备份能不能用,只有恢复过才知道。我一般会每周挑一个非关键卷,用脚本自动执行“从最新备份恢复到一个临时卷 → 挂载校验数据 → 删除临时卷”的流程。这个流程不需要人工介入,但能提前发现备份文件损坏、后端认证过期等问题。恢复演练的脚本可以复用创建备份的认证和轮询逻辑,只是把 POST 的端点换成/backups/{id}/restore,并在请求体里指定volume_id为新卷 ID。

最后一个习惯:所有脚本操作都留审计日志。不只是记录成功失败,还要记录“谁在什么时候对哪个卷发起了什么操作”。我通常会在脚本入口处打一行 JSON 日志,包含时间戳、操作类型、卷 ID、备份 ID、结果。这些日志积累下来,就是排查问题时最好的后悔药。备份这件事,宁可多记一行日志,也不要事后靠猜。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询