Files
zhangxiang 4ab2f3cd8e chore(infra): 新增 Gitea+Jenkins CI/CD 部署与凭据整改
- infra/cicd:docker-compose(gitea/jenkins)、JCasC(凭据统一 ${ENV} 注入,无硬编码)、
  备份/恢复、健康检查、凭据轮换与 git 历史清除脚本
- docs/deployment/cicd:安装、高可用备份监控、凭据事故复盘
- .env.example 仅为占位模板;真实 .env 由 .gitignore 排除
2026-09-20 10:37:57 +08:00

177 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02 · 高可用 / 数据备份 / 日志监控
脚本源码:[`infra/cicd/backup/`](../../../infra/cicd/backup/)、[`infra/cicd/monitoring/`](../../../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`](../../../infra/cicd/backup/backup-cicd.sh) 的关键设计:
```bash
# 每份产物落盘后立刻校验:非空 + 归档可列出,坏备份当场失败
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`](../../../infra/cicd/backup/restore-cicd.sh) 内置三道防线:
1. 归档完整性校验(tar/gzip 可列出)
2. **`secrets/master.key` 存在性校验** —— 缺失则直接拒绝恢复
(否则恢复完所有 Jenkins 凭据都解不开,等于白做)
3. 恢复前先对当前库做一份快照
```bash
./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%。
已做的两件事:
1. 立即 `truncate -s 0` 释放(磁盘 85% → 81%
2. 兜底 cron 每日 05:10 跑 [`truncate-container-logs.sh`](../../../infra/cicd/monitoring/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%)。
```bash
# FAIL 时 exit 1cron 可直接接告警
./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` 追加拼接 | 用 `|| true` + 正则校验三位数字 |
| 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 确认 |