ai / docs /back-restore.md
GGSheng's picture
feat: deploy Gemma 4 to hf space
17e971c verified
# 备份与还原
## 概述
OpenClaw 使用 Hugging Face Dataset 作为备份存储后端,支持增量备份、分卷压缩、动态备份策略等企业级功能。
## 核心概念
### 备份类型
| 类型 | 说明 |
|------|------|
| **完整备份 (Full)** | 备份所有指定文件和目录 |
| **增量备份 (Incremental)** | 仅备份自上次备份以来变化的文件 |
| **分卷 (Split)** | 将大备份分割成多个小文件(默认500MB),是完整/增量备份的可选处理方式 |
| **加密 (Encrypted)** | 使用 AES-256-CBC 加密归档文件,是完整/增量备份的可选处理方式 |
### 两层元数据设计
系统使用两层元数据文件:
#### 1. 备份文件名命名规则
备份文件名格式:`openclaw-backup-{时间戳}-{类型}[-split].tar.gz`
**组成部分**
- `{时间戳}`:格式 `YYYYMMDD-HHmmss`,如 `20260413-120000`
- `{类型}`
- `full`:完整备份
- `inc`:增量备份
- `[-split]`:可选后缀,表示分卷备份
**类型后缀规则**
| 类型 | 后缀 | 示例 |
|------|------|------|
| 单文件完整备份 | `full` | `openclaw-backup-20260413-120000-full.tar.gz` |
| 单文件增量备份 | `inc` | `openclaw-backup-20260414-060000-inc.tar.gz` |
| 分卷完整备份 | `full-split` | `openclaw-backup-20260414-090000-full-split.tar.gz` |
| 分卷增量备份 | `inc-split` | `openclaw-backup-20260414-150000-inc-split.tar.gz` |
**注意**
1. 分卷文件在主文件名后加 `.part-{aa,ab,...}` 后缀
2. 分卷数量由归档后实际大小决定(超过 `OPENCLAW_BACKUP_SPLIT_SIZE` 默认500MB时分卷)
3. `-split` 后缀仅用于区分分卷备份(完整和增量都适用)
4. 加密归档在原文件名后加 `.enc` 后缀(如 `openclaw-backup-20260413-120000-full.tar.gz.enc`
5. 加密与分卷可同时使用,加密后分卷的文件名格式:`原名.tar.gz.enc.part-{aa,ab,...}`
#### 2. 备份索引文件(每个备份独立)
每个备份上传后都会在远端创建一个对应的元数据索引文件,命名格式为 `{归档名}.meta.json`
**示例远端存储结构**(按文件名/时间排序):
```
Dataset根目录/
├── latest-backup.json ← 与backups/同级,必须在根目录下
└── backups/
├── openclaw-backup-20260413-120000-full.tar.gz.meta.json ← 单文件完整备份
├── openclaw-backup-20260413-120000-full.tar.gz
├── openclaw-backup-20260414-060000-inc.tar.gz.meta.json ← 单文件增量备份
├── openclaw-backup-20260414-060000-inc.tar.gz
├── openclaw-backup-20260414-072200-inc.tar.gz.meta.json ← 单文件增量备份
├── openclaw-backup-20260414-072200-inc.tar.gz
├── openclaw-backup-20260414-081100-full.tar.gz.meta.json ← 单文件完整备份
├── openclaw-backup-20260414-081100-full.tar.gz
├── openclaw-backup-20260414-090000-full-split.tar.gz.part-aa ← 分卷完整备份
├── openclaw-backup-20260414-090000-full-split.tar.gz.part-ab
├── openclaw-backup-20260414-090000-full-split.tar.gz.meta.json
├── openclaw-backup-20260414-150000-inc-split.tar.gz.part-aa ← 分卷增量备份
├── openclaw-backup-20260414-150000-inc-split.tar.gz.part-ab
├── openclaw-backup-20260414-150000-inc-split.tar.gz.meta.json
├── openclaw-backup-20260414-152400-inc-split.tar.gz.part-aa ← 分卷增量备份
├── openclaw-backup-20260414-152400-inc-split.tar.gz.part-ab
└── openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json
```
**说明**:按文件名(时间)升序排列。每个分卷备份的文件组顺序为:分卷卷 → meta.json(分卷后原tar.gz不再保留)。
**索引文件内容分场景示例**
**场景A:单文件完整备份**
```json5
// openclaw-backup-20260413-120000-full.tar.gz.meta.json
{
"volumes": ["openclaw-backup-20260413-120000-full.tar.gz"],
"is_split": false,
"backup_type": "full",
"chain_id": "abc123",
"parent": null,
"created_at_utc": "2026-04-13T12:00:00",
"file_count": 42,
"archive_size": 1258291200,
"checksum": "sha256:def456...",
"version": "2.1",
"created_by": "openclaw-backup"
}
```
```json5
// openclaw-backup-20260414-081100-full.tar.gz.meta.json
{
"volumes": ["openclaw-backup-20260414-081100-full.tar.gz"],
"is_split": false,
"backup_type": "full",
"chain_id": "xyz789",
"parent": null,
"created_at_utc": "2026-04-14T08:11:00",
"file_count": 42,
"archive_size": 1258291200,
"checksum": "sha256:def456...",
"version": "2.1",
"created_by": "openclaw-backup"
}
```
**场景B:单文件增量备份**
```json5
// openclaw-backup-20260414-060000-inc.tar.gz.meta.json
{
"volumes": ["openclaw-backup-20260414-060000-inc.tar.gz"],
"is_split": false,
"backup_type": "incremental",
"chain_id": "abc123",
"parent": "openclaw-backup-20260413-120000-full.tar.gz.meta.json",
"created_at_utc": "2026-04-14T06:00:00",
"file_count": 5,
"archive_size": 52428800,
"checksum": "sha256:ghi789...",
"version": "2.1",
"created_by": "openclaw-backup"
}
```
```json5
// openclaw-backup-20260414-072200-inc.tar.gz.meta.json
{
"volumes": ["openclaw-backup-20260414-072200-inc.tar.gz"],
"is_split": false,
"backup_type": "incremental",
"chain_id": "abc123",
"parent": "openclaw-backup-20260414-060000-inc.tar.gz.meta.json",
"created_at_utc": "2026-04-14T07:22:00",
"file_count": 3,
"archive_size": 52428800,
"checksum": "sha256:ghi789...",
"version": "2.1",
"created_by": "openclaw-backup"
}
```
**场景C:分卷完整备份**
```json5
// openclaw-backup-20260414-090000-full-split.tar.gz.meta.json
{
"volumes": [
"openclaw-backup-20260414-090000-full-split.tar.gz.part-aa",
"openclaw-backup-20260414-090000-full-split.tar.gz.part-ab"
],
"is_split": true,
"backup_type": "full",
"chain_id": "xyz789",
"parent": null,
"created_at_utc": "2026-04-14T09:00:00",
"file_count": 128,
"archive_size": 2147483648,
"checksum": "sha256:jkl012...",
"version": "2.1",
"created_by": "openclaw-backup"
}
```
**场景D:分卷增量备份**
```json5
// openclaw-backup-20260414-150000-inc-split.tar.gz.meta.json
{
"volumes": [
"openclaw-backup-20260414-150000-inc-split.tar.gz.part-aa",
"openclaw-backup-20260414-150000-inc-split.tar.gz.part-ab"
],
"is_split": true,
"backup_type": "incremental",
"chain_id": "xyz789",
"parent": "openclaw-backup-20260414-090000-full-split.tar.gz.meta.json",
"created_at_utc": "2026-04-14T15:00:00",
"file_count": 64,
"archive_size": 1073741824,
"checksum": "sha256:mno345...",
"version": "2.1",
"created_by": "openclaw-backup"
}
```
```json5
// openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json
{
"volumes": [
"openclaw-backup-20260414-152400-inc-split.tar.gz.part-aa",
"openclaw-backup-20260414-152400-inc-split.tar.gz.part-ab"
],
"is_split": true,
"backup_type": "incremental",
"chain_id": "xyz789",
"parent": "openclaw-backup-20260414-150000-inc-split.tar.gz.meta.json",
"created_at_utc": "2026-04-14T15:24:00",
"file_count": 8,
"archive_size": 1073741824,
"checksum": "sha256:mno345...",
"version": "2.1",
"created_by": "openclaw-backup"
}
```
**场景E:加密备份(单文件)**
```json5
// openclaw-backup-20260414-160000-full.tar.gz.meta.json
{
"volumes": ["openclaw-backup-20260414-160000-full.tar.gz.enc"],
"is_split": false,
"backup_type": "full",
"chain_id": "enc001",
"parent": null,
"created_at_utc": "2026-04-14T16:00:00",
"file_count": 42,
"archive_size": 1300234240,
"checksum": "sha256:enc123...",
"version": "2.1",
"created_by": "openclaw-backup",
"encrypted": true,
"encryption_algorithm": "AES-256-CBC"
}
```
**场景F:加密+分卷备份**
```json5
// openclaw-backup-20260414-170000-full.tar.gz.meta.json
{
"volumes": [
"openclaw-backup-20260414-170000-full.tar.gz.enc.part-aa",
"openclaw-backup-20260414-170000-full.tar.gz.enc.part-ab",
"openclaw-backup-20260414-170000-full.tar.gz.enc.part-ac"
],
"is_split": true,
"backup_type": "full",
"chain_id": "enc002",
"parent": null,
"created_at_utc": "2026-04-14T17:00:00",
"file_count": 128,
"archive_size": 3221225472,
"checksum": "sha256:enc456...",
"version": "2.1",
"created_by": "openclaw-backup",
"encrypted": true,
"encryption_algorithm": "AES-256-CBC"
}
```
**字段说明**
| 字段 | 说明 |
|------|------|
| `volumes` | 归档文件列表(单文件时为单个元素,分卷时包含所有分卷) |
| `is_split` | 是否分卷备份 |
| `backup_type` | `full`(完整)或 `incremental`(增量) |
| `chain_id` | 备份链标识,同一链的所有备份共享此ID(详见下方生成规则) |
| `parent` | 父备份的 `.meta.json` 文件路径(完整备份为null) |
| `created_at_utc` | 备份创建时间(UTC) |
| `file_count` | 归档内包含的文件数量 |
| `archive_size` | 所有归档文件总大小(字节) |
| `checksum` | 所有归档合并后的SHA256校验和 |
| `version` | 元数据格式版本 |
| `created_by` | 创建工具标识 |
| `encrypted` | 是否加密(可选,默认为false) |
| `encryption_algorithm` | 加密算法(可选,如 `AES-256-CBC`) |
**chain_id 生成规则**:
1. **首次创建链**:生成新的 UUID 前8位作为 chain_id
2. **增量备份**:继承父备份的 chain_id
3. **独立完整备份**:生成新的 chain_id(即使上一个链未完成)
4. **分卷备份**:继承父备份的 chain_id
#### 2. latest-backup.json(最新备份软索引)
位于 Dataset 根目录(与 backups/ 同级).
```json5
{
"dataset": "GGSheng/page-backup",
"latest": "backups/openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json",
"is_split": true,
"created_at_utc": "2026-04-14T15:24:00"
}
```
**注意**:此文件仅存储最新备份的名称,不包含完整信息。完整信息在 `{归档名}.meta.json` 中。
#### 3. 归档内部元数据(last-backup-metadata.json)
在备份过程中,元数据会先写入本地 `work_dir`,然后打包进 tar.gz 归档内部,最后随归档上传到远端。归档内部元数据包含完整的备份信息(与 `.meta.json` 内容一致),用于归档自验证、链合并判断以及下一次增量备份的 parent 确定:
```json5
{
"version": "2.1",
"backup_type": "incremental",
"chain_id": "abc123",
"parent": "openclaw-backup-20260414-060000-inc.tar.gz.meta.json",
"volumes": ["openclaw-backup-20260414-060000-inc.tar.gz"],
"checksum": "sha256:abc123...",
"created_at_utc": "2026-04-14T15:24:00",
"last_backup_time": "2026-04-14T15:24:00",
"file_count": 10,
"archive_size": 5242880,
"is_latest": true,
"created_by": "openclaw-backup"
}
```
**说明**:
- `volumes`:当前备份包含的所有卷文件(单文件为列表,分卷备份为 `part-*` 文件列表)
- `checksum`:归档的 SHA256 校验和
- `file_count`:备份的文件数量
- `archive_size`:归档原始大小(字节)
- `is_latest`:标记是否为最新备份(用于链管理)
- `last_backup_time`:上一次备份的时间戳(UTC),用于判断增量备份的时间间隔
- 完整的备份信息同时存储在远端的 `.meta.json` 中
**重要说明**:`last-backup-metadata.json` 存储在本地 `work_dir`(默认 `/tmp/openclaw-backup`),容器重启后可能丢失。因此系统设计为**以远程元数据为真理源**:每次备份启动时会从远程 `latest-backup.json` 和最新的 `.meta.json` 获取链信息,确保即使本地元数据丢失也能正确构建备份链。
### 增量备份链 (Backup Chain)
增量备份通过 `parent` 字段形成链表:
**备份链路图**(两条独立链):
```
【链 abc123】
完整备份 chain_id: "abc123"
openclaw-backup-20260413-120000-full.tar.gz.meta.json
parent: null
│ parent
增量备份 #1 chain_id: "abc123"
openclaw-backup-20260414-060000-inc.tar.gz.meta.json
parent: "openclaw-backup-20260413-120000-full.tar.gz.meta.json"
│ parent
增量备份 #2 chain_id: "abc123"
openclaw-backup-20260414-072200-inc.tar.gz.meta.json
parent: "openclaw-backup-20260414-060000-inc.tar.gz.meta.json"
```
```
【链 xyz789】(包含最新备份)
完整备份 #1 chain_id: "xyz789"
openclaw-backup-20260414-081100-full.tar.gz.meta.json
parent: null
│ (独立链,无父子关系)
完整备份 #2 chain_id: "xyz789"
openclaw-backup-20260414-090000-full-split.tar.gz.meta.json
parent: null
│ parent
增量备份 #1 chain_id: "xyz789"
openclaw-backup-20260414-150000-inc-split.tar.gz.meta.json
parent: "openclaw-backup-20260414-090000-full-split.tar.gz.meta.json"
│ parent
增量备份 #2 (最新) chain_id: "xyz789"
openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json
parent: "openclaw-backup-20260414-150000-inc-split.tar.gz.meta.json"
│ latest
latest-backup.json → "backups/openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json"
```
**说明**:
- `081100-full` 和 `090000-full` 都是完整备份(parent: null),分属同一 chain_id 但互无父子关系
- 同一 chain_id 表示这些备份共享同一个备份链标识,用于分组和校验
- 恢复时沿 `parent` 链路回溯即可
**恢复流程**:从 latest-backup.json 获取最新备份 → 下载其 meta.json → 沿 parent 回溯到完整备份 → 按顺序合并。
### 远端存储混合场景示例
假设存在以下远端存储结构(包含完整备份链、多次增量、分卷等):
```
Dataset: GGSheng/page-backup
├── latest-backup.json ← 指向最新备份(位于根目录)
│ {"latest": "backups/openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json"}
└── backups/
├── openclaw-backup-20260413-120000-full.tar.gz.meta.json ← 单文件完整备份1
├── openclaw-backup-20260413-120000-full.tar.gz
├── openclaw-backup-20260414-060000-inc.tar.gz.meta.json ← 单文件增量备份1
├── openclaw-backup-20260414-060000-inc.tar.gz
├── openclaw-backup-20260414-072200-inc.tar.gz.meta.json ← 单文件增量备份2
├── openclaw-backup-20260414-072200-inc.tar.gz
├── openclaw-backup-20260414-081100-full.tar.gz.meta.json ← 单文件完整备份2
├── openclaw-backup-20260414-081100-full.tar.gz
├── openclaw-backup-20260414-090000-full-split.tar.gz.meta.json ← 分卷完整备份
├── openclaw-backup-20260414-090000-full-split.tar.gz.part-aa
├── openclaw-backup-20260414-090000-full-split.tar.gz.part-ab
├── openclaw-backup-20260414-150000-inc-split.tar.gz.meta.json ← 分卷增量备份1
├── openclaw-backup-20260414-150000-inc-split.tar.gz.part-aa
├── openclaw-backup-20260414-150000-inc-split.tar.gz.part-ab
├── openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json ← 分卷增量备份2
├── openclaw-backup-20260414-152400-inc-split.tar.gz.part-aa
└── openclaw-backup-20260414-152400-inc-split.tar.gz.part-ab
```
**场景说明**:
| 文件名 | 类型 | chain_id | parent |
|--------|------|----------|--------|
| 20260413-120000-full | 单文件完整备份 | abc123 | null |
| 20260414-060000-inc | 单文件增量备份 | abc123 | 20260413-120000-full.meta.json |
| 20260414-072200-inc | 单文件增量备份 | abc123 | 20260414-060000-inc.meta.json |
| 20260414-081100-full | 单文件完整备份 | xyz789 | null |
| 20260414-090000-full-split | 分卷完整备份 | xyz789 | null |
| 20260414-150000-inc-split | 分卷增量备份 | xyz789 | 20260414-090000-full-split.meta.json |
| 20260414-152400-inc-split | 分卷增量备份 | xyz789 | 20260414-150000-inc-split.meta.json |
**恢复任意备份的流程**:
1. 确定目标备份(如 `openclaw-backup-20260414-090000-full-split.tar.gz`)
2. 下载其 `.meta.json` 获取分卷信息和父备份引用
3. 如有父备份,继续下载父备份的 `.meta.json`
4. 按顺序合并所有归档
5. 恢复到目标目录
---
## 备份流程
### 整体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ 外部 Cron 调度 (每30分钟,由cron定义) │
│ OPENCLAW_BACKUP_CRON="*/30 * * * *" │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ openclaw-backup-cron.sh │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐ │
│ │ 环境准备 │→ │ 判断备份类型│→ │ 创建归档 │→ │ 分卷处理│ │
│ │ 加载环境变量│ │ 完整/增量 │ │ (动态策略) │ │ (如需要)│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐│
│ │ 清理旧备份 │← │ 上传文件 │← │ 创建meta.json ││
│ │ │ │ 分卷/归档 │ │ 更新latest-backup.json ││
│ └─────────────┘ └─────────────┘ └─────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
```
### 详细步骤
#### 1. 环境准备
脚本从以下位置加载环境变量:
- `/etc/profile.d/openclaw-env.sh`
- `/root/.env.d/openclaw-backup.env`
关键环境变量:
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLAW_BACKUP_DATASET_REPO` | (必填) | 备份存储的Dataset仓库 |
| `OPENCLAW_BACKUP_SOURCE_DIR` | `/root/.openclaw` | 备份源目录 |
| `OPENCLAW_BACKUP_WORK_DIR` | `/tmp/openclaw-backup` | 临时工作目录 |
| `OPENCLAW_BACKUP_SPLIT_SIZE` | `500` | 分卷大小(MB),超过此大小则分卷 |
| `OPENCLAW_BACKUP_KEEP_COUNT` | `48` | 保留的备份数量 |
| `OPENCLAW_INCREMENTAL_BACKUP` | `true` | 是否启用增量备份功能(`true`=增量模式,`false`=每次完整备份) |
| `OPENCLAW_INCREMENTAL_INTERVAL_MINUTES` | `10` | 增量备份间隔(分钟),距上次备份>=此时间时执行增量备份 |
| `OPENCLAW_BACKUP_ENCRYPTION_ENABLED` | `false` | 是否启用加密备份(`true`=启用,`false`=禁用) |
| `OPENCLAW_BACKUP_ENCRYPTION_PASSWORD` | (必填) | 加密密码(建议使用 HF Space Secrets 管理) |
**注意**:`OPENCLAW_INCREMENTAL_INTERVAL_MINUTES` 控制的是"每隔多久执行增量备份"。外部 cron 每30分钟触发一次,如果距上次备份时间未超过此间隔,则不执行任何备份;如果超过此间隔,则执行增量备份。
#### 2. 获取远程链信息(真理源)
**关键设计**:由于本地 `last-backup-metadata.json` 存储在 `/tmp/openclaw-backup`(容器重启后可能丢失),系统以**远程元数据为真理源**。
```
备份启动时:
├─→ 下载 latest-backup.json
└─→ 下载 latest .meta.json
├─→ last_backup_time → 用于判断距上次备份的时间间隔
├─→ parent_meta_path → 用于增量备份链的 parent 引用
├─→ chain_id → 用于标识同一备份链
└─→ volumes → 用于分卷备份的文件列表
```
此步骤确保即使本地元数据丢失,也能正确构建备份链。
#### 3. 动态备份策略
系统根据文件变化率和预估大小自动调整备份参数:
```
预估大小判断:
├── < 500MB → 小文件,快压缩(级别3),单文件备份
├── 500MB-2GB → 中等文件,平衡压缩(级别6),单文件备份
└── > 2GB → 大文件,最大压缩(级别9)
变化率调整:
├── > 10文件/分钟 → 高变化率,降低压缩级别优先速度
└── < 2文件/分钟 → 低变化率,提高压缩级别,可能跳过备份
```
**分卷触发**:归档完成后,检查归档实际大小是否超过 `OPENCLAW_BACKUP_SPLIT_SIZE`(默认500MB)。如果超过,则自动启用分卷。预估大小 `> 2GB` 只是提示系统采用最大压缩级别,但最终是否分卷由实际大小决定。
#### 4. 判断备份类型
根据以下条件判断是否执行完整备份或增量备份:
```
INCREMENTAL_BACKUP = false?
├── YES → 执行完整备份(每次都备份所有文件,创建新链)
└── NO ↓
首次备份 或 手动触发完整备份?
├── YES → 执行完整备份(备份所有文件,创建新链)
└── NO ↓
距上次备份时间 >= INCREMENTAL_INTERVAL ?
├── YES → 执行增量备份(只打包变化的文件,继承链)
└── NO → 跳过本次备份(等待下次 cron 触发)
```
**说明**:
- 完整备份:创建新的备份链,生成新的 `chain_id`
- 增量备份:继承父备份的 `chain_id`
- 外部 cron 每30分钟触发一次,只有满足时间条件才执行增量备份
- `OPENCLAW_INCREMENTAL_BACKUP=false` 时,每次都执行完整备份
- 归档完成后检查大小,超过 `OPENCLAW_BACKUP_SPLIT_SIZE` 则自动分卷
#### 5. 创建归档
根据策略创建 tar.gz 归档:
**完整备份归档结构**:
```
openclaw-backup-20260414-120000-full.tar.gz
├── openclaw-state/ # 主状态目录
│ ├── config.json
│ └── ...
├── root-config/ # 额外目录
├── root-ssh/ # SSH配置
└── last-backup-metadata.json # 备份链元数据
```
**增量备份归档结构**:
```
openclaw-backup-20260414-130000-inc.tar.gz
├── openclaw-state/ # 仅变化的文件
│ └── changed-file.txt
├── root-config/ # 仅变化的额外目录
└── last-backup-metadata.json # 包含chain_id和parent信息
```
**分卷备份归档结构**:
分卷备份的原始 tar.gz 归档结构与完整/增量相同,只是在归档完成后被分割成多个 part 文件。
```
# 归档前的原始结构(分卷完整备份为例)
openclaw-backup-20260414-090000-full.tar.gz
├── openclaw-state/ # 主状态目录
├── root-config/
├── root-ssh/
└── last-backup-metadata.json # 分卷前记录原始归档名
# 分割后的文件
openclaw-backup-20260414-090000-full.tar.gz.part-aa # 包含 last-backup-metadata.json
openclaw-backup-20204-090000-full.tar.gz.part-ab # 包含 last-backup-metadata.json
```
**分卷文件中 `last-backup-metadata.json` 的内容说明**:
每个分卷 part 文件内部都包含相同的 `last-backup-metadata.json`(因为是通过 split 分割原始 tar.gz 得到的)。其内容在分卷****确定:
```json5
{
"version": "2.1",
"backup_type": "full",
"chain_id": "xyz789",
"parent": null,
"volumes": ["openclaw-backup-20260414-090000-full.tar.gz"], // 分卷前为原始归档名
"checksum": "sha256:abc123...",
"created_at_utc": "2026-04-14T09:00:00",
"last_backup_time": "2026-04-14T09:00:00",
"file_count": 128,
"archive_size": 2147483648,
"is_latest": true,
"created_by": "openclaw-backup"
}
```
**注意**:`volumes` 字段在分卷前记录原始归档名,分卷后通过 `_update_volumes_in_metadata()` 更新为本地的 `last-backup-metadata.json`,但**分卷文件内部的副本不会改变**。上传到远端的 `.meta.json` 文件由程序生成,其 `volumes` 字段会被更新为所有分卷文件名列表:
```json5
{
"volumes": [
"openclaw-backup-20260414-090000-full.tar.gz.part-aa",
"openclaw-backup-20260414-090000-full.tar.gz.part-ab"
],
...
}
```
#### 6. 分卷处理(如需要)
当备份超过 `OPENCLAW_BACKUP_SPLIT_SIZE`(默认500MB)时,使用 `split` 命令分割:
```bash
split -b 500M openclaw-backup-20260414-120000-full.tar.gz \
openclaw-backup-20260414-120000-full.tar.gz.part-
# 生成: part-aa, part-ab, part-ac...
```
#### 7. 上传到 HuggingFace
**分卷备份上传**:
1. 所有分卷卷文件(如 `openclaw-backup-20260414-090000-full-split.tar.gz.part-aa`, `openclaw-backup-20260414-090000-full-split.tar.gz.part-ab`)
2. `.meta.json` 元数据索引文件
**单文件备份上传**:
1. `.tar.gz` 归档文件
2. `.meta.json` 元数据索引文件
**统一上传顺序**:
1. 所有分卷卷文件(如有)
2. 单文件归档文件(如有)
3. `.meta.json` 元数据索引文件
4. 更新 `latest-backup.json` 指向最新备份的 `.meta.json`
**注意**:`latest-backup.json` 存储最新备份的 `.meta.json` 路径(如 `backups/openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json`),恢复时直接下载它获取完整信息。
#### 8. 清理旧备份
保留最近 `OPENCLAW_BACKUP_KEEP_COUNT`(默认48)个备份:
```python
# 清理逻辑(简化)
all_backups = list_repo_files() # 获取所有备份文件
valid_backups = []
for backup in sorted(all_backups, key=timestamp, reverse=True):
if "-split.tar.gz" in backup:
# 分卷备份:检查所有卷是否都存在
if all_volumes_exist(backup):
valid_backups.append(backup)
elif backup.endswith(".meta.json"):
# 元数据文件:分卷备份直接保留(.tar.gz已被分割)
# 单文件备份需检查归档是否存在
main_archive = backup.replace(".meta.json", "")
if "-split" in main_archive or archive_exists(main_archive):
valid_backups.append(backup)
else:
valid_backups.append(backup)
# 删除超过keep_count的旧备份
for backup in valid_backups[keep_count:]:
delete(backup)
```
---
## 还原流程
### 整体流程
```
┌─────────────────────────────────────────────────────────────────┐
│ openclaw-restore.sh │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ 下载索引 │→ │ 解析备份链 │→ │ 按顺序下载归档 │ │
│ │ latest.json │ │ 下载meta.json│ │ (从完整到最新) │ │
│ │ │ │ 沿parent回溯 │ │ 合并分卷(如需要) │ │
│ └──────────────┘ └──────────────┘ └────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ 恢复到目标目录 │ │
│ │ │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 详细步骤
#### 1. 下载备份索引
从 HuggingFace Dataset 下载 `latest-backup.json`,获取最新备份的 `.meta.json` 路径:
```json
{
"latest": "backups/openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json",
"created_at_utc": "2026-04-14T15:24:00"
}
```
#### 2. 解析备份链(下载所有 meta.json)
从最新备份开始,沿 `parent` 字段回溯到完整备份,下载所有需要的 `.meta.json`:
```bash
# 回溯下载流程(从新到旧收集)
hf download GGSheng/page-backup backups/openclaw-backup-20260414-152400-inc-split.tar.gz.meta.json
# → parent: "openclaw-backup-20260414-060000-inc.tar.gz.meta.json"
hf download GGSheng/page-backup backups/openclaw-backup-20260414-060000-inc.tar.gz.meta.json
# → parent: "openclaw-backup-20260413-120000-full.tar.gz.meta.json"
hf download GGSheng/page-backup backups/openclaw-backup-20260413-120000-full.tar.gz.meta.json
# → parent: null (完整备份)
```
回溯完成后,收集到的 meta.json 列表需要反转,得到完整备份链(从旧到新):
```
合并顺序: [20260413-120000(full), 20260414-060000(inc), 20260414-152400(inc-split)]
```
#### 3. 按顺序下载归档文件
根据备份链**从旧到新**依次下载归档并合并:
- **分卷备份**:下载所有分卷卷文件(如 `part-aa`, `part-ab`),然后合并
- **单文件备份**:直接下载 `.tar.gz` 文件
```python
# 按顺序下载并合并归档
chain = [full_meta, inc1_meta, inc2_meta] # 从完整备份到最新
merged = {}
for meta in chain:
# 下载归档(分卷则先合并)
# volumes 格式: ["backups/xxx.part-aa", "backups/xxx.part-ab"] 或 ["backups/xxx.tar.gz"]
volumes = meta["volumes"]
if meta["is_split"]:
# 下载所有分卷,合并为一个临时文件
with open("temp.tar.gz", "wb") as out:
for vol in sorted(volumes):
download(vol)
with open(vol, "rb") as inp:
out.write(inp.read())
archive = "temp.tar.gz"
else:
archive = volumes[0]
download(archive)
# 校验完整性
assert calculate_checksum(archive) == meta["checksum"]
# 提取并合并到merged
for file in extract(archive):
merged[file.path] = file # 后者覆盖前者
```
#### 4. 恢复文件
将合并后的文件恢复到配置的目标目录:
| 归档内容 | 恢复路径 |
|----------|----------|
| `openclaw-state/` | `OPENCLAW_BACKUP_SOURCE_DIR` |
| `root-config/` | `OPENCLAW_BACKUP_ROOT_CONFIG_DIR` |
| `root-ssh/` | `OPENCLAW_BACKUP_ROOT_SSH_DIR` |
| 其他 | 按配置恢复 |
#### 5. 指定备份恢复
恢复时可以指定任意备份,系统会自动完成备份链回溯和合并。
**通过环境变量指定**:
```bash
# 指定要恢复的归档文件名(不含路径,包含.tar.gz扩展名)
# 系统会自动添加 .meta.json 后缀
export OPENCLAW_RESTORE_ARCHIVE="openclaw-backup-20260414-090000-full-split.tar.gz"
# 执行恢复
python3 /opt/openclaw-hf/openclaw_hf/backup.py restore
```
**恢复流程**:
1. 系统根据 `OPENCLAW_BACKUP_PATH_PREFIX` 构造元数据文件路径
2. 解析备份链:下载 `.meta.json` 并沿 `parent` 回溯到完整备份
3. 按顺序下载归档并合并(从完整备份到最新)
4. **如果归档加密**(`encrypted: true`),使用 `OPENCLAW_BACKUP_ENCRYPTION_PASSWORD` 解密
5. 恢复到目标目录
**示例**:假设指定恢复 `openclaw-backup-20260414-090000-full-split.tar.gz`(一个分卷完整备份):
```
指定归档 → openclaw-backup-20260414-090000-full-split.tar.gz
↓ 系统添加后缀 → openclaw-backup-20260414-090000-full-split.tar.gz.meta.json
↓ 下载 .meta.json: is_split=true, volumes=["backups/...part-aa", "backups/...part-ab"]
↓ parent: null (完整备份,无需回溯)
Step 3:
(1)下载所有分卷: part-aa, part-ab
(2)合并分卷为完整归档
(3)如果 encrypted=true,解密归档
Step 4: 提取文件到目标目录
```
**加密恢复注意事项**:
- 如果备份是加密的但未提供 `OPENCLAW_BACKUP_ENCRYPTION_PASSWORD`,恢复会失败并报错
- 加密备份与普通备份可以共存于同一 Dataset 中,恢复时自动识别
---
## 备份调度
### Cron 配置
默认每30分钟执行一次备份检查:
```bash
OPENCLAW_BACKUP_CRON="*/30 * * * *"
```
### 调度执行
由 `openclaw-entrypoint.sh` 启动:
```bash
# 在容器启动时设置cron
echo "$OPENCLAW_BACKUP_CRON root /usr/local/bin/openclaw-backup-cron.sh" >> /etc/crontab
```
### 执行保障
- **健康检查**:备份前可选执行 `--check`,失败时尝试 `--repair`
- **重试机制**:默认3次重试,间隔递增(10s, 20s, 30s)
- **看门狗**:作为最后防线,确保备份按时执行
### 并发执行控制
备份使用文件锁机制防止并发执行:
```python
lock_path = "/tmp/openclaw-backup/openclaw-backup.lock"
lock_fd = open(lock_path, "w")
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) # 非阻塞获取锁
except (OSError, IOError):
print("backup skipped: another backup is already in progress")
return None
```
**流程**:
1. 尝试获取排他锁(`LOCK_EX | LOCK_NB`)
2. 如果锁被占用,跳过本次备份
3. 备份完成后释放锁
**注意**:如果备份任务执行时间超过 cron 间隔(例如30分钟),新的备份任务会跳过,确保不会同时执行两个备份。
---
## 环境变量参考
### 必需变量
| 变量 | 说明 |
|------|------|
| `OPENCLAW_BACKUP_DATASET_REPO` | HuggingFace Dataset 仓库ID (如 `GGSheng/page-backup`) |
### 备份配置
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLAW_BACKUP_SOURCE_DIR` | `/root/.openclaw` | 备份的源目录 |
| `OPENCLAW_BACKUP_WORK_DIR` | `/tmp/openclaw-backup` | 临时工作目录 |
| `OPENCLAW_BACKUP_PATH_PREFIX` | `backups` | 仓库内的路径前缀 |
| `OPENCLAW_BACKUP_PRIVATE` | `true` | 是否创建为私有仓库 |
### 增量备份
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLAW_INCREMENTAL_BACKUP` | `true` | 启用增量备份 |
| `OPENCLAW_INCREMENTAL_INTERVAL_MINUTES` | `10` | 增量备份间隔(分钟),每隔此时间执行一次增量备份 |
**注意**:此变量控制增量备份的执行频率,非完整备份间隔。外部 cron 每30分钟触发一次,如果距上次备份未超过此间隔则跳过。超过此间隔则执行增量备份。
### 性能调优
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLAW_BACKUP_COMPRESSION_LEVEL` | `6` | 压缩级别 (1-9,9最大) |
| `OPENCLAW_BACKUP_SPLIT_SIZE` | `500M` | 分卷大小,空=不分卷 |
| `OPENCLAW_BACKUP_SIZE_WARNING_MB` | `1500` | 备份大小警告阈值(MB) |
| `OPENCLAW_BACKUP_KEEP_COUNT` | `48` | 保留备份数量 |
### 动态备份策略
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLAW_DYNAMIC_BACKUP` | `true` | 启用动态策略 |
| `OPENCLAW_DYNAMIC_SMALL_THRESHOLD_MB` | `500` | 小文件阈值 |
| `OPENCLAW_DYNAMIC_MEDIUM_THRESHOLD_MB` | `2000` | 中等文件阈值 |
| `OPENCLAW_DYNAMIC_HIGH_CHANGE_RATE` | `10` | 高变化率阈值(文件/分钟) |
| `OPENCLAW_DYNAMIC_LOW_CHANGE_RATE` | `2` | 低变化率阈值(文件/分钟) |
### 额外目录和文件
通过环境变量配置额外的备份内容:
```bash
# 额外目录格式: "归档名:/路径"
OPENCLAW_BACKUP_EXTRA_DIRS="root-config:/root/.config,root-ssh:/root/.ssh"
# 额外文件格式: "归档名:/路径"
OPENCLAW_BACKUP_EXTRA_FILES="bt-default:/www/server/panel/default.pl,root-bashrc:/root/.bashrc"
```
### 健康检查
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLAW_BACKUP_HEALTH_CHECK_ENABLED` | `false` | 启用健康检查 |
| `OPENCLAW_BACKUP_HEALTH_CHECK_BEFORE` | `false` | 备份前检查 |
| `OPENCLAW_BACKUP_HEALTH_CHECK_AFTER` | `false` | 备份后检查 |
| `OPENCLAW_BACKUP_MAX_RETRIES` | `3` | 最大重试次数 |
### 恢复配置
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLAW_RESTORE_ARCHIVE` | (空) | 指定恢复的归档文件名(不含路径),如 `openclaw-backup-20260414-090000.tar.gz`。默认为空时恢复最新备份。 |
**注意**:恢复时系统会自动查找 `{OPENCLAW_RESTORE_ARCHIVE}.meta.json` 获取备份的完整信息(volumes分卷列表、parent链路上游、chain_id等),无需单独指定元数据文件。
---
## 使用示例
### 首次部署
```bash
# 在 bootstrap-hf.sh 中配置
export OPENCLAW_BACKUP_DATASET_REPO="GGSheng/page-backup"
export OPENCLAW_INCREMENTAL_BACKUP="true"
export OPENCLAW_BACKUP_KEEP_COUNT="48"
```
### 手动触发备份
```bash
python3 /opt/openclaw-hf/openclaw_hf/backup.py backup
```
### 手动触发恢复
```bash
python3 /opt/openclaw-hf/openclaw_hf/backup.py restore
```
**注意**:`backup.py` 和 `restore` 操作均通过环境变量配置,不支持命令行参数指定备份文件或其他选项。相关配置通过 `OPENCLAW_RESTORE_*` 系列环境变量指定(详见环境变量参考)。
### 查看备份状态
```bash
# 查看最新备份索引
hf download GGSheng/page-backup backups/latest-backup.json
cat backups/latest-backup.json
```
---
## 故障排除
### 备份失败
1. 检查网络连接和 HF_TOKEN 权限
2. 查看日志:`/var/log/openclaw/backup.log`
3. 确认 Dataset 仓库存在且有写入权限
### 恢复失败
1. 确认备份元数据完整
2. 检查恢复目标目录有足够空间
3. 元数据版本不兼容时,系统会以兼容模式尝试恢复
### 大文件备份超时
- 启用分卷备份:`OPENCLAW_BACKUP_SPLIT_SIZE="500M"`
- 增加 HuggingFace 下载超时:`HF_HUB_DOWNLOAD_TIMEOUT=300`
---
## 元数据版本兼容性
| 版本 | 说明 |
|------|------|
| 1.0 | 初始版本 |
| 2.0 | 支持增量备份链 |
| 2.1 | 增强兼容性检查 |
系统支持向后兼容,降级使用时会有警告但不影响基本功能。