# EasyAI Gateway 三节点 K3s 高可用运行手册

## 范围与目标

本手册覆盖宁波、香港、洛杉矶三个 K3s server 和一个可选专用 Worker agent。宁波、香港
承载 API、Web 与 CloudNativePG；Worker 只调度到带 `easyai.io/worker=true` 标签的节点。
洛杉矶只作为 K3s server/etcd 仲裁节点，并带
`easyai.io/witness=true:NoSchedule` 污点。

第 4 台服务器是站点可配置的纯 K3s agent，不加入 etcd 或 CloudNativePG。节点名和逻辑站点
分别由 `AI_GATEWAY_WORKER_NODE_4_NAME`、`AI_GATEWAY_WORKER_NODE_4_SITE` 指定，固定使用
WireGuard 地址 `10.77.0.4`。当前计划节点为 `easyai-hongkong-worker-2`，归入逻辑
`hongkong` Worker 池；稳定基线为宁波 0 副本、香港 2 副本，并跨香港两台物理机分布。
`easyai.io/worker-only=true:NoSchedule` 污点确保除 Worker 外的业务 Pod 不会调度到该机；
`easyai.io/worker=true` 标签确保容量控制器只把专用 Worker 节点纳入资源预算。接入使用：

```bash
./scripts/cluster/add-worker-node.sh preflight
./scripts/cluster/add-worker-node.sh all
```

新节点与宁波、香港、洛杉矶使用 WireGuard 全互联直连，不经过中继。公网 K3s 直连虽然可以
单独保护 API TLS，但当前 Flannel、Pod 和数据库网络依赖 `10.77.0.0/24`，禁止改成未加密的
公网 VXLAN。巡检以 10 包丢包率低于 1%为硬门禁；宁波和香港路径 RTT 低于 80 ms，洛杉矶
路径低于 300 ms。新节点默认监听标准 UDP/51820，可通过
`AI_GATEWAY_WORKER_NODE_4_WIREGUARD_PORT` 配置；不得为了绕过服务商风控擅自伪装端口。

服务商必须先通过工单书面确认“仅用于 Kubernetes 私有集群互联，不提供 VPN、代理或流量
转发服务”符合其政策；确认后才可在 `.env.local` 设置
`AI_GATEWAY_WORKER_NODE_4_NETWORK_APPROVED=true`。未设置时，脚本只允许执行只读
`preflight`，所有 WireGuard 与 K3s 写操作都会停止。

本地 `.env.local` 保存 `AI_GATEWAY_WORKER_NODE_4_HOST/NAME/SITE` 和 SSH 接入密码，必须保持
`0600` 且不得进入 Git。新节点只保留原有 SSH 入口，封锁 K3s API、kubelet、VXLAN和全部
NodePort 公网入口。节点防火墙继续由原有 UFW 管理，接入脚本
禁止安装与 UFW 冲突的 `iptables-persistent`；UFW 未安装或未启用时必须在预检阶段停止。
节点完成加入和验收后才设置 `AI_GATEWAY_WORKER_NODE_4_ENABLED=true`；候选服务器仅写入本地
配置但尚未加入时必须保持 `false`，避免日常巡检误把候选机计入生产拓扑。

生产上线前的双节点高媒体负载、Worker 强杀和 P24/P28/P32 容量搜索见
[生产同构验收模式与高媒体压力测试](production-acceptance.md)。

- 健康双库：同步复制，RPO=0。
- 单库降级：`dataDurability: preferred` 保持写入，`archive_timeout=5min`，灾难 RPO 目标不超过 5 分钟。生产 WAL 段为 16 MiB；禁止把该值降到 60 秒，否则仅空闲切段就要求跨地域链路持续承载至少 2.24 Mbit/s，会使同步副本在当前带宽下永久追不平。生产同时显式设置 `wal_sender_timeout=30s` 和 `wal_receiver_timeout=30s`，避免 5 秒级控制面或 Pod 网络抖动反复拆除 WAL 流；该配置只降低瞬时抖动放大，不替代同步复制、六向链路和复制日志门禁。
- 数据库故障 RTO：不超过 5 分钟。
- 应用发布：香港验收后再滚动宁波，计划内零停机。
- 公网入口：双 NGINX 加人工 AliDNS 切换。接入带健康检查的 GTM 前，不宣称公网入口自动高可用。

