# 10 · 部署与运维

## 1. 环境矩阵

| 环境 | 用途 | 规模 |
|------|------|------|
| **local** | 开发者本地 | Docker Compose 一键起 |
| **dev** | 集成测试、PR 预览 | 单节点 K8s / docker-compose |
| **staging** | 预发布、压测 | 2 节点 K8s,真实数据缩减版 |
| **prod** | 生产 | 多可用区 K8s + 完整灾备 |

## 2. Docker Compose(本地开发)

```yaml
# docker-compose.yml
version: "3.8"
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: yaoai123
      MYSQL_DATABASE: yaoai
    ports: ["13306:3306"]
    volumes: ["mysql_data:/var/lib/mysql"]

  redis:
    # Redis Stack = Redis + RediSearch(向量)+ RedisJSON + RedisInsight
    image: redis/redis-stack:7.2.0-v10
    command: redis-stack-server --appendonly yes
    ports: ["16379:6379", "18001:8001"]   # 8001 是 RedisInsight 控制台
    volumes: ["redis_data:/data"]

  rabbitmq:
    image: rabbitmq:3.13-management
    ports: ["5672:5672", "15672:15672"]
    volumes: ["rabbit_data:/var/lib/rabbitmq"]

  # 对象存储:统一使用阿里云 OSS / 火山引擎 TOS,不在 docker-compose 内起
  # 本地开发直连开发环境 OSS Bucket(yaoai-dev),通过 AK/SK 读写
  # 未来若需完全离线开发,再按 TODO 加回 MinIO(S3 兼容)
  # 注:Elasticsearch 暂不引入,MVP 用 MySQL FULLTEXT;M8 团队协作/资产库阶段再评估

  api:
    build: .
    depends_on: [mysql, redis, rabbitmq]
    environment:
      SPRING_PROFILES_ACTIVE: local
      MYSQL_HOST: mysql
      REDIS_HOST: redis                 # 同一 Redis Stack 实例同时承载缓存 + 向量
      RABBIT_HOST: rabbitmq
      YAOAI_STORAGE_PROVIDER: oss       # oss | tos(MinIO 作为未来 TODO)
      YAOAI_VECTOR_PROVIDER: redis
      OSS_ENDPOINT: ${OSS_ENDPOINT}     # 生产:oss-cn-hangzhou.aliyuncs.com
      OSS_ACCESS_KEY: ${OSS_ACCESS_KEY}
      OSS_SECRET_KEY: ${OSS_SECRET_KEY}
      QWEN_API_KEY: ${QWEN_API_KEY}
    ports: ["18080:8080"]
    command: java -jar app.jar --spring.profiles.active=local,api

  worker:
    build: .
    depends_on: [mysql, redis, rabbitmq]
    environment: (同上)
    command: java -jar app.jar --spring.profiles.active=local,worker
    deploy:
      replicas: 2

volumes:
  mysql_data: {}
  redis_data: {}
  rabbit_data: {}
```

## 3. Dockerfile(多阶段构建)

```dockerfile
# 构建阶段
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /build
COPY . .
RUN ./mvnw -B -DskipTests package -pl yaoai-bootstrap -am

# 运行阶段
FROM eclipse-temurin:17-jre-alpine
# 安装 FFmpeg(Assemble Worker 需要)
RUN apk add --no-cache ffmpeg
WORKDIR /app
COPY --from=builder /build/yaoai-bootstrap/target/yaoai-bootstrap-*.jar app.jar
COPY skills/ ./skills/
EXPOSE 8080
ENTRYPOINT ["java","-XX:+UseContainerSupport","-jar","app.jar"]
```

## 4. Kubernetes 部署(生产)

### 4.1 部署单元

```
Deployment: yaoai-api          replicas=3  (HPA by CPU 70%)
Deployment: yaoai-worker-text  replicas=2  (HPA by queue depth)
Deployment: yaoai-worker-image replicas=3  (HPA by queue depth)
Deployment: yaoai-worker-video replicas=2  (HPA by queue depth, GPU)
Deployment: yaoai-worker-voice replicas=2
Deployment: yaoai-worker-assemble replicas=1 (FFmpeg,CPU 密集)
CronJob:   yaoai-scheduler    (定时任务,单实例)

StatefulSet: mysql              (或用云 RDS)
StatefulSet: redis-stack        (或云 Tair / Redis Enterprise 带 RediSearch 模块)
StatefulSet: rabbitmq 集群
对象存储:  阿里云 OSS / 火山引擎 TOS(托管,不在 K8s 内)
Ingress:   yaoai.com / api.yaoai.com
```

