- infra/cicd:docker-compose(gitea/jenkins)、JCasC(凭据统一 ${ENV} 注入,无硬编码)、
备份/恢复、健康检查、凭据轮换与 git 历史清除脚本
- docs/deployment/cicd:安装、高可用备份监控、凭据事故复盘
- .env.example 仅为占位模板;真实 .env 由 .gitignore 排除
8.1 KiB
02 · 高可用 / 数据备份 / 日志监控
脚本源码:infra/cicd/backup/、infra/cicd/monitoring/。
本文所有数字均为 2026-09-14 在生产实测所得。
1. 高可用:先说清现状边界
当前是单机部署,没有真正的 HA。 不要被 compose 里的 deploy.replicas 迷惑 ——
那是 Swarm 语法,docker compose 非 swarm 模式下完全忽略,replicas 实际恒为 1。
| 组件 | 现状 | 单点风险 | 建议(按性价比排序) |
|---|---|---|---|
| Jenkins | 单实例 | 构建历史丢失(可重建) | 接受单点,靠备份,不值得上 HA |
| Gitea | 单实例 | 代码托管中断(clone 有本地副本) | 接受单点 + 异地备份;量大了再考虑主从 |
| PostgreSQL | 单实例 | 用户/权限/PR/Issue/Webhook 唯一副本 | 最高优先级,每日备份必保 |
| nginx | 单实例 | 全站入口 | 20G/2C 小机,先做日志轮转与监控,不建议硬上 keepalived |
| 宿主机 | 单机 | 全部 | 异地备份是唯一真正有效的容灾 |
结论:这台 2C/20G 机器的 HA 策略 = 快速恢复能力(备份 + 恢复演练 + 运维手册), 而不是冗余部署。冗余的成本(至少 3 台机器 + 运维复杂度)远超当前业务规模所需。
2. 数据备份
2.1 备份什么(以及为什么这样分)
| 数据 | 体积(实测) | 丢失后果 | 策略 |
|---|---|---|---|
| Gitea PostgreSQL | 1.7MB | 不可重建:用户、权限、PR、Issue、webhook | 每日必备 |
| Jenkins 配置 | 18KB | 作业定义、凭据、用户 | 每日必备 |
| Jenkins plugins | 265M | 可从清单重装 | 不备份,导出清单 |
| Gitea 裸库 | 939M | Git 分布式,每份 clone 都是完整历史 | 每周全量 + 异地 |
| jenkins_home/war | 111M | 镜像自带 | 不备份 |
关键取舍:Jenkins 薄备份原本 222MB,99% 是
plugins/。 排除后降到 18KB,磁盘压力从"不可持续"变为"可忽略"。 恢复时用jenkins-plugins-<ts>.txt+jenkins-plugin-cli重装即可。
2.2 备份脚本
backup-cicd.sh 的关键设计:
# 每份产物落盘后立刻校验:非空 + 归档可列出,坏备份当场失败
verify_archive "$f" tar
# 磁盘预检:剩余 < MIN_FREE_GB 直接中止(防写满拖垮 nginx)
if (( FREE_GB < MIN_FREE_GB )); then fail "..."; fi
# Jenkins 归档容忍退出码 1("读取时文件变化"),>=2 才算失败
if (( TAR_RC >= 2 )); then fail "..."; fi
实测输出(首份全量备份):
✅ gitea-db-20260914_174059.sql.gz 1.7MB
✅ gitea-repos-20260914_174059.tar.gz 939MB (3177 条目)
✅ jenkins-config-20260914_174059.tar.gz 222MB (2829 条目)
✅ 凭据解密所需的 master key 与 hudson.util.Secret 均已包含
2.3 调度(已安装到生产 crontab)
30 3 * * 1-6 backup-cicd.sh --skip-repos # 周一至周六:薄(~2MB)
30 3 * * 0 backup-cicd.sh # 周日:全量(~940MB)
保留策略按类型分开计数(BACKUP_FULL_RETENTION=1、BACKUP_THIN_RETENTION=3):
踩坑记录:若统一按「最近 N 份」裁剪,周中的薄备份会把周日全量挤出窗口, 仓库实际只受保护 3 天。分层计数后才真正保住全量。
2.4 恢复
restore-cicd.sh 内置三道防线:
- 归档完整性校验(tar/gzip 可列出)
secrets/master.key存在性校验 —— 缺失则直接拒绝恢复 (否则恢复完所有 Jenkins 凭据都解不开,等于白做)- 恢复前先对当前库做一份快照
./restore-cicd.sh --from /home/novalon/backups/cicd/<ts> --component jenkins --dry-run
./restore-cicd.sh --from /home/novalon/backups/cicd/<ts> --component jenkins
铁律:未做过恢复演练的备份等于没有备份。 每季度至少跑一次 --dry-run。
2.5 ⚠️ 当前最大缺口:仅本地备份
同盘备份不抗主机故障(盘坏/被删/勒索 = 全没)。脚本会持续警告:
⚠️ 未配置 RCLONE_REMOTE —— 备份仅存本地。同盘备份不抗主机故障。
待办(需要决策):
- 配置对象存储(COS/OSS/S3 任一),
rclone安装后在.env填RCLONE_REMOTE - 或扩容磁盘后仍建议至少一份异地
3. 日志
3.1 已知事故:849MB 单份日志
2026-09-14 实测:novalon-cicd/docker-compose.yml 里明确配了
logging: { max-size: 10m, max-file: 3 },但运行中容器的
HostConfig.LogConfig 实际是 {"Type":"json-file","Config":{}} —— 空配置。
根因:logging 参数在容器创建时固化,事后改 compose 不重建就不生效。 Gitea 日志因此涨到 849MB,占整盘 4%。
已做的两件事:
- 立即
truncate -s 0释放(磁盘 85% → 81%) - 兜底 cron 每日 05:10 跑
truncate-container-logs.sh
终态修复(待维护窗口):用 infra/cicd/docker-compose.gitea.yml 重建容器,
使 logging 配置真正生效,之后兜底脚本可退化为纯监控。
3.2 日志留存
| 来源 | 位置 | 轮转 |
|---|---|---|
| 容器 stdout | /var/lib/docker/containers/<id>/*-json.log |
重建后 10m×3;兜底 cron 每日截断 >100MB |
| nginx access/error | /var/log/nginx/*.log |
docker-compose-nginx.yml 挂载宿主 |
| 备份 | /var/log/cicd-backup.log |
无(体积小,建议加 logrotate) |
| 健康检查 | /var/log/cicd-healthcheck.log |
无(同上) |
4. 监控
4.1 healthcheck-cicd.sh(每 10 分钟)
6 个维度,全部实测可跑:
[1] 容器状态 gitea/jenkins/postgresql/nginx 的 healthy
[2] HTTPS 端点 三个域名 200/303
[3] TLS 证书 剩余天数(<21 天 WARN,<7 天 FAIL)
[4] 磁盘与卷 使用率(>=80 WARN,>=90 FAIL)+ jenkins_home 体积
[5] 备份新鲜度 最新备份距今 <36h + 产物数 >=2
[6] Jenkins 队列 积压 >10 任务 FAIL
实测首跑即抓到 2 个真问题(容器 healthcheck 判定 bug 已修 + 磁盘 90%)。
# FAIL 时 exit 1,cron 可直接接告警
./healthcheck-cicd.sh # 全量输出
./healthcheck-cicd.sh --quiet # 只在 FAIL/WARN 时输出
./healthcheck-cicd.sh --json # 供采集器
4.2 告警
.env 配 ALERT_WEBHOOK_URL(企业微信/钉钉 webhook)后,
健康检查 FAIL 和备份失败都会推送。
4.3 现有监控资产
scripts/monitoring/ 已有轻量方案(setup-lightweight-monitoring.sh 等),
本次未改动。若要上 Prometheus,注意这台机器磁盘余量,指标留存周期要设短。
5. 已知环境陷阱(排障速查)
| 症状 | 真因 | 处理 |
|---|---|---|
| compose 配了资源限制但没生效 | deploy: 段非 swarm 下被忽略 |
用 cpus/mem_limit |
| compose 配了日志轮转但没生效 | logging 参数创建时固化 | 重建容器 |
grep -q 在 set -o pipefail 下恒假 |
-q 提前退出 → 上游 SIGPIPE(141) |
用 herestring 或 grep -c |
| 函数做布尔用却返回非 0 | f() { [[ cond ]] && echo; } 在 cond 为假时返回 1,cond2 && f || g 会跌进 g |
函数末尾显式 return 0 |
健康检查 FAIL 数随 --quiet/--json 抖动 |
同上:输出被抑制 → 助手函数返回 1 | 同上 |
curl -w '%{http_code}' || echo 000 得到 200000 |
curl 已输出 200 但退出码非 0,echo 追加拼接 |
用 ` |
| tar 报错但文件都存在 | 归档列表里写了不存在的路径(如 nodes/) |
归档 . + --exclude |
for x in "$@" 内 shift 取错值 |
for 迭代的是展开后的固定列表,shift 不影响序列 | 用 while [[ $# -gt 0 ]] 解析 |
| SSH 备份脚本中途死掉 | 断连触发 SIGHUP | cron 场景无此问题;手动跑用 setsid |
/root 占 2.7G |
/root/.trae-cn-server(bin 2.1G + manager-logs 522M) |
属 AI 工具数据,清理需 owner 确认 |