test / docs /back-restore.md
GGSheng's picture
feat: deploy Gemma 4 to hf space
08c964e 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:单文件完整备份

// 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"
}
// 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:单文件增量备份

// 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"
}
// 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:分卷完整备份

// 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:分卷增量备份

// 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"
}
// 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:加密备份(单文件)

// 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:加密+分卷备份

// 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/ 同级).

{
  "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 确定:

{
  "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-full090000-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 得到的)。其内容在分卷确定:

{
  "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 字段会被更新为所有分卷文件名列表:

{
  "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 命令分割:

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)个备份:

# 清理逻辑(简化)
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 路径:

{
  "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

# 回溯下载流程(从新到旧收集)
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 文件
# 按顺序下载并合并归档
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. 指定备份恢复

恢复时可以指定任意备份,系统会自动完成备份链回溯和合并。

通过环境变量指定

# 指定要恢复的归档文件名(不含路径,包含.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分钟执行一次备份检查:

OPENCLAW_BACKUP_CRON="*/30 * * * *"

调度执行

openclaw-entrypoint.sh 启动:

# 在容器启动时设置cron
echo "$OPENCLAW_BACKUP_CRON root /usr/local/bin/openclaw-backup-cron.sh" >> /etc/crontab

执行保障

  • 健康检查:备份前可选执行 --check,失败时尝试 --repair
  • 重试机制:默认3次重试,间隔递增(10s, 20s, 30s)
  • 看门狗:作为最后防线,确保备份按时执行

并发执行控制

备份使用文件锁机制防止并发执行:

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 低变化率阈值(文件/分钟)

额外目录和文件

通过环境变量配置额外的备份内容:

# 额外目录格式: "归档名:/路径"
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等),无需单独指定元数据文件。


使用示例

首次部署

# 在 bootstrap-hf.sh 中配置
export OPENCLAW_BACKUP_DATASET_REPO="GGSheng/page-backup"
export OPENCLAW_INCREMENTAL_BACKUP="true"
export OPENCLAW_BACKUP_KEEP_COUNT="48"

手动触发备份

python3 /opt/openclaw-hf/openclaw_hf/backup.py backup

手动触发恢复

python3 /opt/openclaw-hf/openclaw_hf/backup.py restore

注意backup.pyrestore 操作均通过环境变量配置,不支持命令行参数指定备份文件或其他选项。相关配置通过 OPENCLAW_RESTORE_* 系列环境变量指定(详见环境变量参考)。

查看备份状态

# 查看最新备份索引
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 增强兼容性检查

系统支持向后兼容,降级使用时会有警告但不影响基本功能。