# 阿里云部署与不停机升级方案

> 前提：上线后**不允许停机维护**。本文给出可落地的部署拓扑、灰度流程与回滚预案。

---

## 一、环境划分

| 环境 | 用途 | 规格建议 | 数据 |
| --- | --- | --- | --- |
| **dev** | 开发自测 | 2C4G × 1 | 造数据 |
| **test** | 集成测试 | 4C8G × 2 | 脱敏数据 |
| **staging** | 预发（**与生产同规格**） | 8C16G × 2 | 生产脱敏快照 |
| **prod** | 生产 | 8C32G × 3（最小） | 真实 |

> **staging 必须与生产同规格**，否则压测与容量评估全部失效。
> 这是最容易被省钱砍掉、然后在上线当天付出代价的一项。

---

## 二、生产最小可用拓扑（一期）

```
                    ┌──────────────┐
                    │  SLB / ALB   │  ← 公网入口，多可用区
                    └──────┬───────┘
                           │
              ┌────────────┴────────────┐
              │        WAF 3.0          │
              └────────────┬────────────┘
                           │
         ┌─────────────────┴──────────────────┐
         │      ACK 集群（3 节点，多可用区）     │
         │  ┌──────────────────────────────┐  │
         │  │ gateway (2 副本)              │  │
         │  │  ├─ 鉴权 / 限流 / 租户识别     │  │
         │  │  └─ 灰度路由                  │  │
         │  ├──────────────────────────────┤  │
         │  │ business-svc (3 副本)         │  │  ← 房源/合同/计费/工单
         │  │ integration-svc (2 副本)      │  │  ← 支付/OTA/签章/AR 集成
         │  │ agent-svc (2 副本)            │  │  ← Agent 能力
         │  │ job-worker (2 副本)           │  │  ← 账单生成/对账/outbox 投递
         │  └──────────────────────────────┘  │
         └─────────────────┬──────────────────┘
                           │
    ┌──────────┬───────────┼────────────┬──────────────┐
    │          │           │            │              │
┌───┴────┐ ┌───┴────┐ ┌────┴─────┐ ┌────┴────┐ ┌──────┴──────┐
│RDS PG  │ │Redis   │ │RocketMQ  │ │  OSS    │ │Elasticsearch│
│主+2只读│ │集群版  │ │专业版    │ │+ CDN    │ │ 3 节点      │
│多可用区│ │双副本  │ │          │ │         │ │             │
└────────┘ └────────┘ └──────────┘ └─────────┘ └─────────────┘

旁路（二期）：ClickHouse（IoT/报表）
```

### 规格建议（一期，100 万房源以内）

| 组件 | 规格 | 说明 |
| --- | --- | --- |
| ACK 节点 | 8C32G × 3 | 多可用区部署，单节点故障不影响 |
| RDS PostgreSQL | 16C64G，ESSD PL1 1TB，多可用区 | 主库 |
| RDS 只读实例 | 8C32G × 2 | ①报表 ②C 端查询 |
| Redis 集群版 | 8G × 3 分片，双副本 | 会话/缓存/锁 |
| RocketMQ | 专业版，4C8G | 事务消息必需 |
| OSS | 标准存储 + CDN | 图片/AR 资源 |
| SLB/ALB | 性能保障型 | 按流量选 |

> ⚠️ **RDS 规格宁大勿小**：数据库升级需要停机或切换，
> 而应用层扩缩容是滚动无感的。**把资源余量留在数据库上。**

---

## 三、不停机升级的标准流程

### 3.1 应用发布（无状态服务）

```
① 代码合并 → CI 构建镜像 → 推送 ACR
② staging 部署 → 自动化回归 + 冒烟
③ 生产灰度：
   ├─ 步骤1：部署 1 个新版本 Pod（占 1/4 流量）
   ├─ 步骤2：观察 30 分钟（错误率/延迟/业务指标）
   ├─ 步骤3：扩到 50% → 观察
   ├─ 步骤4：全量
   └─ 任一步异常 → 立即缩容新版本，流量回旧版本（秒级）
④ 旧版本 Pod 保留至少 30 分钟再下线（保证长连接平滑迁移）
```

**技术前提（缺一不可）：**

