Files
Profeto/docs/06-deployment.md
T
shangfangjian 4b0d6ee58a chore(P3): baseline 落库下沉 + MatchPredictPanel 拆分 + 多 worker/CSRF 文档
P3-2 baseline 落库从路由下沉到服务层(predict_baseline 内直接落库),
删除路由层 _persist_baseline,三种模式统一 result.prediction_id,对外 JSON 不变。

P3-1 MatchPredictPanel.PredictionPanel 拆为 OutcomePanel/AgentsPanel/ReasoningPanel
三个子组件,本文件保留 PredictModal/PredictProgress/Spinner,对外导出路径不变。

P3-3 docs 加 ⚠️ 多 worker 陷阱红字 + STRICT_SINGLE_WORKER 环境变量(启动期强制拒绝多 worker)。
P3-4 docs 新增「同站部署 vs 跨站 CSRF」节。
2026-09-21 23:29:09 +08:00

312 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 06 · 部署
## 前置要求
- Docker & Docker Compose
- bzzoiro API Key(必填)
- LLM API Key(必填,OpenAI / Deepseek / 兼容接口)
## Docker Compose 部署(推荐)
```bash
# 1. 配置环境变量
cp .env.example .env
# 编辑 .env: 填 LLM_API_KEY / BZZOIRO_KEY
# 2. 启动(自动执行数据库迁移)
docker compose up -d --build
# 3. 验证
curl http://localhost:8000/health
```
`docker-compose.yml` 包含 3 个服务:
| 服务 | 端口 | 说明 |
|---|---|---|
| `postgres` | 5433 | PostgreSQL 16 |
| `api` | 8000 | FastAPI 应用(启动时自动执行 `alembic upgrade head`) |
| `frontend` | 3000 | React 前端(多阶段构建,nginx 服务静态文件) |
数据卷 `pgdata` 持久化数据库,重启不丢数据。
> **注意**: `api` 服务启动时会先执行 `alembic upgrade head` 迁移数据库,再启动 uvicorn。
> 容器内数据库连接自动使用 `postgres` 服务名(通过 compose `environment` 覆盖 `.env` 中的 `DB_HOST`)。
## 生产上线检查清单
公网上线前逐项勾选。第 14 项由 `src/core/security_check.py``APP_ENV=production`
启动时**强制校验**,不满足直接拒绝启动(开发环境仅告警);管理鉴权另有请求期 fail-closed(503)。
- [ ] **1. `APP_ENV=production`** — 安全校验、Cookie `Secure`、管理端点 fail-closed 均以它为总开关
- [ ] **2. `SECRET_KEY` 强随机** — 用 `openssl rand -base64 32` 生成;禁止弱值/短值(弱值黑名单会拒绝启动,含把生成指令原样粘进去的情况)
- [ ] **3. 管理鉴权至少其一**`ADMIN_PASSWORD``ADMIN_API_KEY`(后台改过密码后以数据库哈希优先);两者皆空时管理接口 503
- [ ] **4. 数据库强密码** — 禁止 `football:football` 等示例弱密码(启动校验会拒绝);compose 的 `POSTGRES_PASSWORD` 必填,缺失时容器拒绝启动
- [ ] **5. HTTPS** — 由反代(Nginx/Caddy)终结 TLS;`APP_ENV=production` 下会话 Cookie 自动 `Secure`(且 HttpOnly + SameSite=Lax)
- [ ] **6. 反代信任头**`TRUST_PROXY_HEADERS=True`,且**仅可信反代可达 API**;反代需设置 `X-Forwarded-For`(`$proxy_add_x_forwarded_for`)与 `X-Real-IP`,否则限流/日志按反代 IP 计数
- [ ] **7. 限流前置到网关** — 推荐 Nginx `limit_req`(配置见[安全与限流](#安全与限流));应用内限流与 KeyRing 为**单进程内存实现**,多 worker 各自独立计数会把实际配额放大 N 倍(启动时会打印一次性告警)
- [ ] **8. uvicorn 单 worker** — compose/Dockerfile 默认单 worker,保持即可;需横向扩容时先在网关统一限流,再起多实例(每实例仍单 worker)
> ⚠️ **多 worker 陷阱**:应用内限流(`_RateLimiter`)与 KeyRing 均为**进程内纯内存状态**,多 worker 部署(如 `uvicorn --workers 4`)时各进程**各自独立计数、互不共享**——实际限流配额会被放大 N 倍、KeyRing 限流状态也不同步。
> 若确需多 worker,必须前置 Nginx/网关做**全局限流**(见[安全与限流](#安全与限流)),并设环境变量 `STRICT_SINGLE_WORKER=True`(见下)在启动期强制拒绝多 worker,避免静默配额漂移。
- [ ] **9. 启动后健康检查**`curl /health` 返回 200(存活);`curl /health/ready` 返回 200(就绪,校验数据库连通,不可达时 503)
- [ ] **10. 数据库迁移** — compose/Dockerfile 启动命令已内置 `alembic upgrade head && uvicorn …`,升级镜像重启即自动迁移,无需手动执行
## 本地开发部署
```bash
# 1. 安装依赖
pip install -e ".[dev]"
# 2. 启动 PostgreSQL(单独)
docker run -d --name profeto-pg \
-e POSTGRES_USER=football -e POSTGRES_PASSWORD=football -e POSTGRES_DB=football \
-p 5432:5432 postgres:16-alpine
# 3. 配置 .env
cp .env.example .env
# 4. 建表
alembic upgrade head
# 5. 启动 API
uvicorn src.api.app:app --reload
# 6. 启动前端(另一个终端)
cd frontend && npm install && npm run dev
```
访问:
- API 文档: http://localhost:8000/docs
- 前端界面: http://localhost:5173
## 环境变量
| 变量 | 必需 | 默认值 | 说明 |
|---|---|---|---|
| `APP_ENV` | ❌ | `development` | `production` / `development` |
| `LOG_LEVEL` | ❌ | `INFO` | 日志级别 |
| `API_PORT` | ❌ | `8000` | API 服务端口映射 |
| `FRONTEND_PORT` | ❌ | `3000` | 前端服务端口映射 |
| `POSTGRES_USER` | ✅ | — | PostgreSQL 用户名 |
| `POSTGRES_PASSWORD` | ✅ | — | PostgreSQL 密码 |
| `POSTGRES_DB` | ❌ | `football` | PostgreSQL 数据库名 |
| `POSTGRES_PORT` | ❌ | `5433` | PostgreSQL 端口映射 |
| `DATABASE_URL` | ✅ | — | PostgreSQL 连接 URL(Docker 内会被覆盖) |
| `LLM_PROVIDER` | ❌ | `openai` | 提供商名(仅标记) |
| `LLM_API_KEY` | ✅ | — | API Key |
| `LLM_BASE_URL` | ❌ | `https://api.openai.com/v1` | 接口地址(Ollama/Deepseek 用) |
| `LLM_MODEL` | ❌ | `gpt-4o` | 默认模型 |
| `LLM_TIMEOUT` | ❌ | `60` | 单次调用超时(秒) |
| `LLM_SPECIALIST_MODEL` | ❌ | — | 专家模型(回落 `LLM_MODEL`) |
| `LLM_AGGREGATOR_MODEL` | ❌ | — | 终裁模型(回落 `LLM_MODEL`) |
| `BZZOIRO_KEY` | ✅ | — | bzzoiro 数据源 Key(唯一数据源) |
| `CORS_ORIGINS` | ❌ | `http://localhost:5173,...` | 允许的跨域来源 |
| `STRICT_SINGLE_WORKER` | ❌ | `False` | `True` 时若以多 worker 启动则拒绝(防限流配额漂移) |
| `SECRET_KEY` | ❌ | — | 加密主密钥(生产环境必填) |
| `ADMIN_PASSWORD` | ❌ | — | 管理后台密码(留空=不启用) |
| `ADMIN_API_KEY` | ❌ | — | 机器/脚本调用的 API Key |
## 同站部署 vs 跨站 CSRF
Profeto 管理鉴权使用 **HttpOnly Cookie 会话**(登录后服务端写入),`allow_credentials=True` 的 CORS 配置允许浏览器跨域携带 Cookie——这也引入了 CSRF 面。部署拓扑决定风险等级:
**同站部署(推荐)**: 前端与 API 同域(反代把 `/``/api` 都转发到同一后端,或同源端口)。
- 浏览器视为 **same-origin**,CORS 不触发;`SameSite=Lax` 会话 Cookie 天然阻断跨站请求携带。
- 风险最低。`CORS_ORIGINS` 可设为空或同域来源,仅作兜底。
**跨站部署**: 前端与 API 不同域(如前端 `app.example.com`、API `api.example.com`,或开发时 `localhost:3000``localhost:8000`)。
- 必须把 API 域名列入 `CORS_ORIGINS`,且 `allow_credentials=True` 才能携带 Cookie。
- 此时任何被允许域下的页面都能构造带 Cookie 的请求 → **CSRF 面**:
- 状态变更接口(采集/回测/改密等写操作)要求**管理员 Cookie + 同域**,攻击者无法从第三方站点读取 Cookie,但可构造跨域表单/请求——`SameSite=Lax` 会阻断跨站 POST 表单提交(顶级导航 GET 仍放行),这是当前主要防线。
- `GET /api/v1/admin/*` 只读接口受 `SameSite=Lax` 下顶级导航可能被利用,但攻击者无法读取响应(CORS 不匹配时浏览器拦截)。
- **加固建议**:
1. 反代层加 `Origin`/`Referer` 校验,仅放行 `CORS_ORIGINS` 列表中的来源(即便 FastAPI CORS 已通过,反代校验是多一层纵深)。
2. 写操作要求自定义请求头(如 `X-Requested-With: XMLHttpRequest`),第三方站点无法在无预检下添加自定义头,天然阻断简单跨站 POST。
3. 生产强制 HTTPS(`APP_ENV=production` 下 Cookie 自动 `Secure`),防中间人窃 Cookie。
## LLM 提供商配置示例
### OpenAI
```bash
LLM_API_KEY=sk-xxxx
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o
```
### Deepseek
```bash
LLM_API_KEY=sk-xxxx
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chat
```
### Ollama(本地)
```bash
LLM_API_KEY=ollama
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL=llama3.1
```
### 分档配置(专家用便宜模型)
```bash
LLM_MODEL=gpt-4o
LLM_SPECIALIST_MODEL=gpt-4o-mini
LLM_AGGREGATOR_MODEL=gpt-4o
```
## 数据库迁移
Alembic 管理 schema 变更:
```bash
# 查看当前版本
alembic current
# 升级到最新
alembic upgrade head
# 回退一级
alembic downgrade -1
# 生成新迁移(改 models.py 后)
alembic revision --autogenerate -m "描述"
# 空迁移(手动写 SQL)
alembic revision -m "描述"
```
**Docker Compose 自动迁移**: `api` 容器启动时会自动执行 `alembic upgrade head`,
无需手动运行。本地开发时需手动执行迁移。
已有迁移:
- `0001_initial`: 初始 5 张表
- `0002_agent_outputs`: predictions 加 `mode` + `agent_outputs`
- `0003_injuries`: 增加 injuries 表
- `0004_snapshot_and_constraints`: 增加约束
- `0005_prediction_status_and_stats_provenance`: 增加时间语义
- `0006-0012`: 后续 schema 调整、约束命名对齐、partial unique index 等
## 备份与恢复
```bash
# 备份
docker exec profeto-postgres pg_dump -U football football > backup.sql
# 恢复
cat backup.sql | docker exec -i profeto-postgres psql -U football football
```
## 监控
### 健康检查
| 端点 | 含义 | HTTP 状态码 |
|---|---|---|
| `/health` | 存活检查(liveness) | 始终 200(进程在跑即活) |
| `/health/ready` | 就绪检查(readiness) | DB 可达 200,不可达 **503** |
### 探针配置
#### Docker Compose
`docker-compose.yml` 已为 `api` 服务配置 readiness:
```yaml
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8000/health/ready || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
```
**要点**:必须指向 `/health/ready` 而非 `/health`——后者始终 200,在数据库故障时仍会接收流量,导致请求全部失败。
#### Kubernetes
```yaml
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 15
readinessProbe:
httpGet:
path: /health/ready
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
```
**两探针必须区分**:
- `livenessProbe``/health`:仅在进程死锁/崩溃时重启,避免误杀。
- `readinessProbe``/health/ready`:DB 不可用时停止转发流量,恢复后自动切回。
#### 验证
```bash
# 宿主机直接运行(经本地 8000 端口)
python3 tests/test_health_ready.py
```
### 评估
## 安全与限流
### 内存限流(按进程)
`/api/v1/predict` 与登录防爆破均使用**进程内内存**计数:
| 机制 | 位置 | 局限 |
|------|------|------|
| `/predict` 限流 | `_RateLimiter`(内存) | 每 worker 独立计数,不共享 |
| 登录防爆破 | `_fail_times`(内存) | 同上 |
**多 worker 部署时**(如 `uvicorn --workers 4`),每进程各自计数,实际限额为 `N × 单进程限制`
### 公网部署建议
```
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ Nginx │────▶│ API │
│ │ │ 限流层 │ │ 内存限流 │
└─────────┘ └──────────┘ └──────────┘
```
**推荐配置**:
1. **Nginx 层限流**(第一道防线):
```nginx
limit_req_zone $binary_remote_addr zone=predict:10m rate=10r/m;
location /api/v1/predict {
limit_req zone=predict burst=20 nodelay;
proxy_pass http://api:8000;
}
```
2. **TRUST_PROXY_HEADERS=True** 时必须由可信反代设置 `X-Forwarded-For`:
```nginx
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
```
- 若为 `False`: 仅用 `request.client.host`,忽略 `X-Forwarded-For`,防伪造
- 若为 `True`: 解析 `X-Forwarded-For` 第一个 IP,反代后方可信
3. **生产环境必须配置管理鉴权**:
```bash
APP_ENV=production
REQUIRE_ADMIN_AUTH=True
ADMIN_PASSWORD=your_secure_password
```
未配置时 `/admin` 等管理接口返回 503。
### 参数调优
| 参数 | 默认 | 说明 |
|------|------|------|
| `_predict_limiter.max_requests` | 10 | 每分钟每 IP 最大请求数 |
| `_predict_limiter.window_seconds` | 60 | 滑动窗口时长 |
| `ADMIN_SESSION_TTL_HOURS` | 168 | 管理会话有效期(天) |