每日备份(backup-daily.sh)¶
仓库路径:
deploy/nfos/backup-daily.sh| 线上路径:/opt/Cy/services/system.d/nfos/backup-daily.sh语言:bash | 定时器:nfos-backup.timer
用途¶
每日 02:00 把日志 + 配置 + 数据库打成本地可校验的一份备份,产物根为 /home/ecs-user/temp/<YYYY-MM-DD>/:
| 目录 / 文件 | 内容 |
|---|---|
logs/ |
主节点 /opt/Cy/services/logs 的 tar.gz(排除 30 天前的文件与 *.gz)+ 次节点日志 tar.gz(经 SSH 尽力而为,失败则写 .FAILED.txt 并继续) |
config/ |
站点/后端配置、systemd 单元、监控 yml、NFOS 运维脚本。敏感文件一律不复制,改为在 config/REDACTED.txt 里登记「谁被跳过、为什么」;含内联凭据的文件(JWT_SECRET / PASSWORD / webhook URL 等)做「脱敏复制」(值替换为 __REDACTED__) |
db/ |
nfarmsd SQLite(优先 sqlite3 .backup 一致性快照)、次节点 TimescaleDB pg_dump -Fc(经 SSH 落到主节点)、次节点 NATS JetStream 状态归档 |
MANIFEST.md |
本次备份什么 / 何时 / 多大 / 从哪来 / 怎么恢复 |
CHECKSUMS.sha256 |
所有产物的 sha256,并由脚本自己 sha256sum -c 复核 |
保留策略:只保留最近 KEEP_DAYS(默认 14)个「严格 YYYY-MM-DD 目录」,且绝不删除今天及以后的目录;/home/ecs-user/temp 下的其他文件/目录(用户自己的东西)从不触碰。
与既有 backup-aliyun.sh 的关系(源码注释):本脚本只做「本地可校验的当日备份」,不涉及云盘上传;两者互不冲突(前者 02:00 上传阿里云盘,本脚本同为 02:00,timer 已加 300s 随机延迟)。
何时用¶
- 不用手工跑:
nfos-backup.timer每日 02:00:00(RandomizedDelaySec=300、Persistent=true)。 - 想先看计划与磁盘情况:
--dry-run(-n)——不写任何文件。 - 磁盘紧张时临时少留几份:
--keep 7。 - 次节点维护 / 离线演练:
--no-secondary。 - 验证可恢复性:本脚本自己会
sha256sum -c复核;恢复用MANIFEST.md里的步骤。
命令与参数¶
以下参数逐字取自脚本的 while [ "$#" -gt 0 ]; do case "$1" in … 分发。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--dry-run / -n |
开关 | DRY_RUN=0 |
只打印计划与磁盘情况,不写任何文件 |
--no-secondary |
开关 | SECONDARY_MODE=auto |
跳过次节点(离线演练 / 次节点维护期) |
--keep |
正整数(--keep N) |
KEEP_DAYS=14 |
覆盖保留份数 |
--keep=N |
正整数(= 形式) |
KEEP_DAYS=14 |
同上(KEEP_DAYS="${1#*=}") |
-h / --help |
开关 | — | sed -n '2,55p' "$0" 打印头部注释后 exit 0 |
共 5 个参数(其中 --keep N 与 --keep=N 是同一参数的两种写法)。
未知参数 → echo "未知参数:$1(用法:bash $0 [--dry-run] [--keep N] [--no-secondary])" >&2; exit 2。
不带参数时的默认行为¶
DRY_RUN=0、SECONDARY_MODE=auto、KEEP_DAYS=14 → 真跑一次完整备份(这就是 systemd 单元的形式)。
前置检查顺序(任一不过就中止):
- root:
id -u不是 0 →错误:需要 root 运行(sudo bash $0)。读取 nfarmsd SQLite、容器数据与 /opt/Cy 配置需要 root。→exit 1; - 依赖命令:
tar gzip sha256sum find df stat awk逐个command -v检查,缺一个 →!! 缺少依赖命令:<c>→exit 1; --keep合法性:非数字 →错误:--keep 需要正整数,收到 '<值>'→exit 2;小于 1 →错误:--keep 至少为 1→exit 2;- 磁盘空间:
df -Pk "$TEMP_ROOT"可用空间< MIN_FREE_MB(1024 MB)→ 打印处理建议、notify_backup critical …、echo "BACKUP_ABORT reason=disk_full free_mb=$FREE_MB"→exit 1(在写任何东西之前判定,避免写坏半份备份); --dry-run时打印计划与保留策略后echo "BACKUP_DRYRUN ok=1 dest=$DEST"→exit 0。
示例¶
B=/opt/Cy/services/system.d/nfos/backup-daily.sh
# 1) 只打印计划与磁盘情况,不写任何文件
sudo bash "$B" --dry-run
# 2) 正常执行(systemd 单元用的就是这个形式)
sudo bash "$B"
# 3) 覆盖保留份数
sudo bash "$B" --keep 7
sudo bash "$B" --keep=7
# 4) 跳过次节点(离线演练 / 次节点维护期)
sudo bash "$B" --no-secondary
# 5) 组合:演练 + 少留 + 不碰次节点
sudo bash "$B" --dry-run --keep 3 --no-secondary
# 6) 看头部用法(等价于 --help)
sudo bash "$B" --help
# 7) 事后核对产物与校验
ls -l /home/ecs-user/temp/$(date +%F)/
sudo sha256sum -c /home/ecs-user/temp/$(date +%F)/CHECKSUMS.sha256
依赖与权限¶
| 项 | 内容 |
|---|---|
| 解释器 | bash(#!/usr/bin/env bash,set -uo pipefail) |
| 外部命令 | tar、gzip、sha256sum、find、df、stat、awk(启动时逐个校验);sqlite3(首选 .backup 一致性快照);python3(sqlite3.backup 兜底,见「§3 备份一致性修复」);ssh(次节点)、podman(次节点经 SSH 执行)、chown |
| root | 必须(启动前置检查直接拒绝非 root) |
sudo -n |
脚本自身不调用 sudo;但经 SSH 在次节点执行 podman 需要隧道密钥 /opt/Cy/certs/backend/nfos-tunnel |
| 读取 | /opt/Cy/services/logs、/opt/Cy/services/nginx.d、/etc/nginx/nginx.conf、/opt/Cy/services/system.d/nfarmsd、/opt/Cy/services/system.d/nfos、${NFOS_REPO:-/opt/Cy/deploy-in/nfos}、/etc/systemd/system、nfarmsd SQLite;次节点经 SSH |
| 写入 | 产物根 /home/ecs-user/temp/<YYYY-MM-DD>/;脚本自写日志 /home/ecs-user/temp/last-backup.log;平台日志(非 TTY 时由 unit 的 StandardOutput=append:/opt/Cy/services/logs/backend/nfos/backup.log 承担) |
| 网络 | 出站 SSH 到次节点 ${NFOS_SECONDARY:-ubuntu@43.156.170.157}(BatchMode=yes、ConnectTimeout=10、StrictHostKeyChecking=accept-new、UserKnownHostsFile=/opt/Cy/services/system.d/nfos/known_hosts);本机回环 HTTP 上报告警(${NFARMSD_API:-http://127.0.0.1:9990},身份 nfos-backup / admin) |
| 超时预算 | 单元 TimeoutStartSec=1800、TimeoutStopSec=120、Nice=10(注释:日志打包 + pg_dump 可能耗时数分钟;备份是 IO 密集任务,降优先级避免影响线上服务) |
敏感文件排除(EXCLUDES,与 release-collect.sh 保持一致) |
*.env *.key *.pem *.crt *.pfx *.p12、known_hosts monitor_known_hosts、initial-admin-password.txt、aliyun-pan.json、*secret* *token* *password* *passwd* *credential* |
凭据处理口径:私钥 / 口令 / webhook 一律不进备份;含内联凭据的文件做「脱敏复制」, 值替换为
__REDACTED__,并在config/REDACTED.txt登记跳过原因。取值本身见secrets/notification.env等文件(不得转录)。
幂等与失败语义¶
- 幂等:产物按日期目录天然幂等;重复执行同一天只会覆盖/补齐
/home/ecs-user/temp/<YYYY-MM-DD>/。没有去重状态文件。 - 破坏性动作只有一个:按天数淘汰自己的旧备份目录,且有三重保护——
guard_path拒绝空值 / 相对路径 / 系统根级路径(/、/opt、/etc、/home、/opt/Cy、/home/ecs-user…)/ 含../ 长度 < 8;- 目标目录名必须匹配严格日期正则;
- 目标必须带本脚本自己的
MANIFEST.md标记(否则视为交互用户自建目录,~ 跳过(无本脚本 MANIFEST.md 标记,视为用户目录))。 另外:base >= TODAY_STR的一律保留(~ 保留(今天或未来,不删)),find结果sort -r倒序。 - 一致性快照的降级链:
sqlite3 .backup失败 → 退回python3的sqlite3.backup(含 WAL 中未 checkpoint 的事务)→ 绝不退回cp(注释:python3 sqlite3.backup 失败(未回退到 cp,避免产生不一致快照))。每条备份后都做integrity_check,异常则FAIL=1。 - 次节点不可达是「降级不是失败」:
secondary_reachable()返回假(--no-secondary、隧道密钥不可读、或ssh … true失败)→ 退化为「仅主节点」备份,并在 MANIFEST 注明。单元用Wants=(弱依赖)而非Requires=,就是为了「不允许因隧道故障而完全跳过备份」。 - 失败会「留下部分产物」:任一产物失败只置
FAIL=1并继续后续步骤。 - 产物校验:生成
CHECKSUMS.sha256后由脚本自己sha256sum -c复核;生成失败或不通过都会FAIL=1(注释:产物校验失败 → 显式报错并非 0 退出)。 - 属主修正:收尾时若
/home/ecs-user/temp的属主不是root:root,会对本次产物chown -R <原属主>(便于交互用户直接读取;还原命令写在日志里:chown -R root:root $DEST)。last-backup.log置chmod 600。 - 退出码:
| 退出码 | 含义 |
|---|---|
0 |
成功;或 --dry-run;或 --help |
1 |
非 root;缺依赖命令;磁盘空间不足(BACKUP_ABORT reason=disk_full);或 FAIL > 0(exit "$FAIL",FAIL 是累加的失败计数,因此退出码可能大于 1) |
2 |
参数错误(未知参数;--keep 非正整数或小于 1) |
成功/失败会发告警:成功
notify_backup recover "备份成功"(dedup_key nfos.backup.daily.failed→/alerts/recover); 失败notify_backup critical "备份存在失败项(failed=N)…"。 产物最后一行是固定标记:BACKUP_DONE failed=<FAIL> dest=<DEST> size=<TOTAL_SIZE>。
常见错误与排查¶
| 现象 | 代码里的对应分支 | 排查步骤 |
|---|---|---|
错误:需要 root 运行(sudo bash …) |
if [ "$(id -u)" -ne 0 ] → exit 1 |
用 sudo bash …;该行同时会写 [<时间>] ERROR 非 root 执行,已中止 到 last-backup.log |
!! 缺少依赖命令:<c> |
依赖循环 → exit 1 |
装对应包(tar / gzip / coreutils / findutils / gawk) |
错误:--keep 需要正整数,收到 '<值>' |
case "$KEEP_DAYS" in ''\|*[!0-9]*) → exit 2 |
--keep 7 或 --keep=7 |
BACKUP_ABORT reason=disk_full free_mb=… |
磁盘检查 → exit 1 |
按脚本给出的建议清理 /home/ecs-user/temp 下旧的 YYYY-MM-DD 目录,或扩容;同时会发 critical 告警 |
BACKUP_DRYRUN ok=1 dest=… 但没有产物 |
--dry-run 分支 → exit 0 |
这是预期行为(未写入任何文件) |
日志 !! 主节点日志打包失败(tar 退出码 N,错误见 …/tar-primary.err) |
FAIL=1 |
看 $DEST/logs/tar-primary.err;常见原因是磁盘满或权限 |
日志 SQLite <db>:python3 sqlite3.backup 失败(未回退到 cp,避免产生不一致快照) |
FAIL=1 + note |
不要手工 cp 库文件去"补"——那会产生不一致快照。先确认 DB 可读、有空间,再重跑 |
次节点日志/pg_dump 缺失,MANIFEST 里只有主节点 |
secondary_reachable() 为假 |
预期降级。检查 /opt/Cy/certs/backend/nfos-tunnel 是否可读、ssh -i … ubuntu@<次节点> true 是否通;需要临时跳过用 --no-secondary |
CHECKSUMS.sha256 校验不通过 / FAIL>1 |
产物校验分支 | 看 sha256sum -c 的输出定位坏产物;坏产物被 rm -f 且 FAIL=1。重跑当日备份即可(产物按日期目录幂等) |
| 用户目录被误删 | 不应发生(MANIFEST.md 标记 + 日期正则 + guard_path) |
若发生,说明保护被改坏。核对 guard_path 与淘汰循环;--dry-run 会打印保留/淘汰计划 |
| 产物的属主不是 root | 收尾的 chown -R "$OWNER" |
这是刻意设计(便于交互用户读取)。要还原:sudo chown -R root:root "$DEST"(脚本已把这条命令写进日志) |
| systemd 下日志里同一行出现两次 | 日志双写保护 | 脚本在有 TTY 时同时写平台日志与 last-backup.log;非 TTY(systemd)时只写 last-backup.log,平台日志由 unit 的 StandardOutput=append: 负责。若出现重复,检查是否手动 tee 过 |
回滚方式¶
备份是只读产物,本身不需要回滚。要「撤销」的只有两种情形:
| 情形 | 处理 |
|---|---|
| 误删了当日目录 | 重新执行一次(产物按日期目录幂等):sudo bash /opt/Cy/services/system.d/nfos/backup-daily.sh |
| 备份被别的流程误动 / 需要恢复 | 用 MANIFEST.md 里的恢复步骤(源码头部给出的手工回滚) |
源码头部给出的手工恢复命令(逐字):
# 恢复配置
cp -a /home/ecs-user/temp/<日期>/config/nginx.d/. /opt/Cy/services/nginx.d/
# 恢复库
sqlite3 /opt/Cy/services/system.d/nfarmsd/data/nfarmsd.db ".restore '<dump>'"
# 恢复 TSDB(次节点执行;dump 需先 scp 到次节点)
cat <dump> | sudo podman exec -i nfos-timescaledb pg_restore -U postgres -d <db> --clean
回滚脚本本体与 unit:
D=/opt/Cy/services/system.d/nfos
BK=$D/state/unit-backups
# 1) 改之前先备份脚本与 unit(unit 的备份必须放这里,不能放 /etc/systemd/system/)
TS=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 755 "$BK"
sudo cp -a "$D/backup-daily.sh" "$BK/backup-daily.sh.bak-$TS"
sudo cp -a /etc/systemd/system/nfos-backup.service "$BK/nfos-backup.service.bak-$TS"
sudo cp -a /etc/systemd/system/nfos-backup.timer "$BK/nfos-backup.timer.bak-$TS"
# 2) 回滚
sudo cp -a "$BK/backup-daily.sh.bak-<时间戳>" "$D/backup-daily.sh"
sudo chown root:root "$D/backup-daily.sh" && sudo chmod 755 "$D/backup-daily.sh"
sudo cp -a "$BK/nfos-backup.service.bak-<时间戳>" /etc/systemd/system/nfos-backup.service
sudo cp -a "$BK/nfos-backup.timer.bak-<时间戳>" /etc/systemd/system/nfos-backup.timer
sudo chown root:root /etc/systemd/system/nfos-backup.{service,timer}
sudo chmod 644 /etc/systemd/system/nfos-backup.{service,timer}
sudo systemctl daemon-reload
sudo systemctl restart nfos-backup.timer
# 3) 验收:先演练,再真跑
sudo bash "$D/backup-daily.sh" --dry-run
ls -l /etc/systemd/system/ | grep '\.bak' # 必须为空(否则触发 PS-002)
备份落点铁律:脚本/单元的任何备份放
/opt/Cy/services/system.d/nfos/state/unit-backups/(现场既有命名<unit 文件名>.bak-YYYYMMDD-HHMMSS)或仓库侧/opt/Cy/nfarms/.backup/。 绝不能放在/etc/systemd/system/—— 该目录下出现多余 fragment 会触发 PS-002 完整性告警。 见 timers.md 第 11 节。
相关 CHANGELOG¶
| CHANGELOG | 章节标题 |
|---|---|
docs/CHANGELOG-2026-09-26.md |
§3. 备份一致性修复(重要) |
docs/CHANGELOG-2026-09-26.md |
§3.1 现象:每日备份会静默丢掉最近的写入 |
docs/CHANGELOG-2026-09-26.md |
§3.2 修复:在两条分支之间插入 python3 分支 |
docs/CHANGELOG-2026-09-26.md |
§3.3 验证:真跑一次隔离备份 |
docs/CHANGELOG-2026-09-26.md |
§8.4 备份脚本回滚 |
备份规范口径(目标、阿里云盘上传、失败重试、恢复演练)见 NFarms 备份机制规范; 定时器节奏、日志落点与保留策略提醒见 timers.md 第 6 节。