| 前提 | 不满足的后果 |
| --- | --- |
| 应用**无状态**（会话在 Redis） | 无法多副本，无法滚动 |
| **优雅关闭**（`preStop` + `SIGTERM` 处理） | 请求被硬断，用户看到错误 |
| **健康检查**（liveness + readiness 分离） | 新 Pod 未就绪就接流量 |
| **数据库变更向后兼容** | 新旧版本数据冲突 |
| **接口幂等** | 重试导致重复扣款 |

### 3.2 数据库变更（最关键，最容易出事）

**必须遵守 Expanding / Contracting 五步法。** 以「账单增加 `refunding` 状态」为例：

| 阶段 | 操作 | 可回滚 | 说明 |
| --- | --- | --- | --- |
| **① Expand** | 放宽 CHECK 约束，允许新值 | ✅ 随时 | 纯 DDL，秒级，不锁表 |
| **② Deploy A** | 新代码可写新值，旧代码读到按 pending 处理 | ✅ 回滚代码即可 | 灰度期间两版共存 |
| **③ Migrate** | 后台分批把存量数据规范化 | ✅ 幂等可重跑 | 每批 5000 行，避免长事务 |
| **④ Deploy B** | 移除兼容旧状态的代码 | ✅ | 此时全量新代码 |
| **⑤ Contract** | 收紧约束、删废弃列 | ❌ 不可回滚 | **必须等所有旧实例下线** |

**红线清单（CI 中应自动拦截）：**

| 操作 | 判定 | 替代方案 |
| --- | --- | --- |
| `ADD COLUMN ... NOT NULL` 无默认值 | 🔴 禁止 | 先 nullable + 回填 + 加约束 |
| `RENAME COLUMN/TABLE` | 🔴 禁止 | 新增 + 双写 + 迁移 + 删除 |
| `ALTER COLUMN TYPE` | 🔴 禁止（大表） | 新增列 + 转换 + 切换 |
| `CREATE INDEX`（不带 CONCURRENTLY） | 🔴 禁止 | `CREATE INDEX CONCURRENTLY` |
| `DROP COLUMN` 后 2 个版本内 | 🟠 警告 | 延后删除 |
| 全表 `UPDATE` | 🔴 禁止 | 分批（LIMIT + 循环） |
| `VACUUM FULL` | 🔴 生产禁止 | `pg_repack` |
| `LOCK TABLE` | 🔴 禁止 | 调整业务逻辑 |

> **配套实现**：`DDL/09-分布式一致性.sql` 中的 `schema_migration` 表带
> `is_destructive` / `is_online_safe` 字段，应在 CI 中强制校验：
> 破坏性变更必须标记并走审批。

### 3.3 账单批生成这类「重任务」的不停机做法

每月 1 号生成数百万张账单，这是最容易造成停机的操作。

**❌ 错误做法**：一个大事务跑完，锁表数分钟。

**✅ 正确做法：**

```
① 按月/租户切分任务，投递到 MQ（每租户一个消息）
② worker 并发消费，每个任务独立事务（只处理一个租户的账单）
③ 幂等：bill 表的 idempotency_key 保证重复消费不会多生成（已验证）
④ 进度可查：任务表记录每个租户的处理状态
⑤ 失败可重试：单租户失败不影响其他租户
⑥ 限流：控制并发数，避免打满数据库
```

**关键点：账单生成必须是「幂等 + 可断点续跑」的。**
月中发现问题可以随时重跑，不会重复生成。

---

## 四、回滚预案

| 场景 | 回滚手段 | 目标时间 |
| --- | --- | --- |
| 应用异常 | 缩容新版本 Pod / 切回旧镜像 | < 1 分钟 |
| 新功能有问题 | **特性开关关闭**（不发版） | < 10 秒 |
| 数据库变更引发问题 | 执行反向迁移脚本 | < 5 分钟 |
| 数据写错 | 按业务单号精确修复（**不做全库回滚**） | 视范围 |
| 主库故障 | RDS 自动主备切换 | 30~60 秒（有闪断） |
| 整个可用区故障 | 多可用区自动切换 | 1~3 分钟 |

**必须提前准备的资产：**
1. 每个数据库变更都配**反向迁移脚本**（写在同一个 PR 里）
2. 关键业务的**数据修复脚本模板**（按单号修复，非全表）
3. 一份**回滚检查清单**，演练过至少一次

---

## 五、监控与告警（上线前必须有）

