跳转至

每日备份(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 单元的形式)。

前置检查顺序(任一不过就中止):

  1. root:id -u 不是 0 → 错误:需要 root 运行(sudo bash $0)。读取 nfarmsd SQLite、容器数据与 /opt/Cy 配置需要 root。 → exit 1;
  2. 依赖命令:tar gzip sha256sum find df stat awk 逐个 command -v 检查,缺一个 → !! 缺少依赖命令:<c> → exit 1;
  3. --keep 合法性:非数字 → 错误:--keep 需要正整数,收到 '<值>' → exit 2;小于 1 → 错误:--keep 至少为 1 → exit 2;
  4. 磁盘空间:df -Pk "$TEMP_ROOT" 可用空间 < MIN_FREE_MB(1024 MB)→ 打印处理建议、notify_backup critical …、echo "BACKUP_ABORT reason=disk_full free_mb=$FREE_MB" → exit 1(在写任何东西之前判定,避免写坏半份备份);
  5. --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 节。