所有命令从仓库根目录执行。生产凭据仅从 `0600` 的 `.env.local` 和
`.local-secrets/cluster/` 读取，不进入 Git、发布 manifest 或验收输出。

## 实施顺序

### 1. 源码与镜像门禁

```bash
cd apps/api && env -u AI_GATEWAY_TEST_DATABASE_URL go test ./... -count=1
cd apps/api && go vet ./...
pnpm install --frozen-lockfile
pnpm lint
pnpm test
pnpm build
shellcheck -x -P . scripts/cluster/*.sh deploy/kubernetes/easyai-ai-gateway-cluster-release
kubectl kustomize deploy/kubernetes/production >/dev/null
./tests/ci/migrations-test.sh
node scripts/ci-validate-migrations.mjs <当前线上完整GitSHA>
```

代码提交并推送到 `origin/main` 后，发布 digest 固定镜像：

```bash
./scripts/publish-release-images.sh --components auto
```

### 2. WireGuard 与 K3s

```bash
./scripts/cluster/bootstrap-wireguard.sh
./scripts/cluster/bootstrap-k3s.sh backup
./scripts/cluster/harden-node-network.sh
./scripts/cluster/bootstrap-k3s.sh install
./scripts/cluster/verify-k3s-quorum.sh --execute
./scripts/cluster/verify-cluster.sh precutover
```

`backup` 会在卸载旧 K3s 前，将 SQLite、manifests 和 staging 资源归档并校验后上传
OSS `easyai-ai-gateway/production/pre-ha/`。`install` 固定
`v1.36.2+k3s1`，使用 embedded etcd、静态 Secret 加密、500ms heartbeat 和 5s election timeout。
K3s containerd 同时配置 Docker Hub、GHCR 与 Quay 的镜像端点；已有集群需要滚动刷新时执行：

```bash
./scripts/cluster/configure-k3s-registries.sh
```

`verify-k3s-quorum.sh` 会依次停止并恢复洛杉矶、香港、宁波 K3s，确认每次剩余两个
server 都保持 etcd quorum。宁波停机可能令香港数据库副本晋升；首次切换前如需恢复宁波
初始主库，显式执行 `promote-cnpg-primary.sh`，故障恢复流程不得自动回切。

#### 香港控制面云盘延迟缓解

生产同构验收前必须用 etcd metrics 的增量窗口检查 backend commit 与 WAL fsync，不能只看
节点 Ready。香港虚拟磁盘若使用 `none` 调度器并出现持续 `Handler timeout`、
`etcdserver: request timed out` 或 PostgreSQL `API server connectivity issue`，可以先应用已验证、
可回滚的 `mq-deadline` 缓解：

```bash
./scripts/cluster/configure-control-plane-io.sh status
./scripts/cluster/configure-control-plane-io.sh apply
```

该脚本只修改香港根块设备调度器，写入一个早于 `k3s.service` 执行的 systemd oneshot；应用时
不重启 K3s。回退命令会删除该 unit 并恢复 `none`：

```bash
./scripts/cluster/configure-control-plane-io.sh rollback
```

这只是尾延迟缓解，不是慢云盘的容量认证。任何 60 秒窗口中 backend commit 平均值达到
50 ms、WAL fsync 达到 15 ms，或仍出现上述控制面硬错误，都必须停止验收。最终修复应升级
为具有稳定同步写延迟的高性能云盘，或按 etcd 成员替换流程迁移香港控制节点；禁止以
`unsafe-no-fsync`、双成员 etcd 或放宽硬门禁代替存储修复。

### 3. 平台和入口预置

```bash
./scripts/cluster/bootstrap-platform.sh prepare dist/releases/<SHA>.json
./scripts/cluster/install-cnpg-plugin.sh
./scripts/cluster/install-etcdutl.sh
./scripts/cluster/install-nginx-edges.sh prepare
./scripts/cluster/install-certificate-sync.sh
./scripts/cluster/install-legacy-cleaner.sh
```