| 层次 | 指标 | 告警阈值 | 工具 |
| --- | --- | --- | --- |
| 数据库 | 连接数使用率 | > 80% | RDS 监控 |
| 数据库 | 主备延迟 | > 10 秒 | RDS 监控 |
| 数据库 | 慢查询数（>1s） | > 10 条/分钟 | RDS 慢日志 + ARMS |
| 数据库 | **死锁与长事务** | 长事务 > 30 秒 | 自定义告警 |
| 应用 | 接口 P99 延迟 | > 500ms | ARMS |
| 应用 | 错误率 | > 1% | ARMS |
| 业务 | **收租成功率** | < 99% | 自定义埋点 |
| 业务 | 支付回调失败数 | > 0 | 对账任务 |
| 业务 | **outbox 积压量** | > 1000 | 定时查询 |
| 集成 | 第三方接口失败率 | > 5% | integration_call_log |
| 对账 | **对账差异数** | > 0 | 对账任务 |

> **只监控技术指标是不够的。** 上面标粗的三项是业务指标：
> 收租成功率掉点、outbox 积压、对账差异 —— 这三项能最早发现业务级故障。

---

## 六、上线前检查清单

**基础设施**
- [ ] RDS 多可用区已开启，自动备份已验证可恢复
- [ ] 只读副本延迟正常（< 1 秒）
- [ ] Redis 集群双副本，故障切换已演练
- [ ] OSS 已配 CDN，防盗链已开
- [ ] WAF 已开启，限流规则已配

**应用**
- [ ] 无状态（会话在 Redis），优雅关闭已实现
- [ ] liveness / readiness 探针已配，且超时合理
- [ ] 数据库连接池大小已算过（见下）
- [ ] 所有对外接口幂等
- [ ] 重试策略已配（指数退避 + 抖动）

**数据**
- [ ] 大表已分区（bill / payment / room_calendar）
- [ ] 高频查询都有 `tenant_id` 前导索引（**实测差距 1250 倍**）
- [ ] autovacuum 已调优，监控已配（**统计信息滞后导致 280 倍退化**）
- [ ] 反向迁移脚本已准备
- [ ] 备份恢复演练已做过（**必须实际演练，不能只看配置**）

**业务**
- [ ] 支付对账任务已上线，差异告警已配
- [ ] outbox 投递任务在跑，积压告警已配
- [ ] 账单批生成的幂等已验证
- [ ] 灰度开关可用，回滚演练已做

**连接池大小计算（重要）**
```
应用连接数 = Pod数 × (每Pod最大连接数)
必须满足：应用连接总数 < RDS max_connections × 0.8

例：6 个 Pod × 每 Pod 20 连接 = 120 连接
    RDS 16C64G 的 max_connections 约 4000，安全
```

> ⚠️ 但**连接数不是越多越好**。PostgreSQL 的经验值是
> `活跃连接数 ≈ CPU 核数 × 2~4`，超过后性能反而下降。
> 单 Pod 的连接池建议 10~20，靠多 Pod 横向扩展。

---

## 七、二期/三期演进路径

| 触发条件 | 演进动作 |
| --- | --- |
| 只读副本 CPU > 70% | 增加只读副本 |
| 主库 CPU > 50% 持续 | 读写分离细化（C 端走副本） |
| 报表拖慢主库 | 引入 ClickHouse / AnalyticDB |
| 单库 > 2TB | 按 `tenant_id` 分库（ShardingSphere） |
| 单个大租户资源占用过高 | 该租户迁独立库（`isolation_mode` 已预留） |
| 商城/直播上线 | **独立集群 + 独立库**，与主业务物理隔离 |

**分库的技术前提（现在就要准备）：**
1. 所有表必须有 `tenant_id`（**已满足**）
2. 所有查询必须带 `tenant_id`（**RLS 已强制**）
3. **避免跨租户 JOIN**（当前设计已避免）
4. 分布式 ID 生成（当前用 `bigint identity`，分库时需改雪花 ID）
5. 分布式事务方案确定（建议：**避免跨库事务**，用 Saga/最终一致）

> **第 4 点是个隐患**：当前主键用 `GENERATED ALWAYS AS IDENTITY`，
> 分库后会冲突。**建议一期就引入雪花 ID 或号段分配**，
> 避免分库时大规模改主键。这是我在本设计中要提醒的改进项。

---

*配套：`../分布式架构/01-阿里云分布式架构.md`*
