Seafile 云盘备份到本地磁盘:从 SeaDrive+rsync 踩坑到年度快照方案
背景
一直以来我用 SeaDrive + rsync 的组合来备份 Seafile 云盘数据到本地磁盘。但用久了发现这个方案有一个根本性问题,值得写出来给同样在用 Seafile 的朋友避坑。
问题:SeaDrive 是虚拟盘,rsync 备份的可能只是"影子"
SeaDrive 的工作模式是 on-demand(按需下载):本地默认只保存"影子文件"(占位符),真正的文件内容要在被访问时才会从服务器拉取下来。
这意味着直接对 SeaDrive 挂载目录跑 rsync,会遇到两难:
- 要么触发大量按需下载:rsync 遍历目录时把所有影子文件的实际内容都拉取一遍,速度慢、流量大,尤其是移动网络或跨境访问服务器时体验很差;
- 要么根本没备份到完整内容:如果不触发下载,rsync 复制到的可能只是占位符,不是真实文件,备份形同虚设。
这也是我决定换方案的直接原因。
候选方案对比
| 方案 | 说明 | 适用场景 |
|---|---|---|
| seaf-cli | 官方命令行同步客户端,本地是真实文件,非虚拟盘 | 命令行环境、树莓派等 headless 设备 |
| seafile-client(同步模式) | 官方桌面客户端,选"同步"而非"虚拟盘" | 有图形界面的常驻电脑 |
| rclone + WebDAV | 利用 Seafile 的 WebDAV 接口做同步/备份 | 想要多目标备份(同时备份到 S3、OneDrive 等)的场景 |
| 强制落盘后 rsync | 先遍历触发 SeaDrive 全量下载,再 rsync | 不推荐,效率低、逻辑 tricky |
最终选择基于官方 seaf-cli,因为它本身就是同步工具,不需要再叠加 rsync 这一层,依赖轻,适合脚本化和定时任务。
我的场景:另一台电脑 + 移动硬盘,每年备份一次
具体需求是:
- 不影响 Seafile 服务器端的数据;
- 用一台额外的电脑,接上移动硬盘做本地留档;
- 备份频率很低——一年一次。
关键坑:官方客户端默认是双向同步
无论是 seafile-client 桌面版还是 seaf-cli,默认都是双向同步。这意味着如果备份电脑上误删了文件,或者移动硬盘故障导致文件丢失/损坏,这个"删除"或"损坏"状态会被同步推回服务器,造成反向污染——这是这套方案里最容易被忽视、也最危险的一点。
解决思路:单向"快照式"备份
不做长期挂载的双向同步,而是把每次备份当作一次性的"拉取快照":
# 每年备份时执行
seaf-cli sync -l <repo-id> -s https://your-server -u user -p pass -d /path/to/backup
# 同步完成后立刻断开
seaf-cli desync -d /path/to/backup
这样处理后,移动硬盘在平时(也就是一年中的绝大部分时间)是完全离线、与服务器无任何连接的状态,即使备份电脑本身出问题(误操作、勒索软件等),也不会波及服务器数据。风险被压缩在"一年一次、几十分钟到几小时"的备份窗口内。
架构演进:每年全量占用空间太快,改用增量快照
按上面的思路,早期版本的脚本是每年建一个独立的全量目录(seafile-backup-2026、seafile-backup-2027……),互不覆盖、历史可追溯。但实际用起来发现一个问题:库本身有上百G,如果每年都全量下载一份存成独立目录,移动硬盘的空间会很快撑不住。
目标:既要保留"每次快照独立、互不覆盖,服务器出问题不会连累历史备份"这个安全特性,又不想每次都占用一份完整的全量空间。
解决方案:seaf-cli 负责的部分只做增量同步到一个固定的"live"目录(不带年份,每次都是最新状态,网络传输量很小);live目录同步完成后,再用 rsync --link-dest 把这次状态"快照"进一个带时间戳的独立目录——没有变化的文件用硬链接(本质是同一份数据的另一个目录项,几乎不占用新的磁盘空间),只有真正新增/修改的文件才会占用新空间。
效果:
- 磁盘空间增长量 ≈ 每次实际变化的数据量,而不是每次全量重复
- 每次快照依然是独立、可回溯的普通目录(可以直接进去看文件,不需要额外工具解压/挂载),即使服务器某次出问题(误删/勒索软件/意外覆盖),live目录被"污染"同步过去后,之前生成的快照完全不受影响——因为rsync快照是纯本地操作,快照生成之后就和live目录彻底脱钩了
seaf-cli sync每次只需要传输增量,网络开销和之前对比也更低
完整脚本:增量同步 + rsync硬链接快照 + 轮询等待 + 超时保护 + 重试机制
考虑到大库同步可能要跑很久,简单的 sleep N 猜测等待时间并不可靠,所以脚本里加了几个健壮性设计:
- 轮询检测同步状态,而不是固定等待时间
- 超时保护(默认 6 小时),避免异常情况下无限挂起
- 失败自动重试(默认 3 次)
- 信号捕获:即使中途 Ctrl+C 或脚本崩溃,也会自动断开同步、清理现场,不会留下"半连接"状态
- 密码运行时输入,不写死在脚本里
- 增量快照:
seaf-cli只负责把数据同步到固定的 live 目录,rsync --link-dest负责生成带时间戳、硬链接去重的历史快照,空间增长量约等于每次实际变化的数据量
#!/bin/bash
#
# Seafile 增量备份脚本(live目录增量同步 + rsync硬链接快照)
# 用法: ./seafile_incremental_backup.sh
#
# 架构说明(相比早期"每年建一个全量目录"的版本做了改进):
# 1. seaf-cli 永远只同步到一个固定的 "live" 目录(不带年份),走增量对齐,
# 每次运行只传输相对上次的变化部分,网络开销小
# 2. 同步完成、desync 之后,用 rsync --link-dest 把 live 目录的当前状态
# "快照"进一个带时间戳的独立目录:没有变化的文件用硬链接(几乎不占
# 额外磁盘空间,只是个指针),只有真正新增/变化的文件才占用新空间
# 3. 效果:磁盘空间增长量 ≈ 每次实际变化的数据量,而不是每年全量重复;
# 同时每次快照依然是独立、可回溯的——即使服务器某次出问题(误删/
# 勒索软件/意外覆盖),live目录同步过去后,之前的快照完全不受影响,
# 不会被"污染"覆盖
#
# ⚠️ 重要安全须知(血泪教训,务必遵守):
# seaf-cli sync 是双向同步机制。一旦某个库被 sync 追踪,本地目标目录里
# 发生的任何"变化"(包括你手动删除文件),daemon 都可能当成本地编辑,
# commit 后反向 upload 回服务器,有污染服务器数据的风险。
#
# 因此:
# - 【绝对不要】在库还处于追踪状态时,手动 rm -rf / 修改 live 目录里的内容
# - 如果确实需要中途清理/重置某个库的本地目录,正确顺序是:
# 1) 先尝试 seaf-cli desync -d <目录> (正式解除追踪)
# 2) 如果 desync 失败(比如库还在clone阶段、list里查不到),
# 改用 seaf-cli stop 把整个daemon停掉——此时没有daemon在监控,
# 再删除目录才是安全的
# 3) 清理完成后再 seaf-cli start 重新开始
# - 快照目录(snapshots/ 下)是只读留档,任何情况下都不要手动修改,
# 只有本脚本的 rsync 步骤会写入
# - 本脚本已经做了"失败自动 desync 清理"和"结束后正式收尾",正常运行
# 全程不需要人工介入删除目录;只有异常中断(断网、硬件故障等)才需要
# 按上面的手动流程处理
#
set -euo pipefail
# ============ 配置区(根据实际情况修改) ============
SERVER="https://your-seafile-server"
USER="your@email.com"
BACKUP_BASE_DIR="/Volumes/BackupDisk" # 移动硬盘挂载路径
CONF_DIR="$HOME/.seafile-backup-conf" # seaf-cli 配置/状态目录
# 要备份的资料库列表,格式为 "库名:repo-id"
# 库名用于本地目录命名(方便查看),repo-id 用于实际同步命令
# 获取方式: seaf-cli list-remote -s "$SERVER" -u "$USER"
REPOS=(
"财务报表:repo-id-1"
"工作文档:repo-id-2"
)
# 轮询设置
POLL_INTERVAL=30 # 每次检查间隔(秒)
MAX_WAIT_SECONDS=21600 # 最长等待时间,默认6小时,超时则报警退出
MAX_RETRIES=3 # 单个库同步失败后的最大重试次数
# ============ 配置区结束 ============
LIVE_DIR="${BACKUP_BASE_DIR}/seafile-live" # seaf-cli 实际同步的目录,永远是"当前最新状态"
SNAPSHOT_BASE="${BACKUP_BASE_DIR}/seafile-snapshots" # 历史快照存放的根目录
LOG_DIR="${BACKUP_BASE_DIR}/seafile-logs"
LOG_FILE="${LOG_DIR}/backup_log_$(date +%Y%m%d_%H%M%S).txt"
log() {
# 加 || true:即使日志写入(tee到移动硬盘上的日志文件)因为磁盘抖动等
# 原因短暂失败,也不能让 set -e 把整个备份脚本直接杀死——日志写不进去
# 不该比"备份任务本身"更优先,宁可丢一行日志,也不能让脚本无声退出
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" 2>/dev/null || \
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
fail() {
log "❌ 错误: $*"
cleanup_and_exit 1
}
cleanup_and_exit() {
local exit_code=$1
log "开始清理:断开所有已同步的库并停止 seaf-cli..."
for ENTRY in "${REPOS[@]}"; do
local repo_name="${ENTRY%%:*}"
local safe_name
safe_name=$(echo "$repo_name" | tr '/\:' '___')
local target_dir="${LIVE_DIR}/${safe_name}"
if [ -d "$target_dir" ]; then
seaf-cli desync -d "$target_dir" 2>/dev/null || true
fi
done
seaf-cli stop 2>/dev/null || true
log "清理完成,退出码: $exit_code"
exit "$exit_code"
}
# 捕获中断信号(Ctrl+C、异常退出、SSH断线等),确保不会留下挂着的同步连接
# 注意:HUP 是 SSH 连接断开时会发给前台进程的信号——如果没有用 screen/nohup
# 包裹脚本运行,SSH一断线bash就会收到HUP直接终止;这里加上HUP捕获后,
# 至少能在真正被杀死前跑一次清理(desync+stop),避免库停留在"追踪中但
# 未收尾"的悬空状态。但这只是兜底,最稳妥的做法仍然是用 screen/nohup 运行
# (见文档"备份数据量大时:用 screen 防止 SSH 断线中断脚本"一节)。
trap 'log "收到中断信号"; cleanup_and_exit 1' INT TERM HUP
# 诊断用:记录脚本退出时的退出码,方便下次遇到"不明不白中断"时快速定位
# 是正常结束(0)、还是被 set -e 因某条命令失败而静默终止(非0、且没有
# 走 INT/TERM/HUP 分支)
trap 'EXIT_CODE=$?; if [ "$EXIT_CODE" -ne 0 ]; then log "⚠ 脚本以非正常退出码 $EXIT_CODE 结束(可能是某条命令失败触发了 set -e,比如日志/磁盘写入短暂失败)"; fi' EXIT
# ------------ 预检查 ------------
if ! command -v seaf-cli &> /dev/null; then
echo "错误: 未找到 seaf-cli,请先安装 (apt install seafile-cli 或 brew install seaf-cli)"
exit 1
fi
if ! command -v rsync &> /dev/null; then
echo "错误: 未找到 rsync,请先安装 (apt install rsync)"
exit 1
fi
if [ ! -d "$BACKUP_BASE_DIR" ]; then
echo "错误: 移动硬盘路径不存在: $BACKUP_BASE_DIR"
echo "请确认硬盘已挂载"
exit 1
fi
mkdir -p "$LIVE_DIR" "$SNAPSHOT_BASE" "$LOG_DIR"
touch "$LOG_FILE"
log "========================================="
log "Seafile 增量备份开始"
log "Live 目录: $LIVE_DIR"
log "快照根目录: $SNAPSHOT_BASE"
log "========================================="
# 密码运行时输入,不落盘
if [ -z "${SEAFILE_PASSWORD:-}" ]; then
read -rsp "请输入 Seafile 密码: " SEAFILE_PASSWORD
echo
fi
mkdir -p "$CONF_DIR"
seaf-cli init -d "$CONF_DIR" 2>/dev/null || true
seaf-cli start
sleep 2
# ------------ 轮询等待单个库同步完成 ------------
# 返回值: 0=成功, 1=超时, 2=同步出错
#
# 注意:seaf-cli status 的输出格式是 "库名 状态 进度",不包含 repo-id,
# 所以这里必须按库名(repo_name)匹配,而不是 repo-id。
# 用 grep -F 做固定字符串匹配,避免库名中出现正则特殊字符导致误判。
wait_for_sync() {
local repo_name=$1
local target_dir=$2
local elapsed=0
while [ "$elapsed" -lt "$MAX_WAIT_SECONDS" ]; do
local status_output
status_output=$(seaf-cli status 2>/dev/null || echo "")
local repo_line
repo_line=$(echo "$status_output" | grep -F "$repo_name" || true)
if [ -n "$repo_line" ]; then
if echo "$repo_line" | grep -qiE "synchronized|up to date|up-to-date|file(s)? synced"; then
return 0
elif echo "$repo_line" | grep -qiE "error|failed"; then
log " ⚠ 检测到同步错误状态: $repo_line"
return 2
fi
# 显示 downloading/syncing/committing/uploading 等,仍在进行中,记录进度并继续等待
log " ...同步中: $repo_line"
else
# seaf-cli status 中已找不到该库条目,通常表示同步完成(已从任务列表移除)
# 做一次额外确认:目录是否存在且非空
if [ -d "$target_dir" ] && [ "$(ls -A "$target_dir" 2>/dev/null)" ]; then
return 0
fi
fi
sleep "$POLL_INTERVAL"
elapsed=$((elapsed + POLL_INTERVAL))
done
return 1 # 超时
}
# ------------ 主同步流程(带重试) ------------
FAILED_REPOS=()
SUCCESS_REPOS=()
for ENTRY in "${REPOS[@]}"; do
REPO_NAME="${ENTRY%%:*}"
REPO_ID="${ENTRY#*:}"
# 目录名里的斜杠、冒号、反斜杠替换成下划线,避免破坏路径结构
SAFE_NAME=$(echo "$REPO_NAME" | tr '/\:' '___')
TARGET_DIR="${LIVE_DIR}/${SAFE_NAME}"
log "-----------------------------------------"
log ">>> 开始同步库: $REPO_NAME (repo-id: $REPO_ID)"
log " 目标目录: $TARGET_DIR"
ATTEMPT=1
REPO_OK=false
# seaf-cli sync 要求 -d 指定的目录必须提前存在,不会自动创建
mkdir -p "$TARGET_DIR"
while [ "$ATTEMPT" -le "$MAX_RETRIES" ]; do
log " 第 $ATTEMPT 次尝试..."
# 预检查:如果该库已经在 seaf-cli status 中(比如上次运行被中断但daemon仍在后台下载),
# 不要重复调用 sync(会报 "is not a library" 之类的错误),直接跳到轮询等待即可
ALREADY_SYNCING=$(seaf-cli status 2>/dev/null | grep -F "$REPO_NAME" || true)
if [ -n "$ALREADY_SYNCING" ]; then
log " 检测到该库已在同步中,跳过重复调用 sync,直接等待完成: $ALREADY_SYNCING"
SYNC_CALL_OK=true
elif seaf-cli sync -l "$REPO_ID" -s "$SERVER" -u "$USER" -p "$SEAFILE_PASSWORD" -d "$TARGET_DIR" 2>>"$LOG_FILE"; then
SYNC_CALL_OK=true
else
SYNC_CALL_OK=false
fi
if [ "$SYNC_CALL_OK" = true ]; then
wait_for_sync "$REPO_NAME" "$TARGET_DIR"
RESULT=$?
if [ "$RESULT" -eq 0 ]; then
log " ✅ 库 $REPO_NAME 同步成功"
REPO_OK=true
break
elif [ "$RESULT" -eq 1 ]; then
log " ⏱ 库 $REPO_NAME 同步超时(超过 ${MAX_WAIT_SECONDS}s)"
else
log " ⚠ 库 $REPO_NAME 同步过程中报错"
fi
else
log " ⚠ seaf-cli sync 命令执行失败"
fi
# 失败后先 desync 清理,再决定是否重试
seaf-cli desync -d "$TARGET_DIR" 2>/dev/null || true
ATTEMPT=$((ATTEMPT + 1))
[ "$ATTEMPT" -le "$MAX_RETRIES" ] && sleep 10
done
if [ "$REPO_OK" = true ]; then
SUCCESS_REPOS+=("$REPO_NAME")
# 成功后立即断开,缩短联网窗口
seaf-cli desync -d "$TARGET_DIR" 2>/dev/null || true
log " 已断开 $REPO_NAME 的同步连接"
else
FAILED_REPOS+=("$REPO_NAME")
log " ❌ 库 $REPO_NAME 在 $MAX_RETRIES 次尝试后仍然失败"
fi
done
# 所有库处理完毕,彻底停掉 daemon,之后 live 目录不再有任何进程监控/联网
seaf-cli stop 2>/dev/null || true
unset SEAFILE_PASSWORD
log "-----------------------------------------"
log "seaf-cli 同步阶段结束"
log "成功: ${#SUCCESS_REPOS[@]} 个库 (${SUCCESS_REPOS[*]:-无})"
log "失败: ${#FAILED_REPOS[@]} 个库 (${FAILED_REPOS[*]:-无})"
# ------------ 快照阶段:用 rsync --link-dest 生成硬链接快照 ------------
#
# 没有变化的文件走硬链接,几乎不占新空间;只有新增/变化的文件才占用
# 实际磁盘空间。找不到上一次快照时(首次运行),退化为一次全量拷贝。
SNAPSHOT_NAME="snapshot-$(date +%Y%m%d_%H%M%S)"
if [ "${#FAILED_REPOS[@]}" -gt 0 ]; then
SNAPSHOT_NAME="${SNAPSHOT_NAME}-partial"
fi
NEW_SNAPSHOT="${SNAPSHOT_BASE}/${SNAPSHOT_NAME}"
PREV_SNAPSHOT=$(find "$SNAPSHOT_BASE" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort | tail -n 1 || true)
log "-----------------------------------------"
log "开始生成快照: $NEW_SNAPSHOT"
if [ -n "$PREV_SNAPSHOT" ]; then
log "参考上一次快照做硬链接去重: $PREV_SNAPSHOT"
if rsync -a --delete --info=progress2 --link-dest="$PREV_SNAPSHOT" "${LIVE_DIR}/" "${NEW_SNAPSHOT}/" 2>>"$LOG_FILE"; then
log "✅ 快照生成成功(增量硬链接模式)"
else
log "❌ 快照生成失败,请检查日志: $LOG_FILE"
exit 1
fi
else
log "未找到历史快照,本次为首次运行,执行全量拷贝"
if rsync -a --info=progress2 "${LIVE_DIR}/" "${NEW_SNAPSHOT}/" 2>>"$LOG_FILE"; then
log "✅ 快照生成成功(首次全量模式)"
else
log "❌ 快照生成失败,请检查日志: $LOG_FILE"
exit 1
fi
fi
# 快照占用空间参考(因为硬链接的存在,du 统计的是"实际独占空间"更有参考意义;
# 如果想看这份快照包含的总文件体积,用 du --apparent-size)
SNAPSHOT_ACTUAL_SIZE=$(du -sh "$NEW_SNAPSHOT" 2>/dev/null | cut -f1)
log "========================================="
log "备份任务全部结束"
log "本次快照: $NEW_SNAPSHOT"
log "本次快照实际占用空间(去重后): $SNAPSHOT_ACTUAL_SIZE"
log "Live 目录: $LIVE_DIR"
log "日志文件: $LOG_FILE"
log "========================================="
if [ "${#FAILED_REPOS[@]}" -gt 0 ]; then
echo ""
echo "⚠ 部分库同步失败(已生成 -partial 标记的快照),请检查日志: $LOG_FILE"
exit 1
fi
echo ""
echo "✅ 全部备份成功完成,快照已生成: $NEW_SNAPSHOT"
exit 0
实战踩坑记录:seaf-cli init 在旧版本上的初始化失败
方案定下来之后,实际部署时在一台 Debian(oldstable,seafile-cli 版本 8.0.10)机器上遇到了一连串初始化失败的问题,折腾了不少时间才排查清楚,记录下来供同样环境的朋友参考。
现象:list-remote 直接报错
seaf-cli list-remote -s https://10.7.8.18 -u your@email.com
报错:
Migrating device id from ccnet conf
Traceback (most recent call last):
...
File "/usr/bin/seaf-cli", line 276, in get_device_id
with open(idfile, 'w') as fp:
FileNotFoundError: [Errno 2] No such file or directory: '/home/wyw/Seafile/.seafile-data/id'
原因:seaf-cli 任何命令(包括 list-remote)都依赖配置目录已初始化,而这里还没跑过 seaf-cli init。
第一个坑:残留的 ~/.ccnet 导致 init 静默跳过
跑 seaf-cli init -d ~/Seafile 后提示 /home/wyw/.ccnet already exists,然后 seaf-cli start 报错 Could not load .../seafile.ini、Invalid config directory。
原因:~/.ccnet 目录之前已经存在(可能是早期尝试留下的残留),seaf-cli init 检测到该目录存在就直接跳过了初始化逻辑,导致 ~/Seafile/.seafile-data/id、~/.ccnet/seafile.ini 等必要文件根本没有被创建——这是 8.0.10 这个版本对"目录已存在但内容不完整"这种边界情况处理有缺陷。
第二个坑:清空重试后,init 反而报"目录不存在"
删掉残留目录后重新 init,这次提示 /home/wyw/Seafile not exists,但检查发现该命令依然没有真正创建任何目录或文件:
ls -la ~/.ccnet/ # 只有一个空的 logs 目录
ls -la ~/Seafile/.seafile-data/ # No such file or directory
也就是说无论目录"已存在"还是"不存在",这个版本的 init 命令在这台机器上都无法正常完成全部初始化流程,属实是个两头堵的 bug。
尝试升级到官方最新版(因网络环境未果)
正常思路是换成 Seafile 官方 apt 源装新版本,绕开发行版仓库里的旧包:
wget -qO - https://linux-clients.seafile.com/seafile.pub.gpg-key | gpg --dearmor | sudo tee /usr/share/keyrings/seafile-archive-keyring.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/seafile-archive-keyring.gpg] https://linux-clients.seafile.com/seafile-deb/$(lsb_release -cs)/ stable main" | sudo tee /etc/apt/sources.list.d/seafile.list
sudo apt update
但这台机器访问境外站点直接超时(Connection timed out),说明这台机器没有配置代理、无法直连境外服务器。考虑到这台机器本身只是用来跑本地备份,没有必要为了升级一个客户端引入代理依赖,所以放弃了这个方向,转为手动补全配置文件绕过 bug。
最终方案:手动重建 seaf-cli 所需的完整配置骨架
seaf-cli 依赖的配置目录(默认 ~/.ccnet)需要包含以下几样东西,而这些正是 8.0.10 的 init 逻辑没能正确生成的:
| 文件/目录 | 作用 |
|---|---|
~/.ccnet/ccnet.conf | 客户端基础配置(设备ID、名称等) |
~/.ccnet/seafile.ini | 一行文本,指向实际数据目录的绝对路径 |
~/.ccnet/logs/ | 日志目录——缺了这个会导致 seaf-daemon 启动时直接崩溃 |
~/Seafile/.seafile-data/id | 设备ID文件 |
完整重建脚本:
# 1. 彻底清空,不留残留
seaf-cli stop 2>/dev/null
rm -rf ~/.ccnet ~/Seafile
# 2. 创建目录骨架(包括容易被忽略的 logs 目录)
mkdir -p ~/Seafile/.seafile-data
mkdir -p ~/.ccnet/logs
# 3. 生成设备ID
DEVICE_ID=$(python3 -c "import hashlib,uuid; print(hashlib.sha1(uuid.uuid4().bytes).hexdigest())")
# 4. 写入 id 文件
echo -n "$DEVICE_ID" > ~/Seafile/.seafile-data/id
# 5. 写入 ccnet.conf
cat > ~/.ccnet/ccnet.conf << EOF
[General]
ID = $DEVICE_ID
NAME = $(hostname)-backup
SERVICE_URL =
[Network]
PORT = 13419
EOF
# 6. 写入 seafile.ini —— 内容就是数据目录的绝对路径,一行文本
echo -n "$HOME/Seafile/.seafile-data" > ~/.ccnet/seafile.ini
# 7. 启动
seaf-cli start
这一步最容易漏掉的是 ~/.ccnet/logs/ 目录——如果缺失,seaf-daemon(seaf-cli 背后实际干活的守护进程)会在初始化日志系统时直接失败退出:
[log.c(147): Failed to open file /home/wyw/.ccnet/logs/seafile.log
seaf-daemon.c(501): Failed to init log.
补上这个目录后,daemon 才能正常常驻后台,seaf-cli sync 命令才能通过本地命名管道正常和它通信(否则会报 _queue.Empty / socket 找不到 之类的RPC连接错误)。
验证 daemon 是否真的启动成功的小技巧
如果 seaf-cli start 之后同步命令依然报 RPC 连接失败,可以绕开 seaf-cli start,直接前台跑 seaf-daemon 观察真实报错(这一步能看到 seaf-cli start 未必会暴露的详细错误信息):
seaf-daemon -c ~/.ccnet -d ~/Seafile/.seafile-data
正常情况下这条命令会前台阻塞、没有任何输出——这其实是好现象,说明 daemon 正常常驻运行;如果报错后直接退出回到命令提示符,才说明初始化真的有问题。
环境细节:局域网内用 http 而非 https
这台 Seafile 服务器部署时没有启用 https,list-remote/sync 等命令里服务器地址要用 http:// 而不是 https://:
seaf-cli sync -l <repo-id> -s http://10.7.8.18 -u your@email.com -d ~/Seafile/target-dir
因为客户端和服务器都在同一局域网内,数据不会经过公网,http 明文传输的风险可以接受,不需要为此额外折腾证书或 VPN 隧道。如果你的场景是跨公网访问内网 Seafile 服务器,则建议走已有的 VPN/隧道通道,避免账号密码明文暴露在公网上。
小结:这次踩坑对应的检查清单
如果你在旧版本 seaf-cli 上遇到类似问题,可以按这个顺序排查:
seaf-cli list-remote/sync报FileNotFoundError→ 配置目录没有 init 或 init 不完整seaf-cli init提示 “already exists” 但功能仍不正常 → 清空~/.ccnet和数据目录,彻底重来- 清空后 init 仍提示 “not exists” 且没有实际生成文件 → 该版本 init 逻辑有 bug,需要手动补全配置骨架
seaf-cli sync报 RPC / socket 相关错误(_queue.Empty、FileNotFoundError指向.sock)→ daemon 没有正常启动,前台跑seaf-daemon看真实报错seaf-daemon报Failed to open file .../logs/xxx.log→ 缺少logs目录,mkdir -p ~/.ccnet/logs即可解决seaf-cli sync报The local directory does not exists/... is not a library→-d指定的目标目录必须提前存在,seaf-cli不会自动创建;脚本里在调用sync前需要先mkdir -p "$TARGET_DIR"(下方脚本已修复此问题)- 脚本轮询迟迟不判定完成、或中断后重跑时对同一个库反复报
is not a library→seaf-cli status的输出格式是"库名 状态 进度",不包含 repo-id;如果轮询逻辑拿 repo-id 去匹配这行输出会永远匹配不上,导致轮询名存实亡(小库因为下载快、赶在轮询超时前就已经完成,容易掩盖这个 bug;数据量大时才会暴露)。同时,如果脚本中途被中断(比如 SSH 断线、误按 Ctrl+C),daemon 可能仍在后台继续下载,重新运行脚本时对同一个库再次调用sync会冲突报错——需要先检查该库是否已经在status里同步中,是的话跳过重复调用,直接进入轮询等待(下方脚本已修复此问题)
关于反向上传风险的实战教训
在实际跑一批大库(其中一个上百G)的过程中,还踩到了一个比配置问题更值得警惕的坑,这里单独记录。
起因:移动硬盘中途"掉线重连"
跑到一半时移动硬盘发生了设备号漂移(/dev/sdb 变成了 /dev/sdc),导致原来的挂载点悬空,读写这个挂载点下的文件全部报 Buffer I/O error / Input/output error。排查一圈后发现磁盘本身没有真正硬件坏道,只是设备重新枚举,重新挂载到新的 /dev/sdc1 后即可恢复:
sudo umount -f /mnt/seafile-backup-2026
sudo mount /dev/sdc1 /mnt/seafile-backup-2026
(建议长期用 UUID 挂载,避免设备号漂移这类问题:sudo blkid /dev/sdc1 拿到UUID后用 mount UUID="xxxx" /mnt/... 挂载。)
真正的坑:处理故障过程中的 rm -rf 触发了反向上传
排查这次硬盘问题时,为了清理一个下载到一半、状态异常的目录,直接对仍处于 seaf-cli sync 追踪状态的目标目录执行了 rm -rf。结果重新插拔硬盘、恢复挂载、重跑脚本后,daemon 把这次"目录内容突然消失又重新出现"的变化当成了本地编辑,自动触发了 committing → uploading,也就是把本地这次异常状态反向推送回了服务器。
这正是我们最早方案讨论阶段就担心的"官方客户端默认双向同步"风险,只是没想到触发方式是"手动清理故障目录"这种排障过程中的常规操作。
弯路:曾尝试寻找真正的"单向下载"命令,但没有
一度以为 seaf-cli download 是和 sync 不同的单向拉取命令,实测发现:下载完成后同样会出现在 seaf-cli list 追踪列表里、状态显示 synchronized——download 和 sync 底层是同一套追踪机制,并没有真正意义上的单向命令。
也考虑过 SeafDAV(WebDAV)+ rclone 的方案,理论上能从协议层面杜绝反向写入,但需要修改服务器端 seafile.conf 开启 SeafDAV 并重启服务,对生产环境有一定操作成本;同时对上百G级别的库,如果改用"生成分享链接下载zip"的方式,会有服务器现场打包压力大、单个大文件下载无断点续传等问题,权衡下来都不如直接解决操作纪律问题来得实际。
也考虑过给账号设置库的只读权限,从服务器端强制隔离——但这只在多用户版本下可行,专业版单用户账号本身就是唯一的读写账号,无法做权限隔离。
最终结论:不改造方案,靠操作纪律规避
结合库很大(100G+)、局域网环境、没有多用户权限体系这几个约束条件,最终决定继续用 seaf-cli sync,但明确一套安全操作纪律,写进脚本注释里长期提醒自己:
- 绝对不要在库还处于追踪状态时,手动
rm -rf/ 修改目标目录里的内容 - 如果确实需要中途清理/重置某个库的本地目录,正确顺序是:
- 先尝试
seaf-cli desync -d <目录>(正式解除追踪) - 如果 desync 失败(比如库还在 clone 阶段、
list里查不到),改用seaf-cli stop把整个 daemon 停掉——没有 daemon 在监控的情况下,删除目录才是安全的 - 清理完成后再
seaf-cli start重新开始
- 先尝试
- 脚本本身"失败自动 desync 清理"和"结束后正式收尾"的逻辑,正常运行全程不需要人工介入删除目录;只有断网、硬件故障这类异常情况才需要按上面的手动流程处理
这次意外上传发生后,务必去 Seafile 网页端检查对应库的文件是否完整、历史版本里有没有异常提交,需要的话可以用历史版本回滚。
使用方法
1. 安装 seaf-cli
# Debian/Ubuntu
sudo apt install seafile-cli
# macOS
brew install seafile-client seaf-cli
2. 获取要备份的资料库 repo-id
seaf-cli list-remote -s https://your-seafile-server -u your@email.com
3. 修改脚本配置区
SERVER、USER:Seafile 服务器地址和账号BACKUP_BASE_DIR:移动硬盘挂载路径(Linux 类似/media/username/BackupDisk,macOS 是/Volumes/BackupDisk)。脚本会在这个路径下自动建三个子目录:seafile-live(seaf-cli实际同步的最新状态)、seafile-snapshots(历史快照,每次运行生成一个带时间戳的独立目录)、seafile-logs(运行日志)REPOS:格式为"库名:repo-id",例如"财务报表:726bc5b7-0104-4887-8ee9-124a03d820da"。本地目录会用库名命名(自动把库名里的/、\、:替换成_,避免破坏路径),方便日后直接从文件夹名认出是哪个库,而不用对着一串UUID猜
4. 赋予执行权限并运行
chmod +x seafile_incremental_backup.sh
./seafile_incremental_backup.sh
密码会在运行时以隐式方式输入,不落盘。如果想跳过交互(比如接入 cron),可以提前 export SEAFILE_PASSWORD=xxx。
5. 查看快照
每次运行结束后,去 $BACKUP_BASE_DIR/seafile-snapshots/ 下面会看到一个新的 snapshot-年月日_时分秒 目录(如果本次有库同步失败,目录名会带上 -partial 后缀,提醒这份快照不完整)。可以直接进去看文件,不需要额外工具解压或挂载。如果想确认某次快照实际占用了多少新增空间(而不是包含硬链接在内的总体积),脚本日志里已经打印了 du -sh 统计的实际占用大小。
踩坑:首次快照阶段"卡着不动",其实是rsync默认静默
第一次运行时,因为没有历史快照可以参考,快照阶段会退化成一次全量拷贝(后面详细展开),如果拷贝的数据量是几十上百G,这个过程会跑很久。而 rsync -a 默认不会打印任何进度,只有拷贝完成或出错才会有下一条日志——中间这段时间日志停在"开始生成快照…执行全量拷贝"这一行不动,看起来像卡死了,其实很可能只是在正常干活、没有输出而已。
排查方法:
# 看rsync进程是否存在、状态是否正常(不是僵死)
ps aux | grep rsync
# 更直接:看快照目录的大小是否在持续增长
watch -n 2 'du -sh /mnt/backup/seafile-snapshots/snapshot-最新那个时间戳'
如果这个数字在持续变大,说明确实在正常拷贝,耐心等就行。脚本后续版本已经给 rsync 加上了 --info=progress2 参数,会显示整体拷贝进度百分比,不会再有这种"看起来卡死"的困惑。
关于首次全量拷贝:不是浪费,是硬链接快照方案必经的第一步
硬链接的本质是"指向已经存在的某个文件",首次运行时快照目录下什么都没有,没有任何"已存在的文件"可以链接,所以第一次必然是一次完整的物理拷贝,占用空间约等于 seafile-live 目录的总大小。这不是脚本设计缺陷——所有基于硬链接的增量快照方案(Time Machine、rsnapshot、Borg等)都是同样的逻辑:第一次必须有一份完整的"基准",后续才能对比差异。从第二次运行开始,--link-dest 才能真正发挥省空间的效果:没有变化的文件变成硬链接(几乎不占新空间),只有新增/修改的文件才占用真正的新空间。
⚠️ 这里有个前提容易被忽略:硬链接依赖文件系统支持。exFAT 这类为了跨平台兼容而简化设计的文件系统不支持硬链接,如果备份盘是 exFAT 格式,--link-dest 会静默失效、退化成每次都全量拷贝,整套"省空间"的设计就白费了。这块盘需要格式化成 ext4(Linux原生文件系统,完整支持硬链接)才能让这套方案真正生效,代价是可能没法直接在Windows/macOS上免驱动读取,需要权衡"跨平台兼容"和"增量快照真正省空间"这两者。
验证硬链接是否生效,可以在盘上直接测试:
cd /mnt/backup
echo "test" > test_a.txt
ln test_a.txt test_b.txt
ls -li test_a.txt test_b.txt # 如果两个文件的inode号相同,说明硬链接成功
rm test_a.txt test_b.txt
或者等第二次快照生成后,对比两次快照里同一个文件的inode号,相同就说明硬链接确实生效、没有占用新空间。
备份数据量大时:用 screen 防止 SSH 断线中断脚本
如果备份的库比较大、脚本要跑很久,强烈建议不要直接在 SSH 会话里裸跑脚本——之前实测过一次 SSH 意外断线,直接把当时前台运行的 seaf-daemon 进程带走了,导致同步中断在不上不下的状态。
脚本里的 seaf-cli start 启动的 daemon 本身是后台常驻、不依附于 SSH 会话的,但脚本本身(前台运行的 bash 进程)依然依附于 SSH 会话——一旦断线,脚本的重试逻辑、日志记录、最后的 desync + stop 收尾步骤会全部中断。
为什么选 screen 而不是 tmux
| tmux | screen | |
|---|---|---|
| 学习曲线 | 稍陡,功能更多(分屏、插件、状态栏定制) | 极简,核心用途就是"断开重连" |
| 默认快捷键 | Ctrl+B 前缀,还要记子命令 | Ctrl+A 前缀,日常只需要脱离和重连两个操作 |
| 系统预装情况 | 通常需要额外安装 | 很多Linux发行版(包括Debian)历史悠久、几乎所有服务器都有 |
如果只是偶尔用一次、目的单纯是防止SSH断线打断脚本,不需要tmux那些分屏、多窗口管理和插件生态,screen 三个命令就够用,学习成本更低。(如果你本来就熟悉/常用tmux做其他多任务管理,继续用tmux也完全没问题,两者都能达到同样的防断线效果。)
screen 用法(就这几步)
# 1. 安装(Debian 通常默认已装,没有的话装一下)
sudo apt install screen
# 2. 新建一个命名会话,在里面正常跑脚本
screen -S seafile-backup
./seafile_incremental_backup.sh
# 3. 运行过程中如果要主动脱离(脚本继续在后台跑)
# 按 Ctrl+A,松开后再按 D
SSH断了或者主动脱离后,重新连上服务器,直接接回去看实时进度:
ssh wyw@x230
screen -r seafile-backup
如果重连时提示 “already attached”(比如上次没正常脱离),加个 -d 强制抢回来:
screen -d -r seafile-backup
踩坑:screen 里中文显示乱码
配好 screen 后实际用起来,发现脚本日志里的中文全部变成了乱码。排查下来是Mac客户端终端软件的 locale 传递问题,不是screen或服务器本身的锅,记录一下排查过程方便对照。
排查步骤:
- 先确认外层终端本身能不能正常显示中文,再进screen里对比一次,缩小范围是screen的问题还是终端本身的问题
- 在服务器上跑
locale,发现报错Cannot set LC_CTYPE to default locale: No such file or directory,且LC_CTYPE的值是裸的UTF-8(正常应该是en_US.UTF-8这种"语言_地区.编码"完整格式) - 检查服务器上所有可能设置locale的地方(
.bashrc、.zshrc、.profile、/etc/default/locale)都没有找到这行有问题的设置,说明这个错误值不是服务器本地配置产生的 - 最终定位到根源:这个变量是SSH客户端从本地Mac传过来的。Mac上用的终端是 Alacritty,它默认不会像 Terminal.app/iTerm2 那样自动从系统读取完整的locale设置,只留了一个不完整的
LC_CTYPE=UTF-8;SSH客户端配置默认会把本地的LC_*环境变量转发到远程服务器(SendEnv/AcceptEnv机制),这个不完整的值就把服务器本来正确的locale设置覆盖掉了
解决方法:
在Mac本地的shell配置(~/.zshrc)里手动补上完整的locale设置:
cat >> ~/.zshrc << 'EOF'
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
EOF
source ~/.zshrc
也可以更彻底地直接在 Alacritty 配置文件(~/.config/alacritty/alacritty.toml)里指定环境变量,对所有shell生效:
[env]
LANG = "en_US.UTF-8"
LC_ALL = "en_US.UTF-8"
服务器端建议也顺手加一道保险(防止以后换其他终端/设备连接又带来同样问题):
# 在服务器的 ~/.zshrc 里
export LC_CTYPE=en_US.UTF-8
export LC_ALL=en_US.UTF-8
两边都设置好后重新连接,screen里的中文显示恢复正常。
事后复盘:如何验证数据完整性、要不要重新验证脚本
反向上传事件发生后,做了两件收尾的事,也一并记录下来。
通过历史记录确认数据没有被破坏
去 Seafile 网页端查看了触发反向上传的那个库的历史记录,重点看两点:
- 变更记录的动作类型——正常的意外提交通常是"添加或修改了 xxx 文件",如果看到大量"删除了 xxx 文件"的记录,才是真正需要警惕、可能需要回滚的信号
- 文件本身的内容日期有没有变化——照片/视频这类文件,即使被重新提交,文件内容(拍摄日期等元数据)如果和原来一致,说明只是daemon重新扫描后做了一次"内容对齐"式的重复提交,不是破坏性覆盖
这次检查下来,触发的记录全部是"添加或修改",文件日期也都正常,没有删除类记录,确认服务器数据完好。如果真发现了异常删除,Seafile 的历史版本通常支持直接**还原(Revert)**到问题发生前的版本。
要不要专门为了验证新脚本再跑一遍——评估下来价值不高
脚本改完(轮询逻辑修复、目标目录预创建、重复调用sync预检查)之后,一度想专门找一个库重新跑一遍验证效果。但仔细想了想,这几个修复点其实已经在这次"排障过程中中断又重新接上"的真实场景里被动验证过了——脚本确实正确识别了"库已在同步中"并跳过了重复调用,最终也正确显示为 synchronized。
专门为了测试重新触发一次sync,对已经同步完成的大库来说,daemon依然需要完整遍历、逐一比对文件哈希才能确认"无需下载",这个核对开销对大库并不算小;而且每多跑一次同步流程,就多一次"意外发生"的窗口。既然核心逻辑已经在真实故障恢复场景里得到验证,没必要再为了"仪式感"制造一次没有实际数据变化的测试。真正有意义的验证时机,留给下次有新库要备份、或者明年正式跑年度备份的"冷启动"场景更合适。
未解之谜:screen 会话连同脚本进程一起消失的排查全过程
用上增量快照架构、也确认开着 screen 之后,还是遇到了一次相当诡异的情况,完整排查过程记录下来,即使最终没有找到确切元凶,这套排查思路本身也值得参考。
现象
跑完第一个库、正准备继续第二个库的交接时刻,脚本进程和它所在的 screen 会话一起消失了。查看现场:
seaf-cli status/list显示第一个库是synchronized状态,依然被追踪着(说明脚本在跑完desync之前就没了,或者干脆没执行到desync那一步)screen -ls显示的会话创建时间是当天早上(也就是手动重启后新建的那个),昨晚跑的那个 screen 会话完全找不到了,不是"detached",是压根不存在了- 日志文件停在某一行"…同步中"之后再无任何内容,没有任何报错、没有trap捕获到信号的提示
排查过程:一路排除法
1. 怀疑SSH断线杀死进程(SIGHUP)——最先怀疑这个,因为之前已经踩过一次"前台跑 seaf-daemon 被SSH断线带走"的坑。但确认这次已经用了screen,screen的设计初衷正是为了防止SSH断线影响里面的进程,这个方向被排除。仍然顺手给脚本的 trap 加上了 HUP 信号捕获,作为额外的防御——万一某次真的没用screen,至少能在被杀死前跑一次清理。
2. 怀疑整机重启/挂起——uptime 显示机器已连续运行数天,没有中断;last reboot 也没有对应时间点的重启记录,journalctl 里没有任何 suspend/resume 相关日志。排除。
3. 怀疑硬盘I/O错误导致日志写入失败、触发 set -e 静默退出——脚本用了 set -euo pipefail,log() 函数内部是 echo | tee -a "$LOG_FILE" 这种管道写入;如果移动硬盘(之前已经出现过一次设备号漂移+I/O错误的先例)再次抖动,写入失败会让 set -e 直接杀死整个脚本,且不会触发任何trap(因为不是INT/TERM/HUP信号,是命令失败退出)。查了 dmesg 确实有硬盘I/O错误的记录,但时间对不上——硬盘报错是前一天晚上19点多,跟脚本消失的03点多完全是两个时间点。这个猜测被证据推翻,但依然值得修——不管是不是这次的元凶,“日志写入失败能拖累整个备份任务"这件事本身就是个隐患,所以给 log() 函数加上了容错(写入失败就退化成直接输出到终端,不再让它有能力杀死脚本)。
4. 怀疑 OOM killer 强杀进程——dmesg | grep -i "oom\|killed process" 什么都没查到。排除。
5. 怀疑USB设备断开重连——dmesg 确实有相关记录,但同样是前一天晚上的事件,跟这次时间对不上。排除。
6. 怀疑Wi-Fi异常/网络中断——这台机器用的是无线网卡(wlp3s0),检查了对应时间段的内核日志,只有UFW防火墙拦截mDNS组播包的常规噪音,没有任何断线/重连的迹象。排除。
7. 怀疑 seaf-daemon 自己崩溃重启——查了 ~/.ccnet/logs/events.log,daemon只在真正的启动时刻才会打印"Starting record seafile events"这行标志性日志;从昨晚开始跑到今早手动重启之间,没有出现第二次,说明 daemon 全程是同一个进程实例、没有崩溃重启过。这也解释了为什么发现问题时 status 显示第一个库已经是 synchronized——daemon 自己在后台把活干完了,只是外层负责调度、desync、写日志的脚本进程没了。排除。
8. 怀疑 systemd 在SSH会话结束时批量清理用户进程(KillUserProcesses)——这是本以为最有希望的方向:如果开启了这个选项,SSH登录会话被判定结束时,systemd会把该用户名下所有还挂在这个登录会话cgroup里的进程一起清理掉,包括detach状态的screen会话;而 seaf-daemon 因为自己会做双重fork脱离父会话,不受影响——这能完美解释"daemon活着、screen和脚本一起消失"的组合现象。但实际检查 /etc/systemd/logind.conf,KillUserProcesses 是默认值no(虽然写成注释状态,但注释掉就是维持默认值,默认本身就是no)。排除。
9. 怀疑 .screenrc 配置了异常的超时行为,或者 ulimit 限制影响了长时间运行的进程——.screenrc 根本不存在(没有任何自定义配置);ulimit -a 里所有数值都在正常默认范围,没有异常收紧。排除。
结论:未解之谜,但排查思路本身有价值
一路排除下来,硬件、网络、系统重启、OOM、daemon自身、systemd清理机制、screen配置、ulimit——所有能想到的系统层面原因全部核实排除,依然没有找到确凿的元凶。这种情况在实际运维中并不罕见:可能是某种极小概率的、难以复现的边缘状况(内核瞬时调度异常、screen本身某个冷门bug等),继续深挖的边际收益已经很低。
最终决定:不再执着于找出确切原因,而是从"即使原因不明,也要让系统更抗造"的角度加固:
log()函数加上容错:日志写入(管道到tee)失败不再能通过set -e杀死整个脚本- 新增
EXITtrap 诊断:以后再发生类似"不明不白的中断”,只要脚本走的是正常bash退出流程(而不是这次这种连trap都没触发的更诡异情况),至少能在日志里留一条"以非正常退出码结束"的记录,方便下次直接定位 trap增加HUP信号捕获:即使某次忘了用screen,也能多一层兜底- 作为备选方案,遇到同样情况时可以换用
nohup交叉验证:如果某次用screen又出现类似情况,可以换成nohup ./script.sh > out.log 2>&1 & disown的方式重跑,如果这种方式全程正常,至少能把问题范围进一步收窄到screen这一层;如果nohup方式也出现同样情况,说明根源在更底层,需要继续排查
处理原则:如果类似情况再次发生、脚本进程消失,不要惊慌重跑,先检查daemon和已下载数据的状态——从这次的经验看,daemon很可能还在后台正常干活,seaf-cli status/list 能看到实际进度,被中断的只是外层负责调度的脚本;这时候正确的做法是直接重新执行脚本(脚本自带的"检测到已在同步中,跳过重复调用sync"逻辑会正确接上),而不是纠结"数据是不是丢了"——大概率没丢,只是记录同步进度的这层壳暂时掉了。
小结
| 要点 | 说明 |
|---|---|
| 别用虚拟盘做 rsync 备份源 | SeaDrive 的"影子文件"机制会让 rsync 备份不完整或效率低下 |
| 优先用官方同步工具 | seaf-cli / seafile-client 的同步模式本地就是真实文件 |
| 警惕默认的双向同步 | 备份电脑上的误删/损坏、或者排障过程中误操作被追踪目录,都可能被同步回服务器,没有真正的"单向下载"命令,只能靠操作纪律规避 |
| 低频备份用"快照式"而非长期挂载 | 同步完立即 desync,缩短暴露窗口,离线时零风险 |
| 大库多次备份用增量快照,而非每次全量 | live目录做增量同步,rsync --link-dest 生成硬链接快照,空间增长量约等于每次实际变化的数据量 |
| 脚本要考虑异常情况 | 超时保护、重试、信号捕获,避免"半成品"备份或悬挂状态 |
| 追踪状态下的目录不能手动删改 | 必须先 desync(或 stop)解除追踪,再做任何本地清理 |
这套方案目前用在我自己的场景里:一台闲置电脑 + 移动硬盘,每年跑一次,跑完硬盘直接拔下离线保存。如果你的备份频率更高(比如每周/每天),更适合用**方案 A:只读同步(download-only)**长期挂载,而不是这种一次性快照方式,具体可以根据自己的风险偏好选择。