`prepare` 将应用副本保持在 0，但启动 CNPG 双实例、WAL 归档和每日备份。OSS 的 Barman
链路使用 `s3.oss-cn-shanghai.aliyuncs.com`、virtual-hosted addressing 和 boto3
Signature V2；`AWS_CONFIG_FILE` 通过 CNPG projected ConfigMap 同时挂载到主库、副本及恢复
集群。预检会强制产生并归档一个新 WAL，并要求 48 小时内至少存在一份完成的全量备份。
SecretStore
的 4 个值从旧 volume 直接导入 Kubernetes Secret，脚本只校验数量与总字节数，不打印值。

首次切换前如滚动安装令主库落在香港，可显式切回宁波。故障晋升后禁止自动执行该命令：

```bash
./scripts/cluster/promote-cnpg-primary.sh easyai-postgres-1
```

### 4. 02:00–04:00 切换

先执行只读预检：

```bash
./scripts/cluster/migrate-production.sh preflight dist/releases/<SHA>.json
```

进入北京时间窗口后执行：

```bash
./scripts/cluster/migrate-production.sh cutover dist/releases/<SHA>.json
```

脚本依次启用维护页、停止旧 API、完整备份旧 PostgreSQL、上传 OSS、恢复 CNPG、比对数据库
版本/扩展/catalog/表数/序列/关键行数/抽样 ID、运行 digest 固定 migrator、冻结 Worker 启动
两地应用、做 NodePort 验收，再切换双 NGINX。公开流量提交后，先启用香港 Worker，再启用宁波
Worker。脚本等待切换后的 CNPG 全量备份状态变为 `completed` 后，才停止旧 Docker
PostgreSQL；数据卷继续保留，不自动删除。

在公共流量提交前，任一步失败或耗时超过 10 分钟都会恢复旧 Docker 与旧 NGINX。公共流量
提交后禁止自动切回旧数据库，避免双写分叉。

## 上线验收

```bash
./scripts/cluster/verify-cluster.sh postcutover
./scripts/cluster/run-cross-node-file-e2e.sh
./scripts/cluster/failure-drill.sh api --execute
./scripts/cluster/failure-drill.sh database --execute
./scripts/cluster/failure-drill.sh witness --execute
./scripts/cluster/restore-backup-drill.sh --execute
./scripts/cluster/verify-etcd-backup.sh
```

跨节点 E2E 通过宁波私有 NGINX 固定进入宁波 API，关闭宁波 River Worker，由香港 Worker
执行真实 multipart 图片编辑任务；校验输入和输出 URL 在两节点下载的长度与 SHA-256
一致，并检查 `river_job.attempted_by`、唯一成功 attempt、唯一结算/扣费及无重复回调。

数据库故障演练会强制删除当前主 Pod，确认 5 分钟内晋升；在副本恢复前写入一条专用
`system_settings` 探针，恢复同步后删除。备份恢复演练会从 OSS 创建一个独立 CNPG Cluster，
验证数据库登录、schema、行数和抽样值后删除临时 Cluster 与 PVC。

## 发布与回滚

切换成功后，本地环境自动设置 `AI_GATEWAY_DEPLOY_MODE=kubernetes`。日常发布仍使用：

```bash
./scripts/deploy-production-release.sh dist/releases/<SHA>.json
./scripts/deploy-production-release.sh --status
./scripts/deploy-production-release.sh --rollback <历史完整SHA>
```

集群 helper 从 `easyai-gateway-release` ConfigMap 读取当前 release，数据库迁移前先等待一次
CNPG 备份，然后香港滚动、验收，再滚动宁波。`--rollback` 只恢复应用 digest，绝不逆向执行迁移。

## 旧文件与公网故障

宁波每小时执行一次清理，以当前时间减文件 `mtime` 严格大于 86,400 秒为删除条件。未过期
文件由宁波只读 NGINX 提供，香港通过 WireGuard 回源。清理结果持续为 0 个存量文件后，删除
两个 legacy static include 和宁波 18080 origin；历史过期 URL 返回 410。

公网入口故障时，先使用 `curl --resolve` 确认香港入口健康，再人工将 `ai.51easyai.com`
AliDNS A 记录切到香港 IP。恢复后不自动回切；确认连接和任务稳定后再择时切回。
