720 lines
30 KiB
Markdown
720 lines
30 KiB
Markdown
# Docker → Kubernetes 迁移 设计文档
|
|
|
|
- 日期: 2026-06-08
|
|
- 涉及模块: `docker/` 全部产物、新增 `k8s/` 目录、未来四个新服务
|
|
- 现状: `docker-compose.prod.yml` 在单台 VM (`101.132.250.62`) 上跑 10 个 Go 服务 + Postgres + Redis
|
|
- 目标: 分两阶段把服务搬到 Kubernetes
|
|
|
|
---
|
|
|
|
## 〇、分阶段策略 (重要)
|
|
|
|
为避免过早引入复杂的多租户隔离,本次迁移明确分为**两个独立阶段**,每个阶段是单独的任务、设计、实现、验证周期:
|
|
|
|
### 第一阶段 (本次任务,合并部署降本) ⭐ 当前
|
|
|
|
- **范围**: 把现有 10 个 Go 服务 + 外部 DB 全部迁到 K8s,**所有明星共享一个 namespace、一套部署**
|
|
- **目标**:
|
|
- 脱离单 VM 限制,利用 K8s 自动扩缩容和资源调度
|
|
- 降低基础设施成本(按需分配 Pod,不用维护一台大 VM)
|
|
- 为后续接 admin/review/ai-* 新服务铺路
|
|
- **不做**:
|
|
- ❌ 不做按明星组的 namespace 拆分
|
|
- ❌ 不做 ResourceQuota / LimitRange
|
|
- ❌ 不做应用层多租户改造 (group_id 字段)
|
|
- ❌ 不实现新服务 (admin / review / ai-image-gen / ai-chat)
|
|
|
|
### 第二阶段 (未来任务,按组隔离扩展)
|
|
|
|
- **触发条件**: 第一阶段稳定运行后,头部明星流量增长到需要独立扩展 / 出现吵市占率问题
|
|
- **范围**: 把第一阶段合并的部署,按"明星组"拆到独立 namespace,每组独立 gateway + 9 个数据服务
|
|
- **详细设计**: 见本文档 **第十一章 (Phase 2 未来设计)**,本阶段不实现
|
|
|
|
> **本设计文档主要覆盖第一阶段**,但保留第二阶段的设计草图作为远期参考。
|
|
|
|
---
|
|
|
|
## 一、背景与动机
|
|
|
|
### 1.1 业务背景
|
|
|
|
TopFans 是一个"明星粉丝平台"。不同明星的粉丝量差异极大:
|
|
|
|
- **头部明星**: 单明星可占整平台 50%+ 流量
|
|
- **腰部明星**: 几个明星可共享资源池
|
|
- **尾部明星**: 大量冷启动明星,合并部署降低成本
|
|
|
|
**当前阶段的核心诉求是"合并部署降本"** — 用 K8s 替代单 VM,利用云的弹性降低基础设施成本。**不做分组隔离**,所有明星共享一套基础设施。
|
|
|
|
### 1.2 当前架构瓶颈
|
|
|
|
`docker-compose.prod.yml` 是单机部署,本质限制:
|
|
|
|
| 能力 | Docker Compose | Kubernetes |
|
|
|---|---|---|
|
|
| 多机部署 | ❌ | ✅ |
|
|
| 自动扩缩容 | ❌ | ✅ (HPA/KEDA) |
|
|
| 滚动升级 | 手动 | ✅ |
|
|
| 故障自愈 | 手动 | ✅ |
|
|
| 资源按需分配 | ❌ (固定 VM) | ✅ (Pod request/limit) |
|
|
| 新增业务线 | 改 compose | helm install |
|
|
|
|
### 1.3 即将到来的新服务
|
|
|
|
代码或 PRD 中已提及、未来需要独立部署的**新服务**。**与现有服务的关系各不相同**,明确如下:
|
|
|
|
| 服务 | 作用 | 与现有服务的关系 |
|
|
|---|---|---|
|
|
| `admin` | 后台管理平台(运营、客服) | **共享数据库**: 直接连同一个 PostgreSQL 实例(可能不同 schema,共享用户/订单/内容数据),不走 API 调用 |
|
|
| `review` | 审核工作流(UGC 内容审核) | **被现有后端调用**: 现有后端(gateway / assetservice)调用 review 的 API 提交审核任务,review 异步审核后结果回写(可通过 callback / 消息 / 共享表) |
|
|
| `ai-image-gen` | AI 图片生成(镭射卡) | **被现有后端调用**: gateway 改为调用 ai-image-gen 服务的 API(原 MiniMax 调用逻辑迁过去) |
|
|
| `ai-chat` | AI 对话(粉丝互动) | **被现有后端/前端调用**: 现有后端/前端调用 ai-chat 服务的 API(它是新服务,不是从 aichatservice 拆出) |
|
|
|
|
> **关键澄清**:
|
|
> - `admin` **不走 API**,通过共享 DB 直连,**权限和访问范围由 DB 账号/RBAC 控制**
|
|
> - `review` / `ai-image-gen` / `ai-chat` 是**被调用方**,由现有后端(gateway / 前端)主动调用
|
|
> - 这四个服务与现有 `userservice` / `assetservice` / `aichatservice` 等**没有代码级复用关系**
|
|
> - K8s 迁移任务只负责**为它们预留 namespace 位置和部署模板**,不涉及实现
|
|
|
|
---
|
|
|
|
## 二、候选方案 (Phase 1 范围)
|
|
|
|
第一阶段的核心问题是:**要不要迁移到 K8s?如果要,采用什么部署结构?** 给出三个候选:
|
|
|
|
### 2.1 方案 A:不迁移,继续用 Docker Compose
|
|
|
|
**结构**: 维持现状,`docker-compose.prod.yml` 在 VM 上跑。
|
|
|
|
**优点**:
|
|
|
|
1. ✅ **零迁移成本**: 不用动任何东西
|
|
2. ✅ **运维熟悉度最高**: 团队已经在用
|
|
|
|
**缺点**:
|
|
|
|
1. ❌ **单 VM 容量上限**: 2 CPU + 4G RAM 是硬顶,加新服务或新功能会爆
|
|
2. ❌ **没有自动扩缩容**: 流量上来要么硬抗,要么手动加机器
|
|
3. ❌ **没有故障自愈**: 容器挂了要人肉拉起
|
|
4. ❌ **基础设施成本固定**: 不管流量高低都付同样的钱
|
|
5. ❌ **不符合"合并部署降本"目标**: 单 VM 模式无法按需伸缩
|
|
|
|
**结论**: **不推荐**。解决不了用户的核心诉求。
|
|
|
|
---
|
|
|
|
### 2.2 方案 B:K8s 单 namespace 合并部署 ⭐ 第一阶段推荐
|
|
|
|
**结构**: 把 10 个 Go 服务全部部署到 K8s,所有服务在同一个 namespace 下,共享一个 gateway。
|
|
|
|
```
|
|
namespace: topfans
|
|
├── gateway (Deployment + ClusterIP Service + Ingress)
|
|
├── userservice (Deployment + ClusterIP Service)
|
|
├── assetservice (Deployment + ClusterIP Service)
|
|
├── galleryservice (Deployment + ClusterIP Service)
|
|
├── socialservice (Deployment + ClusterIP Service)
|
|
├── activityservice (Deployment + ClusterIP Service)
|
|
├── starbookservice (Deployment + ClusterIP Service)
|
|
├── taskservice (Deployment + ClusterIP Service)
|
|
├── aichatservice (Deployment + ClusterIP Service)
|
|
├── lasercompositor (Deployment + ClusterIP Service)
|
|
├── postgres-ext (Service ExternalName → RDS endpoint)
|
|
├── redis-ext (Service ExternalName → ElastiCache endpoint)
|
|
├── secrets (DB / OSS / JWT / AI API keys)
|
|
└── configmaps (各服务配置)
|
|
Ingress → topfans/gateway:8080
|
|
```
|
|
|
|
**优点**:
|
|
|
|
1. ✅ **完全对齐第一阶段目标**: 合并部署,利用 K8s 弹性降本
|
|
2. ✅ **应用代码零改动**: Dubbo tri:// 用 K8s 短 DNS 名(`tri://userservice:20000`),与原 compose 完全一致
|
|
3. ✅ **自动扩缩容**: HPA 可对 gateway / 高负载服务(如 aichatservice)按 CPU/内存扩缩
|
|
4. ✅ **故障自愈**: Pod 挂了 K8s 自动重启
|
|
5. ✅ **滚动升级**: `helm upgrade` 自动滚动
|
|
6. ✅ **资源按需分配**: 不用维护一台大 VM,Pod 资源 request/limit 精细控制
|
|
7. ✅ **为第二阶段铺路**: 后续要按组拆 namespace,只需 helm values 调整,模板已就位
|
|
8. ✅ **新服务接入简单**: admin / review / ai-* 加几个 Deployment 即可
|
|
|
|
**缺点**:
|
|
|
|
1. ⚠️ **一次性迁移工作量**: 镜像推到 ACR、域名切换、数据迁移(Docker → RDS)
|
|
2. ⚠️ **没有按明星隔离**: 头部明星的爆款活动仍可能影响尾部(可接受,这就是 Phase 1 的取舍)
|
|
3. ⚠️ **DB 走外部托管**: 一次性数据迁移风险(可用阿里云 DTS 平滑迁移)
|
|
4. ⚠️ **新服务只在 K8s 占位**: 本阶段不实现,只是预留
|
|
|
|
**适用场景**: 业务初期/中期、流量逐渐增长、需要脱离单 VM 限制、为未来扩展留空间。**完全匹配第一阶段目标**。
|
|
|
|
**结论**: **第一阶段强烈推荐**。
|
|
|
|
---
|
|
|
|
### 2.3 方案 C:从第一天就做按组隔离的 K8s
|
|
|
|
**结构**: 直接上"namespace per group",把方案 B 的 Phase 1 + Phase 2 合并到一次完成。
|
|
|
|
**优点**:
|
|
|
|
1. ✅ 一步到位,不需要后续重构
|
|
|
|
**缺点**:
|
|
|
|
1. ❌ **过度工程**: 当前业务量不需要,合并部署完全够用
|
|
2. ❌ **迁移复杂度高**: 一次要把"VM → K8s"和"合并 → 隔离"两个变更叠在一起,排查问题难
|
|
3. ❌ **每组 ResourceQuota / 跨 ns Dubbo / Ingress Host 路由** — 都是当前用不到的能力,徒增维护成本
|
|
4. ❌ **违反"YAGNI"原则**: 没遇到的问题不要预先解决
|
|
|
|
**结论**: **不推荐**。会拖慢第一阶段交付。
|
|
|
|
---
|
|
|
|
## 三、方案对比总结表 (第一阶段)
|
|
|
|
| 维度 | A 不迁移 | B 单 namespace 合并 ⭐ | C 第一天就分组 |
|
|
|---|---|---|---|
|
|
| 迁移成本 | 0 | 中等 | 高 |
|
|
| 是否解决"降本"诉求 | ❌ | ✅ | ✅ |
|
|
| 应用代码改动 | 0 | 0 | 少量(可能要改 Dubbo URL) |
|
|
| 故障自愈 | ❌ | ✅ | ✅ |
|
|
| 自动扩缩容 | ❌ | ✅ | ✅ |
|
|
| 后续按组隔离成本 | — | 中(需 helm chart 拆 + 应用层 group_id 改造,详见 §11.5 ~2-3 个月) | 已经是 |
|
|
| 复杂度 | 最低 | 适中 | 高 |
|
|
| 与第一阶段目标匹配 | ❌ | ✅✅✅ | ✅(但过度) |
|
|
| **推荐度** | ❌ | ✅✅✅ 强烈推荐 | ❌ |
|
|
|
|
---
|
|
|
|
## 四、推荐方案 (B) 详细设计 — 第一阶段:合并部署
|
|
|
|
### 4.1 集群拓扑 (Phase 1)
|
|
|
|
```
|
|
K8s 集群 (单 namespace,所有服务合并)
|
|
│
|
|
└── namespace: topfans
|
|
├── gateway — Deployment (1+ replicas) + Service (ClusterIP) + Ingress
|
|
├── userservice — Deployment (1+ replicas) + Service (ClusterIP)
|
|
├── assetservice — Deployment (1+ replicas) + Service (ClusterIP)
|
|
├── galleryservice — Deployment (1+ replicas) + Service (ClusterIP)
|
|
├── socialservice — Deployment (1+ replicas) + Service (ClusterIP)
|
|
├── activityservice — Deployment (1+ replicas) + Service (ClusterIP)
|
|
├── starbookservice — Deployment (1+ replicas) + Service (ClusterIP)
|
|
├── taskservice — Deployment (1+ replicas) + Service (ClusterIP)
|
|
├── aichatservice — Deployment (1+ replicas) + Service (ClusterIP)
|
|
├── lasercompositor — Deployment (1+ replicas) + Service (ClusterIP)
|
|
│
|
|
├── (未来占位) admin — .gitkeep,无 Deployment
|
|
├── (未来占位) review — .gitkeep
|
|
├── (未来占位) ai-image-gen — .gitkeep
|
|
├── (未来占位) ai-chat — .gitkeep
|
|
│
|
|
├── postgres-ext — Service (ExternalName → RDS endpoint)
|
|
├── redis-ext — Service (ExternalName → ElastiCache endpoint)
|
|
│
|
|
├── secrets/ — 各类 Secret (DB / OSS / JWT / AI API keys)
|
|
└── configmaps/ — 各类 ConfigMap (env / app config)
|
|
|
|
Ingress (集群级)
|
|
└── api.example.com → topfans/gateway:8080
|
|
```
|
|
|
|
**关键特征**:
|
|
- **单 namespace**: 所有服务在 `topfans` 一个 namespace,简单清晰
|
|
- **单 gateway**: 全部流量经 gateway,内部短 DNS 名互调
|
|
- **HPA (水平自动扩缩)**: 可对 gateway / aichatservice 等高负载服务启用,按 CPU 扩
|
|
- **无 ResourceQuota**: 第一阶段不限制单组资源(没有组的概念)
|
|
- **无 namespace 拆分**: 不做多租户隔离(第二阶段再做)
|
|
|
|
### 4.2 关键设计点
|
|
|
|
#### 4.2.1 服务间调用 (Gateway 调后端)
|
|
|
|
所有服务在同一 namespace,Dubbo tri:// 用 K8s 短 DNS 名:
|
|
|
|
```yaml
|
|
# gateway 的环境变量 (相对原 compose 几乎无改动)
|
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
|
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
|
DUBBO_SOCIAL_SERVICE_URL: tri://socialservice:20002
|
|
DUBBO_GALLERY_SERVICE_URL: tri://galleryservice:20001
|
|
DUBBO_ACTIVITY_SERVICE_URL: tri://activityservice:20004
|
|
DUBBO_TASK_SERVICE_URL: tri://taskservice:20006
|
|
DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20005
|
|
DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008
|
|
LASER_COMPOSITOR_URL: http://lasercompositor:7002
|
|
```
|
|
|
|
**应用代码零改动** — 与原 `docker-compose.prod.yml` 几乎完全一致,只是服务名沿用,无需改任何代码。
|
|
|
|
#### 4.2.2 Postgres / Redis 走外部托管
|
|
|
|
K8s 内只创建 `ExternalName` Service 占位:
|
|
|
|
```yaml
|
|
# k8s/helm/topfans/templates/external-db/postgres-external.yaml
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: postgres
|
|
namespace: topfans
|
|
spec:
|
|
type: ExternalName
|
|
externalName: rm-xxxxxx.mysql.rds.aliyuncs.com # 阿里云 RDS endpoint
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: redis
|
|
namespace: topfans
|
|
spec:
|
|
type: ExternalName
|
|
externalName: r-xxxxxx.redis.rds.aliyuncs.com # 阿里云 Redis endpoint
|
|
```
|
|
|
|
**好处**:
|
|
|
|
- K8s 不需要管 StatefulSet / PVC / 备份 / 主从切换(全部交给云服务)
|
|
- 应用代码不变,继续用 `DB_HOST=postgres`、`REDIS_HOST=redis` 这种短名
|
|
- 后续压力大了,云上一键升配即可
|
|
|
|
**数据迁移**: 从 VM 上的 Postgres 迁到阿里云 RDS,推荐用阿里云 DTS 做平滑迁移,详见第六章。
|
|
|
|
#### 4.2.3 镜像构建与推送策略
|
|
|
|
**两种选择**:
|
|
|
|
- **方式 1 (推荐)**: CI 构建镜像推送阿里云 ACR,Helm 通过 `image.tag` 引用
|
|
- **方式 2**: 维持当前 `deploy.sh` 的"本地 build → SSH 推到服务器"流程,服务器装 containerd/nerdctl
|
|
|
|
**推荐方式 1**,原因:
|
|
|
|
- 镜像版本化管理 (`:v1.0.0`)
|
|
- 利用镜像分发,build 一次,所有 K8s 节点共享
|
|
- rollback 简单: `helm rollback topfans 1`
|
|
|
|
`Dockerfile.services` 保持不变(已是多阶段构建 OK),CI 脚本里改 tag 即可。
|
|
|
|
#### 4.2.4 健康检查
|
|
|
|
**改用 K8s `livenessProbe` / `readinessProbe` 显式配置**,写在 Helm chart 里的 Deployment 中,而不是依赖 Dockerfile 的 HEALTHCHECK。
|
|
|
|
> 顺带说一下:原 `docker/Dockerfile.services` 有 HEALTHCHECK 端口错配 bug(galleryservice 等用 21001 实际监听 20001),K8s 不依赖 Dockerfile HEALTHCHECK,所以**Phase 1 不修这个 bug**,保持向后兼容,留待单独 issue 处理。详细说明见 §10.4。
|
|
|
|
#### 4.2.5 Secrets 管理
|
|
|
|
```yaml
|
|
# k8s/helm/topfans/templates/secrets/db-credentials.yaml (示例)
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: db-credentials
|
|
namespace: topfans
|
|
type: Opaque
|
|
stringData:
|
|
DB_PASSWORD: <占位>
|
|
REDIS_PASSWORD: <占位>
|
|
JWT_SECRET: <占位>
|
|
OSS_ACCESS_KEY_ID: <占位>
|
|
OSS_ACCESS_KEY_SECRET: <占位>
|
|
DIFY_API_KEY: <占位>
|
|
MINIMAX_API_KEY: <占位>
|
|
QWEN_API_KEY: <占位>
|
|
```
|
|
|
|
**安全约束**:
|
|
- 模板里**只放占位符**,真值不入 git
|
|
- `values-prod.yaml` 加入 `.gitignore`,只提交 `values-prod.example.yaml`(空值)
|
|
- 真值由 CI/CD 注入,或后续接 External Secrets Operator / 阿里云 KMS
|
|
|
|
#### 4.2.6 HPA (水平自动扩缩)
|
|
|
|
为高负载服务启用 HPA,典型配置:
|
|
|
|
```yaml
|
|
apiVersion: autoscaling/v2
|
|
kind: HorizontalPodAutoscaler
|
|
metadata:
|
|
name: gateway
|
|
namespace: topfans
|
|
spec:
|
|
scaleTargetRef:
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
name: gateway
|
|
minReplicas: 2
|
|
maxReplicas: 10
|
|
metrics:
|
|
- type: Resource
|
|
resource:
|
|
name: cpu
|
|
target:
|
|
type: Utilization
|
|
averageUtilization: 60
|
|
```
|
|
|
|
**第一阶段默认配置**: gateway / aichatservice / lasercompositor 启用 HPA(它们是流量敏感型);其他服务保持 replicas=1,后续按需调整。
|
|
|
|
#### 4.2.7 不做的事 (明确边界)
|
|
|
|
第一阶段**不做**:
|
|
|
|
- ❌ **多 namespace**: 不做 per-group 拆分(留给第二阶段)
|
|
- ❌ **ResourceQuota / LimitRange**: 第一阶段没有"组"的概念
|
|
- ❌ **应用层多租户改造**: 不在数据库加 `group_id` 字段,不在 JWT 透传 group_id(留给第二阶段)
|
|
- ❌ **新服务实现**: admin / review / ai-* 只留 `.gitkeep` 占位
|
|
- ❌ **跨 namespace Dubbo**: 不需要(所有服务同 ns)
|
|
- ❌ **Service Mesh**: Istio / Linkerd 第一阶段不引入
|
|
- ❌ **GitOps (ArgoCD)**: 第一阶段用 helm 命令行 / CI 部署即可
|
|
|
|
### 4.3 第二阶段预留 (不实现,但要预留接口)
|
|
|
|
虽然第一阶段不做,但 Helm chart 设计要**便于第二阶段演进**:
|
|
|
|
- **values.yaml 结构**: 用嵌套结构(`gateway.xxx`, `userservice.xxx`),后续可拆 chart
|
|
- **ConfigMap / Secret 命名**: 不用硬编码 group 名(如 `gateway-config` 而非 `group-a-gateway-config`)
|
|
- **服务发现**: 第一阶段用 K8s 短 DNS;第二阶段如需跨 ns,用全限定名(`userservice.topfans-group-a.svc.cluster.local`)
|
|
- **数据层多租户**: 已在本文档 §11.4.4 记录设计草图,第二阶段另起文档详细设计
|
|
|
|
---
|
|
|
|
## 五、目录结构 (Phase 1)
|
|
|
|
```
|
|
k8s/ (新目录,根目录同级)
|
|
├── README.md 运维使用手册
|
|
├── helm/
|
|
│ └── topfans/ 单个 chart,部署整个 topfans
|
|
│ ├── Chart.yaml
|
|
│ ├── values.yaml 默认值(占位 + 文档)
|
|
│ ├── values-prod.example.yaml 生产覆盖模板(空值,真值不入 git)
|
|
│ └── templates/
|
|
│ ├── _helpers.tpl
|
|
│ ├── external-db/
|
|
│ │ ├── postgres-external.yaml ExternalName → RDS
|
|
│ │ └── redis-external.yaml ExternalName → ElastiCache
|
|
│ ├── gateway/ deployment + service + configmap
|
|
│ ├── userservice/ deployment + service + configmap
|
|
│ ├── assetservice/ 同上
|
|
│ ├── galleryservice/ 同上
|
|
│ ├── socialservice/ 同上
|
|
│ ├── activityservice/ 同上
|
|
│ ├── starbookservice/ 同上
|
|
│ ├── taskservice/ 同上
|
|
│ ├── aichatservice/ 同上
|
|
│ ├── lasercompositor/ 同上
|
|
│ ├── hpa/ 各类 HorizontalPodAutoscaler
|
|
│ ├── ingress.yaml 集群级 Ingress → gateway:8080
|
|
│ ├── secrets/ 各类 Secret (DB/OSS/JWT/AI keys)
|
|
│ └── future-services/ (admin/review/ai-*) .gitkeep 占位
|
|
│
|
|
└── (仓库根目录 .gitignore) 忽略 values-prod.yaml 等含真值文件
|
|
```
|
|
|
|
**原则**:
|
|
|
|
- `docker/` 目录保留,继续支撑本地开发 (`docker-compose.local.yml`)
|
|
- `k8s/` 是新增的部署维度,与 `docker/` 并存
|
|
- **单个 Helm chart**(`topfans/`)覆盖整个 Phase 1,第二阶段再考虑拆 chart
|
|
- HPA / Ingress / Secret 单独目录管理,各 service 子目录只放 deployment + service + configmap(关注点分离)
|
|
- 未来四个新服务**不在本次实现**,只留 `.gitkeep` 占位
|
|
- 镜像构建继续走 `docker/Dockerfile.services` (多阶段),不重复造轮子
|
|
- `values-prod.yaml` 之类的真值文件不进 git,`values-prod.example.yaml` 入 git(配合仓库根目录 `.gitignore`)
|
|
|
|
---
|
|
|
|
## 六、迁移计划 (Phase 1)
|
|
|
|
### Step 1: K8s 集群与基础设施
|
|
|
|
- [ ] 选型:阿里云 ACK 测试集群(推荐,或自建 k3s 备选)
|
|
- [ ] 部署 nginx-ingress + cert-manager(HTTPS)
|
|
- [ ] 创建 `topfans` namespace
|
|
- [ ] 创建阿里云 RDS(PostgreSQL)与 ElastiCache(Redis)实例
|
|
- [ ] 用阿里云 DTS 把 VM 上的 Postgres 数据迁到 RDS
|
|
- [ ] 验证: RDS / Redis 端到端连通
|
|
|
|
### Step 2: 编写 Helm Chart
|
|
|
|
- [ ] 初始化 `k8s/helm/topfans/` chart 结构
|
|
- [ ] 编写 9 个数据服务 + gateway 的 Deployment / Service / ConfigMap
|
|
- [ ] 编写 ExternalName Service 占位指向 RDS / Redis
|
|
- [ ] 编写 Secret 模板(DB / OSS / JWT / AI API keys)
|
|
- [ ] 编写 Ingress(指向 gateway:8080)
|
|
- [ ] 编写 HPA(gateway / aichatservice / lasercompositor)
|
|
- [ ] 准备 `values-prod.example.yaml`(空值)
|
|
- [ ] 写 README:部署 / 升级 / 回滚步骤
|
|
|
|
### Step 3: CI 镜像构建
|
|
|
|
- [ ] 选 1: 阿里云 ACR 仓库(推荐)— 写 `.github/workflows/` 或 `gitlab-ci.yml` 构建并推送
|
|
- [ ] 选 2: 保留 `deploy.sh`,改目标为推 ACR 而非 SSH 到服务器
|
|
- [ ] 选其一,写明 deploy.sh 的去留(参见 §10.1)
|
|
|
|
### Step 4: 流量切换 (单 namespace,所有流量一次性切)
|
|
|
|
- [ ] 把现有 `.env.prod` 的所有密钥搬到 K8s Secret(真值不入 git,CI 注入)
|
|
- [ ] 准备 DNS 切换预案 (`api.example.com` 先解析到 K8s,旧 VM 保留回滚)
|
|
- [ ] **PostgreSQL 序列同步 (硬性 blocker)** — 按 CLAUDE.md 规范,切换流量**前**以 K8s Job 形式跑 `setval('xxx_id_seq', (SELECT MAX(id) FROM xxx))` 对所有 BIGSERIAL 表,验证 `pg_sequences` 全部 `is_healthy=true`(详见 §10.3)
|
|
- [ ] 切换 DNS → K8s
|
|
- [ ] 观察 1~2 周
|
|
|
|
### Step 5: 全量切换 + VM 退役
|
|
|
|
- [ ] 验证 K8s 部署稳定后,停 VM 上的 `docker-compose`
|
|
- [ ] 释放 VM 资源
|
|
|
|
### Step 6: 后续优化 (Phase 1 内可做)
|
|
|
|
- [ ] 接 Prometheus + Grafana(可选)
|
|
- [ ] 接 Loki 日志收集(可选)
|
|
- [ ] 接阿里云监控告警(可选)
|
|
- [ ] 验证 HPA 触发条件是否符合预期
|
|
|
|
### Step 7: 第二阶段 (未来,不在本任务)
|
|
|
|
- [ ] 拆 namespace (per-group)
|
|
- [ ] 加 ResourceQuota
|
|
- [ ] 应用层多租户改造 (group_id 字段)
|
|
- [ ] 接 ArgoCD / Flux 做 GitOps
|
|
- [ ] 接 External Secrets Operator
|
|
- [ ] 多集群 / 多 region 容灾
|
|
|
|
---
|
|
|
|
## 七、风险与待定项 (Phase 1)
|
|
|
|
| 风险/待定 | 影响 | 缓解 |
|
|
|---|---|---|
|
|
| VM 上的 docker-compose 在生产跑,切换期间需双轨 | 资源消耗(临时) | 切完即停 VM |
|
|
| Postgres 走外部 RDS,数据迁移 | 数据一致性 | 用阿里云 DTS 做实时同步,切换时切流量 |
|
|
| **PostgreSQL 序列同步** (CLAUDE.md 强制规范) | duplicate key 错误 | 切换流量**前**以 Job 形式跑 `setval(...)`,验证 `pg_sequences` 全部 `is_healthy=true` |
|
|
| 镜像构建走 CI 还是保留 deploy.sh | 流程变更 | 推荐 ACR + CI,过渡期可保留 deploy.sh |
|
|
| 新服务具体技术栈未定 | 模板不完整 | `.gitkeep` 占位,后续按需填 |
|
|
| 集群选型:ACK / TKE / 自建 k3s | 成本/可控性 | 建议先 ACK 测试,稳定后定型 |
|
|
| HPA 是否启用 (按 CPU / 自定义指标) | 资源效率 | Phase 1 启用基础 HPA,稳定后按需调 |
|
|
| 多集群 / 多 region 容灾 | 灾备 | Phase 1 不做,Phase 2/3 规划 |
|
|
|
|
---
|
|
|
|
## 八、用户已确认的关键决策 (Phase 1)
|
|
|
|
1. ✅ **分两阶段**:第一阶段合并部署降本,第二阶段按组隔离(第二阶段不在本任务)
|
|
2. ✅ **配置格式: Helm Chart**
|
|
3. ✅ **数据库: 外部托管 (RDS/ElastiCache)**
|
|
4. ✅ **Gateway: 单 namespace 内单 gateway**
|
|
5. ✅ **新服务 (admin / review / ai-image-gen / ai-chat)**: 只在 K8s 留占位,本阶段不实现
|
|
6. ✅ **保留 `docker/` 目录,继续支撑本地开发**
|
|
|
|
---
|
|
|
|
## 九、不在本次范围 (Phase 1)
|
|
|
|
- 新服务 (admin / review / ai-image-gen / ai-chat) 的实现
|
|
- 应用层多租户改造 (group_id 字段添加与透传)
|
|
- 按明星组的 namespace 拆分(留给第二阶段)
|
|
- ResourceQuota / LimitRange(留给第二阶段)
|
|
- Service Mesh (Istio / Linkerd)
|
|
- 灾备多集群
|
|
|
|
---
|
|
|
|
## 十、Spec Review Refinements (Phase 1 相关)
|
|
|
|
本文档经 spec-document-reviewer 审查通过 (Status: Approved)。原始 review 在两阶段化之前完成,部分细化项已与 Phase 1 范围对齐,新版本不重新跑 review。**Phase 1 范围内需要执行时关注的细化项**:
|
|
|
|
### 10.1 镜像仓库路径决策 (Phase 1 必选)
|
|
|
|
`values.yaml` 中 `image.repository` 必须明确指向一个 registry:
|
|
|
|
- **选项 A (推荐)**: 阿里云 ACR,`image.repository: registry.cn-shanghai.aliyuncs.com/topfans/<service>`,CI 构建推送
|
|
- **选项 B**: 自建 Harbor,`image.repository: harbor.example.com/topfans/<service>`
|
|
|
|
**Phase 1 实施时需选其一**,并写明 deploy.sh 的去留:
|
|
|
|
- 选 A: 改 deploy.sh 为 CI 触发器(或废弃),服务器只拉不构建
|
|
- 选 B: 保留 deploy.sh 构建逻辑,但目标改为推 Harbor 而非本地
|
|
|
|
### 10.2 Secret 落盘策略 (Phase 1 必做)
|
|
|
|
Secret 模板含明文密钥会污染 git。**Phase 1 实施时需**:
|
|
- `values-prod.yaml` 加入 `.gitignore`,只提交 `values-prod.example.yaml`(空值)
|
|
- 真值由 CI/CD 注入,或用 `kubeseal` / `sops` 加密后提交
|
|
- 最低要求: 真值不进 git
|
|
|
|
### 10.3 PostgreSQL 序列同步 (CLAUDE.md 强制规范)
|
|
|
|
CLAUDE.md 明确指出,任何手动 `INSERT` 指定 id 必须同步 `setval(...)`。Phase 1 切换流量**前**必须:
|
|
|
|
1. 用 DTS 把 VM 上的数据迁到 RDS
|
|
2. 切换前以 K8s Job 形式跑 `setval('xxx_id_seq', (SELECT MAX(id) FROM xxx))` 对所有 BIGSERIAL 表
|
|
3. 验证 SQL: `SELECT ... FROM pg_sequences WHERE last_value >= (SELECT MAX(id) FROM ...)` 全部 `is_healthy=true`
|
|
4. 通过后才切流量
|
|
|
|
**该流程是 Phase 1 切换的硬性 blocker**。
|
|
|
|
### 10.4 HEALTHCHECK 修复范围 (Phase 1 不修)
|
|
|
|
K8s 用 Helm chart 里的 `livenessProbe` / `readinessProbe`,**不再依赖 Dockerfile HEALTHCHECK**。原 `docker/Dockerfile.services` 中有端口错配 bug(如 galleryservice 监听 20001 但 HEALTHCHECK 用 21001),**Phase 1 不修**,保持向后兼容,留待单独 issue 处理。
|
|
|
|
### 10.5 不再相关的项 (已 out of Phase 1 范围)
|
|
|
|
以下细化项在原 spec 中存在,但 Phase 1 不实现,移到第十一章作 Phase 2 远期参考:
|
|
|
|
- `group_id` 字段前置条件 → 移到 §11
|
|
- 跨 namespace Dubbo 调用模式 → 移到 §11
|
|
- ResourceQuota / LimitRange 模板 → 移到 §11
|
|
|
|
---
|
|
|
|
## 十一、Phase 2 未来设计 (仅作远期参考,不实现)
|
|
|
|
> **本节是 Phase 2 启动时的设计草图,本任务不实现**。Phase 1 交付且稳定运行后,当头部明星流量增长到需要独立扩展 / 出现吵市占率问题,启动 Phase 2 任务时再细化。
|
|
|
|
### 11.1 触发条件
|
|
|
|
满足以下任一条件时,启动 Phase 2:
|
|
|
|
1. 头部明星的爆款活动导致尾部明星的服务质量明显下降(API 延迟增加、Pod 资源争抢)
|
|
2. 单 Pod 副本数已超过 5 但仍无法满足头部明星流量
|
|
3. 出现合规/数据隔离需求(如某明星有隐私/法规要求)
|
|
4. 头部明星单独付费运营,需要清晰的资源用量账单
|
|
|
|
### 11.2 架构目标
|
|
|
|
把 Phase 1 合并的 `topfans` namespace 拆为:
|
|
|
|
- **`topfans-shared`**: 平台型服务(未来的 admin / review / ai-image-gen / ai-chat)+ ExternalName 占位
|
|
- **`topfans-group-<group-name>`**: 每组 1 个 namespace,包含独立的 gateway + 9 个数据服务 + ResourceQuota
|
|
|
|
### 11.3 目标集群拓扑
|
|
|
|
```
|
|
K8s 集群
|
|
│
|
|
├── namespace: topfans-shared
|
|
│ ├── admin — Deployment (admin 后台管理)
|
|
│ ├── review — Deployment (审核工作流)
|
|
│ ├── ai-image-gen — Deployment (镭射卡图片生成)
|
|
│ ├── ai-chat — Deployment (AI 对话)
|
|
│ ├── postgres-ext — ExternalName → RDS
|
|
│ ├── redis-ext — ExternalName → ElastiCache
|
|
│ └── secrets/ — DB / OSS / AI keys
|
|
│
|
|
├── namespace: topfans-group-a
|
|
│ ├── ResourceQuota + LimitRange
|
|
│ ├── gateway
|
|
│ ├── userservice, assetservice, galleryservice,
|
|
│ │ socialservice, activityservice, starbookservice,
|
|
│ │ taskservice, aichatservice, lasercompositor
|
|
│
|
|
├── namespace: topfans-group-b
|
|
│ └── ... (同样的 9 个数据服务 + gateway)
|
|
│
|
|
└── Ingress (集群级)
|
|
├── group-a.api.example.com → topfans-group-a/gateway
|
|
├── group-b.api.example.com → topfans-group-b/gateway
|
|
└── admin.api.example.com → topfans-shared/admin
|
|
```
|
|
|
|
### 11.4 关键设计点
|
|
|
|
#### 11.4.1 Gateway 拆为每组一份
|
|
|
|
**原因**: Dubbo `tri://` 是 TCP 二进制协议,K8s Ingress / Service 无法做基于 HTTP Host/Path 的智能路由。把 gateway 放在组内,组内服务间用 K8s 短 DNS 名:
|
|
|
|
```yaml
|
|
# 组内 gateway 的环境变量
|
|
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
|
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
|
# ... 其他 7 个数据服务
|
|
```
|
|
|
|
**应用代码零改动** — 与 Phase 1 相同的短 DNS 名。
|
|
|
|
#### 11.4.2 ResourceQuota + LimitRange
|
|
|
|
每组 namespace 加资源限制,防吵市占率:
|
|
|
|
```yaml
|
|
# topfans-group-a/resource-quota.yaml
|
|
apiVersion: v1
|
|
kind: ResourceQuota
|
|
metadata:
|
|
name: group-a-quota
|
|
namespace: topfans-group-a
|
|
spec:
|
|
hard:
|
|
requests.cpu: "10"
|
|
requests.memory: 20Gi
|
|
limits.cpu: "20"
|
|
limits.memory: 40Gi
|
|
pods: "50"
|
|
persistentvolumeclaims: "5"
|
|
```
|
|
|
|
#### 11.4.3 跨 namespace 调用模式 (新服务 ↔ 数据服务)
|
|
|
|
Phase 2 启动后,部署在 `topfans-shared` 的新服务与各组数据服务的交互方式各不相同:
|
|
|
|
**`admin` (共享 DB,不走 API)**:
|
|
- 直接连同一个 PostgreSQL,按 RBAC 权限访问数据
|
|
- **不需要**调用数据服务 API
|
|
- K8s 部署上:`admin` Pod 内的 `DB_HOST=postgres-ext`(同 §4.2.2 的 ExternalName),与现有服务共用同一份 RDS
|
|
|
|
**`review` (被现有后端调用 + 需读数据)**:
|
|
- 现有后端调用 review 提交审核任务(API 入站,不需要跨 ns)
|
|
- review 审核时需要**读取各组**待审内容(如 group-a 的 gallery),需跨 ns 调数据服务
|
|
- 跨 ns Dubbo 调用模式(可选方案):
|
|
|
|
```yaml
|
|
# 方式 1: 静态全限定名(每组一个变量)
|
|
DUBBO_GALLERY_SERVICE_URL_GROUP_A: tri://galleryservice.topfans-group-a.svc.cluster.local:20001
|
|
DUBBO_GALLERY_SERVICE_URL_GROUP_B: tri://galleryservice.topfans-group-b.svc.cluster.local:20001
|
|
|
|
# 方式 2: 用配置中心或服务发现动态获取 (推荐)
|
|
```
|
|
|
|
**`ai-image-gen` / `ai-chat` (被调用,无跨 ns 需求)**:
|
|
- gateway / 前端调用它们时通过 Ingress 进 shared ns,不需要主动调数据服务
|
|
- 可能间接调 userservice(获取用户信息),走短 DNS 名
|
|
|
|
**应用层需支持 group 维度路由**(针对 review)。具体实现属于应用层设计,不在 K8s 范围。
|
|
|
|
#### 11.4.4 应用层多租户改造 (数据层)
|
|
|
|
在所有业务表加 `group_id` 字段,JWT 携带 group_id,Dubbo attachment 透传:
|
|
|
|
| 表 | 加 group_id | 透传方式 |
|
|
|---|---|---|
|
|
| users | ✅ | JWT |
|
|
| galleries / assets / stars | ✅ | Dubbo attachment |
|
|
| 评论/点赞/收藏 | ✅ | 同上 |
|
|
|
|
**这是应用层最大改动**,需另起专门设计文档。Phase 2 启动时再设计。
|
|
|
|
### 11.5 Phase 2 工作量预估 (粗略)
|
|
|
|
| 工作项 | 工作量 |
|
|
|---|---|
|
|
| Helm chart 拆 chart (topfans-shared + topfans-group) | 2~3 天 |
|
|
| 数据迁移 (按 group 拆表 / 加 group_id 列) | 1~2 周(含应用改造) |
|
|
| 应用层 group_id 透传改造 | 1~2 周 |
|
|
| Gateway 拆 N 份 + Ingress Host 路由 | 1 周 |
|
|
| ResourceQuota / LimitRange 模板 | 0.5 天 |
|
|
| 跨 ns Dubbo 路由实现 | 1 周 |
|
|
| 灰度切换 1 组 + 观察 | 1~2 周 |
|
|
| 全量切换 | 1 周 |
|
|
| **总计** | **约 2~3 个月** |
|
|
|
|
### 11.6 Phase 1 → Phase 2 的演进路径
|
|
|
|
Phase 1 的设计已经考虑了 Phase 2 的可演进性:
|
|
|
|
- `values.yaml` 用嵌套结构(`gateway.xxx`, `userservice.xxx`),Phase 2 拆 chart 时直接复用
|
|
- ConfigMap / Secret 命名不带 group 前缀,Phase 2 加前缀时不会冲突
|
|
- 服务发现用 K8s 短 DNS,Phase 2 跨 ns 时换全限定名即可
|
|
- 数据层 schema 暂未加 `group_id`,Phase 2 再做 migration 添加
|
|
|
|
**Phase 1 不会成为 Phase 2 的障碍,只需在 Phase 2 启动时把现有部署重新 helm install 到新 chart 即可。**
|