# 小虾 SaaS 生产资源巡检 Runbook

> 状态：生效中  
> 创建时间：2026-06-23  
> 目的：防止生产主机资源过紧导致 SSH/TLS/业务入口卡死。

---

## 1. 背景

2026-06-23 生产 UAT 期间，公开域名曾出现 TLS 握手超时，SSH 出现 banner exchange 超时。后续确认生产主机只有约 1.7GiB 内存且无 swap，短时间系统压力会影响入口层响应。

已完成止血：

- 添加 `/swapfile` 2GiB。
- Worker 默认并发限制为 1。
- `/health` 显示真实发布版本。

仍需持续监控：

- CPU/load。
- 可用内存和 swap 使用率。
- 根分区磁盘使用率。
- Web/API 健康。
- 关键容器运行状态。
- API 版本是否符合期望。

---

## 2. 巡检脚本

脚本路径：

```sh
scripts/production_resource_check.sh
```

默认输出：

```sh
/var/lib/xiaoxia-ci/duty_report.json
```

默认检查：

- 5 分钟 load 是否超过 `CPU 数 * 1.5`。
- 可用内存是否低于 `256MiB`。
- swap 是否存在，swap 使用率是否超过 `60%`。
- 根分区磁盘使用率是否超过 `85%`。
- `http://127.0.0.1:8001/health` 是否可用。
- `http://127.0.0.1:3002/` 是否可用。
- 关键生产容器是否 running/healthy。

---

## 3. 手工运行

在生产主机项目目录执行：

```sh
cd /var/lib/xiaoxia-saas-production/repo
EXPECTED_VERSION=v0.1.15 sh scripts/production_resource_check.sh
```

如果只想生成报告，不指定版本：

```sh
sh scripts/production_resource_check.sh
```

---

## 4. 报告格式

报告 JSON 包含：

- `status`：`healthy` 或 `warning`。
- `alerts`：需要通知老大的告警。
- `info`：正常信息。
- `actions`：自动修复动作，目前保持空数组。
- `metrics`：load、memory、disk、containers、health 等指标。

OpenClaw 心跳可继续读取：

```sh
ssh xiaoxia-server "cat /var/lib/xiaoxia-ci/duty_report.json 2>/dev/null"
```

---

## 5. 建议 cron

需要老大确认后再启用，不自动创建。

建议每 5 分钟巡检一次：

```cron
*/5 * * * * cd /var/lib/xiaoxia-saas-production/repo && EXPECTED_VERSION=v0.1.15 sh scripts/production_resource_check.sh >/var/log/xiaoxia-resource-check.log 2>&1
```

---

## 6. 告警处理建议

### 6.1 可用内存偏低

1. 先查看 `free -h` 和 `docker stats --no-stream`。
2. 确认 Worker 并发是否仍为 1。
3. 如 swap 持续大量使用，规划升级生产机规格。

### 6.2 磁盘超过 85%

1. 先执行只读检查：`du -h -d 1 /var/lib | sort -h`。
2. 优先清理旧 release tar、旧 runtime images、Docker build cache。
3. 清理前必须确认可回滚版本和备份。

### 6.3 API/Web 健康失败

1. 查看 `docker ps`。
2. 查看容器日志。
3. 如 API 重建过，Web 必须 force recreate，避免 nginx 静态 upstream 缓存旧 IP。

### 6.4 版本不一致

1. 确认 `/health` 返回版本。
2. 确认生产容器镜像 tag。
3. 确认 Gitea release task 是否完成。

---

## 7. 长期建议

- 生产机升级到至少 4GiB，推荐 8GiB。
- Gitea 从生产业务机拆分出去。
- Worker 继续保持并发限制，并按任务队列规模再做动态扩容。
- 后续接入正式监控告警系统，而不是只依赖心跳读取 JSON。