### 4.2 HPA 按队列深度扩缩

```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: yaoai-worker-video
spec:
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: External
      external:
        metric:
          name: rabbitmq_queue_depth
          selector:
            matchLabels:
              queue: video.generate
        target:
          type: AverageValue
          averageValue: "5"  # 每 5 个任务扩 1 个 Pod
```

### 4.3 Secret 管理

- 用 ExternalSecrets + HashiCorp Vault 或 AWS Secrets Manager
- API Key、DB 密码、Sa-Token `token-name`/加密盐绝不进 Git

## 5. CI/CD

```
[ Push to main ]
      │
      ▼
  GitHub Actions
      │
      ├─ mvn test(单元 + 集成 with Testcontainers)
      ├─ ESLint / SonarQube 扫描
      ├─ Docker buildx → push to registry
      ├─ helm upgrade yaoai-staging
      ├─ 冒烟测试(Playwright)
      │
      ▼
  [ Manual Approve ]
      │
      ▼
  helm upgrade yaoai-prod (蓝绿 / 滚动)
```

## 6. 数据迁移

- **Flyway** 管理 schema,强制版本化
- 每次上线先跑 `V{n}__description.sql` 再发 app(新字段 + 默认值兼容旧代码)
- 大表 DDL 用 gh-ost / pt-online-schema-change 避免锁表
- 回滚方案:每个版本都有 `Undo_V{n}.sql`(可选)

## 7. 监控与告警

### 7.1 关键指标

| 类别 | 指标 | 告警阈值 |
|------|------|----------|
| API | p99 延迟 | > 2s |
| API | 5xx 率 | > 1% |
| MQ | 队列积压 | video > 50 / image > 100 |
| Worker | 失败率 | > 5% / 10min |
| DB | 慢查询 | > 1s 超过 10 条 /分钟 |
| AI | Provider 错误率 | > 10% |
| 业务 | 余额扣减异常 | 任何负数 |
| 业务 | 任务 stuck | processing 超过 30min |

### 7.2 告警通道

- 即时: 钉钉/企业微信 机器人
- 持续: PagerDuty / Opsgenie
- 日报: 每日 09:00 推送昨日核心指标

## 8. 备份与灾备

| 数据 | 策略 |
|------|------|
| MySQL | 每日全量 + binlog 增量,异地冷存 30 天 |
| Redis Stack(缓存 + 向量) | AOF 持久化 + 每日 RDB 快照;向量索引实例与缓存实例分开部署,向量实例优先保证持久化 |
| RabbitMQ | 队列持久化 + 定期导出定义 |
| OSS / TOS | 版本化 Bucket + 跨区域复制 + 生命周期归档 |
| 配置 | Git 版本化,不可变 |

RTO < 4h,RPO < 15min。

## 9. 安全基线

- [ ] HTTPS 全站(Let's Encrypt)
- [ ] WAF(Aliyun / Cloudflare)
- [ ] SQL 注入防护(参数化查询,禁 `@Query` 字符串拼接)
- [ ] XSS 防护(DTO 输出 escape)
- [ ] CSRF(Sa-Token 前后端分离通过请求头传 Token,天然免疫)
- [ ] 依赖扫描(Dependabot + Snyk)
- [ ] 敏感日志脱敏(统一 `LogRedactor`)
- [ ] API Key 加密存储(Jasypt + 主密钥放 KMS)
- [ ] 定期渗透测试(半年一次)
- [ ] 漏洞奖励计划(产品稳定后)

## 10. 运营手册(Runbook)片段

### 10.1 视频生成任务卡住

1. `kubectl logs -l app=worker-video --tail=200`
2. 查 `tasks where status='PROCESSING' and heartbeat_at < NOW() - INTERVAL 5 MINUTE`
3. 查第三方 Provider 状态页
4. 临时措施: 手动 `markDead` → 重入队
5. 根因: 如果是 Provider 故障,切 fallback provider

### 10.2 余额异常

1. 查 `user_balances` 中 `balance` 或 `frozen_amount` 为负的行
2. 对照 `balance_transactions` 重算
3. 临时措施: 冻结账户,人工补偿
4. 根因: 定位是否有竞态(没走乐观锁)

### 10.3 Worker OOM

1. `kubectl top pod`
2. 检查是否在单任务下载 1GB+ 视频到堆
3. 修复: 流式下载直接写 OSS,不全量加载
