Completed: - Add 6 core database documents (docs/01-平台基础层/07-数据库/) Architecture overview, migration history, environment comparison, tech debt tracking, seed data management, PostgreSQL extensions - Restructure deployment docs: archive 20 legacy files to _archive-2025/ - Create unified daily operations manual (01-日常更新操作手册.md) - Add pending deployment change tracker (03-待部署变更清单.md) - Update database development standard to v3.0 (three iron rules) - Fix Prisma schema type drift: align @db.* annotations with actual DB IIT: UUID/Timestamptz(6), SSA: Timestamp(6)/VarChar(20/50/100) - Add migration: 20260227_align_schema_with_db_types (idempotent ALTER) - Add Cursor Rule for auto-reminding deployment change documentation - Update system status guide v6.4 with deployment and DB doc references - Add architecture consultation docs (Prisma guide, SAE deployment guide) Technical details: - Manual migration due to shadow DB limitation (TD-001 in tech debt) - Deployment docs reduced from 20+ scattered files to 3 core documents - Cursor Rule triggers on schema.prisma, package.json, Dockerfile changes Made-with: Cursor
1029 lines
25 KiB
Markdown
1029 lines
25 KiB
Markdown
# 关键配置补充说明 - 部署文档勘误与增强
|
||
|
||
> **文档版本:** v1.0
|
||
> **创建日期:** 2025-12-14
|
||
> **文档性质:** 对5个独立部署文档的关键补充
|
||
> **优先级:** ⭐⭐⭐⭐⭐ 必读(包含3个P0/P1致命问题)
|
||
|
||
---
|
||
|
||
## 📋 文档说明
|
||
|
||
本文档基于对5个独立部署文档的深度审查,补充了**3个致命问题**和**若干最佳实践**。这些内容在原文档中遗漏或未充分强调,但对生产环境部署至关重要。
|
||
|
||
**请在部署前务必阅读本文档!**
|
||
|
||
---
|
||
|
||
## 🚨 致命问题修正(P0/P1)
|
||
|
||
### 1. SAE孤岛效应 - NAT网关配置 ⭐⭐⭐⭐⭐
|
||
|
||
**问题严重度:P0(致命)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
SAE部署在VPC内,默认没有公网出口!
|
||
|
||
影响场景:
|
||
❌ 后端调用 DeepSeek/OpenAI API → 超时
|
||
❌ Python下载公网PDF → 超时
|
||
❌ npm install公网依赖(构建时)→ 失败
|
||
|
||
结果:所有AI功能不可用,系统基本瘫痪!
|
||
```
|
||
|
||
#### 解决方案
|
||
|
||
**方案A:NAT网关(推荐,生产环境)**
|
||
|
||
```bash
|
||
# 步骤1:创建NAT网关
|
||
阿里云控制台 > VPC > NAT网关 > 创建NAT网关
|
||
├─ VPC:选择SAE所在的VPC
|
||
├─ 交换机:选择SAE所在的交换机
|
||
├─ 规格:小型(够用)
|
||
└─ 计费方式:按使用量计费
|
||
|
||
# 步骤2:创建并绑定EIP
|
||
NAT网关详情 > 弹性公网IP > 绑定弹性公网IP
|
||
├─ 创建新EIP或选择已有EIP
|
||
├─ 带宽:按使用流量(成本低)
|
||
└─ 确认绑定
|
||
|
||
# 步骤3:配置SNAT条目
|
||
NAT网关详情 > SNAT管理 > 创建SNAT条目
|
||
├─ 选择交换机:SAE所在的交换机(如 vsw-xxxxx)
|
||
├─ 选择公网IP:刚才绑定的EIP
|
||
└─ 确认创建
|
||
|
||
成本:NAT网关¥60/月 + EIP流量费¥30-50/月 = ¥90-110/月
|
||
```
|
||
|
||
**方案B:SAE绑定公网IP(部分地域支持)**
|
||
|
||
```bash
|
||
SAE控制台 > 应用配置 > 网络配置
|
||
└─ 查看是否有"公网访问"或"绑定EIP"选项
|
||
|
||
⚠️ 注意:
|
||
- 并非所有地域都支持
|
||
- 优先使用方案A(更稳定)
|
||
```
|
||
|
||
#### 验证NAT网关是否生效
|
||
|
||
```bash
|
||
# 方法1:在SAE应用日志中查看
|
||
# 应用启动后,查看是否有DeepSeek API调用成功的日志
|
||
|
||
# 方法2:通过云助手执行命令(SAE控制台 > 实例列表 > 登录实例)
|
||
curl -I https://api.deepseek.com
|
||
# 应该返回 200 OK,而不是超时
|
||
|
||
# 方法3:测试Python下载公网PDF
|
||
curl -I https://arxiv.org/pdf/2301.00001.pdf
|
||
# 应该返回 200 OK
|
||
```
|
||
|
||
#### 更新的文档
|
||
|
||
- ✅ `00-部署架构总览.md`:物理架构图已增加NAT网关
|
||
- ✅ `00-部署架构总览.md`:成本估算已更新(¥1,200-1,250/月)
|
||
- ⚠️ `05-Node.js后端-SAE容器部署指南.md`:需要在"SAE应用配置"章节增加网络配置说明
|
||
- ⚠️ `04-Python微服务-SAE容器部署指南.md`:同上
|
||
|
||
---
|
||
|
||
### 2. 部署依赖死锁 - Dify API Key鸡生蛋问题 ⭐⭐⭐⭐⭐
|
||
|
||
**问题严重度:P1(严重)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
死锁链:
|
||
1. 后端启动需要 DIFY_API_KEY
|
||
2. DIFY_API_KEY 需要 Dify 启动并人工登录后才能生成
|
||
3. 后端如果健康检查失败,会无限重启
|
||
|
||
结果:后端无法启动,或启动后PKB模块不可用
|
||
```
|
||
|
||
#### 解决方案(分阶段部署)
|
||
|
||
**阶段1:首次部署后端(临时配置)**
|
||
|
||
```bash
|
||
# SAE环境变量配置
|
||
DIFY_API_KEY=temp_placeholder_will_update_later
|
||
|
||
# ⚠️ 重要:后端代码需要容错处理
|
||
# backend/src/common/rag/DifyClient.ts
|
||
constructor() {
|
||
const apiKey = process.env.DIFY_API_KEY
|
||
|
||
if (!apiKey || apiKey === 'temp' || apiKey.startsWith('temp_')) {
|
||
console.warn('⚠️ Dify API Key未配置,PKB模块将不可用')
|
||
this.enabled = false
|
||
return
|
||
}
|
||
|
||
this.client = new DifySDK(apiKey)
|
||
this.enabled = true
|
||
}
|
||
|
||
// 所有Dify调用前检查
|
||
async createDataset(name: string) {
|
||
if (!this.enabled) {
|
||
throw new Error('Dify服务未配置,请先配置DIFY_API_KEY环境变量')
|
||
}
|
||
// ... 正常逻辑
|
||
}
|
||
```
|
||
|
||
**阶段2:部署Dify并获取真实Key**
|
||
|
||
```bash
|
||
# 1. 部署Dify到ECS(参考 03-Dify-ECS部署完全指南.md)
|
||
cd /opt/dify
|
||
docker-compose up -d
|
||
|
||
# 2. 等待服务启动(约2-3分钟)
|
||
docker-compose logs -f api
|
||
|
||
# 3. 浏览器访问 http://ECS公网IP
|
||
# 4. 注册管理员账号(首次访问会提示)
|
||
# 5. 创建API Key
|
||
# 设置 > API密钥 > 创建密钥 > 复制
|
||
# 格式:app-xxxxxxxxxxxxxxxxxxxxx
|
||
|
||
# 6. 记录API Key(妥善保存)
|
||
DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxx
|
||
```
|
||
|
||
**阶段3:更新后端配置**
|
||
|
||
```bash
|
||
# SAE控制台 > 应用详情 > 环境变量
|
||
# 找到 DIFY_API_KEY,修改为真实值
|
||
DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxx
|
||
|
||
# 保存 > 重启应用
|
||
# SAE会执行滚动重启(零停机)
|
||
```
|
||
|
||
**阶段4:验证PKB功能**
|
||
|
||
```bash
|
||
# 测试知识库创建
|
||
curl -X POST https://your-api.com/api/v1/pkb/knowledge-bases \
|
||
-H "Authorization: Bearer YOUR_USER_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"name":"测试知识库","description":"测试"}'
|
||
|
||
# 应该返回 200 OK,而不是 "Dify服务未配置" 错误
|
||
```
|
||
|
||
#### 更新的文档
|
||
|
||
- ✅ `00-部署架构总览.md`:部署顺序已更新,明确分阶段部署
|
||
- ⚠️ `05-Node.js后端-SAE容器部署指南.md`:需要在"环境变量配置"章节增加临时配置说明
|
||
- ⚠️ `03-Dify-ECS部署完全指南.md`:需要在"首次访问"章节增加API Key生成步骤
|
||
|
||
---
|
||
|
||
### 3. HTTP Client超时配置 - 防止连接泄漏 ⭐⭐⭐⭐
|
||
|
||
**问题严重度:P1(严重)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
Python服务处理PDF/OCR可能需要60-120秒
|
||
如果后端HTTP Client没有设置超时,会导致:
|
||
❌ 连接数堆积
|
||
❌ 后端实例内存耗尽
|
||
❌ 数据库连接池耗尽
|
||
```
|
||
|
||
#### 解决方案
|
||
|
||
**后端HTTP Client配置**
|
||
|
||
```typescript
|
||
// backend/src/common/http/httpClient.ts
|
||
import axios from 'axios'
|
||
|
||
export const pythonServiceClient = axios.create({
|
||
baseURL: process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000',
|
||
timeout: 120000, // ⚠️ 120秒(2分钟)
|
||
timeoutErrorMessage: 'Python微服务响应超时(>2分钟)',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
|
||
// 请求拦截器(可选,用于日志)
|
||
pythonServiceClient.interceptors.request.use(
|
||
(config) => {
|
||
console.log(`[HTTP] 调用Python服务: ${config.method?.toUpperCase()} ${config.url}`)
|
||
return config
|
||
},
|
||
(error) => Promise.reject(error)
|
||
)
|
||
|
||
// 响应拦截器(错误处理)
|
||
pythonServiceClient.interceptors.response.use(
|
||
(response) => response,
|
||
(error) => {
|
||
if (error.code === 'ECONNABORTED') {
|
||
console.error('[HTTP] Python服务超时:', error.message)
|
||
}
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
```
|
||
|
||
**Dify Client配置**
|
||
|
||
```typescript
|
||
// backend/src/common/rag/DifyClient.ts
|
||
import axios from 'axios'
|
||
|
||
const difyHttpClient = axios.create({
|
||
baseURL: process.env.DIFY_API_URL || 'http://localhost/v1',
|
||
timeout: 60000, // ⚠️ 60秒(Dify响应较快)
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.DIFY_API_KEY}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
```
|
||
|
||
#### 超时时间建议
|
||
|
||
| 服务 | 超时时间 | 理由 |
|
||
|------|---------|------|
|
||
| **Python微服务** | 120秒 | PDF解析(Nougat OCR)可能需要60-120秒 |
|
||
| **Dify API** | 60秒 | RAG检索通常<10秒,60秒足够 |
|
||
| **外部LLM API** | 60秒 | DeepSeek/OpenAI流式响应,60秒足够 |
|
||
| **数据库查询** | 30秒 | Prisma默认,复杂查询可能需要10-20秒 |
|
||
|
||
#### 更新的文档
|
||
|
||
- ⚠️ `05-Node.js后端-SAE容器部署指南.md`:需要在"代码准备"章节增加HTTP Client配置
|
||
|
||
---
|
||
|
||
## ⚠️ 重要安全配置
|
||
|
||
### 4. ECS端口安全 - Redis/Weaviate不对外开放 ⭐⭐⭐⭐⭐
|
||
|
||
**问题严重度:P0(致命安全风险)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
Dify的Redis(6379)和Weaviate(8080)如果对公网开放:
|
||
❌ Redis无密码,可被攻击者直接访问
|
||
❌ Weaviate包含敏感的向量数据
|
||
❌ 可能被用于DDoS攻击的跳板
|
||
```
|
||
|
||
#### 正确配置
|
||
|
||
**docker-compose.yaml 端口配置**
|
||
|
||
```yaml
|
||
services:
|
||
# ❌ 错误示例(危险)
|
||
redis:
|
||
ports:
|
||
- "6379:6379" # 对所有网卡开放,包括公网!
|
||
|
||
# ✅ 正确配置
|
||
redis:
|
||
image: redis:6-alpine
|
||
ports:
|
||
- "127.0.0.1:6379:6379" # 只监听 localhost
|
||
restart: always
|
||
volumes:
|
||
- ./volumes/redis/data:/data
|
||
command: redis-server --save 60 1 --loglevel warning
|
||
|
||
# ✅ 正确配置
|
||
weaviate:
|
||
image: semitechnologies/weaviate:1.19.0
|
||
ports:
|
||
- "127.0.0.1:8080:8080" # 只监听 localhost
|
||
restart: always
|
||
environment:
|
||
- QUERY_DEFAULTS_LIMIT=25
|
||
- AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true
|
||
- PERSISTENCE_DATA_PATH=/var/lib/weaviate
|
||
|
||
# ✅ 只有Nginx需要对外(VPC内网)
|
||
nginx:
|
||
image: nginx:latest
|
||
ports:
|
||
- "80:80" # 对VPC内网开放(不是公网)
|
||
restart: always
|
||
volumes:
|
||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||
depends_on:
|
||
- api
|
||
- web
|
||
```
|
||
|
||
#### ECS安全组配置
|
||
|
||
```bash
|
||
# 安全组规则(ECS控制台 > 安全组 > 配置规则)
|
||
|
||
入方向规则:
|
||
├─ 允许 80/TCP 来源:VPC网段(172.16.0.0/12) # Nginx
|
||
├─ 允许 22/TCP 来源:您的办公室IP # SSH管理
|
||
└─ 拒绝 所有 来源:0.0.0.0/0 # 默认拒绝
|
||
|
||
出方向规则:
|
||
└─ 允许 所有 目标:0.0.0.0/0 # 允许访问公网
|
||
```
|
||
|
||
#### 验证安全配置
|
||
|
||
```bash
|
||
# 从公网测试(应该失败)
|
||
telnet ECS公网IP 6379
|
||
# 应该超时或拒绝连接
|
||
|
||
telnet ECS公网IP 8080
|
||
# 应该超时或拒绝连接
|
||
|
||
# 从VPC内测试(应该成功)
|
||
# 在SAE应用中执行
|
||
curl http://172.16.x.x # Dify内网地址
|
||
# 应该返回 Dify 的响应
|
||
```
|
||
|
||
#### 更新的文档
|
||
|
||
- ⚠️ `03-Dify-ECS部署完全指南.md`:需要在"docker-compose.yaml配置"章节强调端口安全
|
||
|
||
---
|
||
|
||
### 5. Nginx client_max_body_size - 支持大文件上传 ⭐⭐⭐⭐
|
||
|
||
**问题严重度:P2(一般)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
医疗PDF可能很大(10-50MB)
|
||
Nginx默认限制:1MB
|
||
结果:用户上传大文件时返回 413 Request Entity Too Large
|
||
```
|
||
|
||
#### 解决方案
|
||
|
||
**前端Nginx配置**
|
||
|
||
```nginx
|
||
# frontend-v2/nginx.conf.template
|
||
http {
|
||
# ⚠️ 新增:支持大文件上传
|
||
client_max_body_size 50M;
|
||
|
||
# ⚠️ 新增:开启gzip(React大体积JS)
|
||
gzip on;
|
||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||
gzip_min_length 1000;
|
||
gzip_comp_level 6;
|
||
|
||
server {
|
||
listen 8080;
|
||
server_name _;
|
||
|
||
# 根目录
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
# API反向代理
|
||
location /api/ {
|
||
proxy_pass http://${BACKEND_SERVICE_HOST}:${BACKEND_SERVICE_PORT}/;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
|
||
# ⚠️ 新增:代理超时配置
|
||
proxy_connect_timeout 120s;
|
||
proxy_send_timeout 120s;
|
||
proxy_read_timeout 120s;
|
||
}
|
||
|
||
# SPA路由
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
|
||
# 健康检查
|
||
location /health {
|
||
access_log off;
|
||
return 200 "healthy\n";
|
||
add_header Content-Type text/plain;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 更新的文档
|
||
|
||
- ⚠️ `06-前端Nginx-SAE容器部署指南.md`:需要在"nginx.conf.template"章节增加配置
|
||
|
||
---
|
||
|
||
### 6. Python Workers限制 - 防止OOM ⭐⭐⭐⭐⭐
|
||
|
||
**问题严重度:P1(严重)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
PyMuPDF/Nougat OCR非常吃内存(单个请求可能占用500MB-1GB)
|
||
SAE配置:2GB内存
|
||
如果Gunicorn workers过多,会导致OOM(Out of Memory)
|
||
```
|
||
|
||
#### 解决方案
|
||
|
||
**Dockerfile配置**
|
||
|
||
```dockerfile
|
||
# extraction_service/Dockerfile
|
||
|
||
# 运行阶段
|
||
FROM python:3.11-slim
|
||
|
||
# ... 其他配置 ...
|
||
|
||
# ⚠️ 关键:限制workers防止OOM
|
||
CMD ["gunicorn", "app.main:app", \
|
||
"--workers", "2", \
|
||
"--worker-class", "uvicorn.workers.UvicornWorker", \
|
||
"--bind", "0.0.0.0:8000", \
|
||
"--timeout", "120", \
|
||
"--max-requests", "100", \
|
||
"--max-requests-jitter", "10", \
|
||
"--access-logfile", "-", \
|
||
"--error-logfile", "-"]
|
||
|
||
# workers=2: 最多2个worker(2GB内存限制)
|
||
# timeout=120: 单个请求最多120秒(OCR可能很慢)
|
||
# max-requests=100: 100个请求后重启worker(防止内存泄漏)
|
||
```
|
||
|
||
**SAE配置**
|
||
|
||
```bash
|
||
# SAE控制台 > 应用配置 > 实例规格
|
||
CPU: 1核
|
||
内存: 2GB # ⚠️ 不要低于2GB
|
||
|
||
# 实例数量
|
||
最小实例数: 1
|
||
最大实例数: 3(根据流量自动扩容)
|
||
```
|
||
|
||
#### Workers数量计算公式
|
||
|
||
```python
|
||
# 经验公式
|
||
workers = (CPU核数 × 2) + 1
|
||
|
||
# 但对于内存密集型应用(如PDF解析)
|
||
workers = min((内存GB / 单worker内存GB), (CPU核数 × 2) + 1)
|
||
|
||
# 示例:SAE 1核2GB
|
||
单worker内存 ≈ 800MB(PyMuPDF + Nougat)
|
||
workers = min(2GB / 0.8GB, 1×2+1) = min(2.5, 3) = 2
|
||
|
||
# 结论:workers=2 是安全值
|
||
```
|
||
|
||
#### 监控OOM
|
||
|
||
```bash
|
||
# SAE控制台 > 监控 > 内存使用率
|
||
# 如果经常达到90%+,说明需要:
|
||
# 1. 减少workers(从2降到1)
|
||
# 2. 增加内存(从2GB升到4GB)
|
||
# 3. 优化代码(减少内存占用)
|
||
```
|
||
|
||
#### 更新的文档
|
||
|
||
- ⚠️ `04-Python微服务-SAE容器部署指南.md`:需要在"Dockerfile"章节强调workers限制
|
||
|
||
---
|
||
|
||
## 📖 开发调试最佳实践
|
||
|
||
### 7. SSH隧道 - 本地直连RDS数据库 ⭐⭐⭐⭐
|
||
|
||
**用途:开发便利性(非必需,但强烈推荐)**
|
||
|
||
#### 场景
|
||
|
||
```
|
||
开发人员需要用Navicat/DBeaver查看RDS数据
|
||
但RDS只允许VPC内网访问
|
||
解决:通过ECS作为跳板机,建立SSH隧道
|
||
```
|
||
|
||
#### 操作步骤
|
||
|
||
**步骤1:确保ECS有SSH访问权限**
|
||
|
||
```bash
|
||
# 本地生成SSH密钥(如果还没有)
|
||
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
|
||
|
||
# 将公钥添加到ECS
|
||
# ECS控制台 > 实例 > 远程连接 > 重置密钥对
|
||
# 或者手动添加到 ~/.ssh/authorized_keys
|
||
```
|
||
|
||
**步骤2:建立SSH隧道**
|
||
|
||
```bash
|
||
# 格式
|
||
ssh -N -L 本地端口:RDS内网地址:RDS端口 root@ECS公网IP -i 密钥文件
|
||
|
||
# 示例
|
||
ssh -N -L 5433:rm-bp1xxxxx.pg.rds.aliyuncs.com:5432 \
|
||
root@120.55.xx.xx \
|
||
-i ~/.ssh/dify-ecs.pem
|
||
|
||
# 参数说明:
|
||
# -N: 不执行远程命令,只建立隧道
|
||
# -L: 本地端口转发
|
||
# 5433: 本地监听端口(避免与本地PostgreSQL 5432冲突)
|
||
# rm-bp1xxxxx...: RDS内网地址
|
||
# 5432: RDS端口
|
||
```
|
||
|
||
**步骤3:Navicat连接**
|
||
|
||
```
|
||
连接类型:PostgreSQL
|
||
主机:localhost
|
||
端口:5433
|
||
用户名:aiclinical_rw
|
||
密码:(RDS密码)
|
||
数据库:ai_clinical_research
|
||
|
||
测试连接 → 成功!
|
||
```
|
||
|
||
**步骤4:后台运行隧道(可选)**
|
||
|
||
```bash
|
||
# 方法1:nohup后台运行
|
||
nohup ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 \
|
||
root@ECS-IP -i key.pem > /dev/null 2>&1 &
|
||
|
||
# 方法2:创建systemd服务(Linux)
|
||
# /etc/systemd/system/rds-tunnel.service
|
||
[Unit]
|
||
Description=SSH Tunnel to RDS
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=your-user
|
||
ExecStart=/usr/bin/ssh -N -L 5433:rm-xxxxx.pg.rds.aliyuncs.com:5432 root@ECS-IP -i /home/your-user/.ssh/key.pem
|
||
Restart=always
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
|
||
# 启动服务
|
||
sudo systemctl start rds-tunnel
|
||
sudo systemctl enable rds-tunnel
|
||
```
|
||
|
||
#### 安全注意事项
|
||
|
||
```
|
||
⚠️ 不要将SSH密钥提交到Git
|
||
⚠️ 定期轮换ECS的SSH密钥
|
||
⚠️ 只在开发环境使用,生产环境通过VPN访问
|
||
```
|
||
|
||
---
|
||
|
||
### 8. 时区统一配置 - 防止日志时间混乱 ⭐⭐⭐⭐⭐
|
||
|
||
**问题严重度:P2(重要)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
不同服务的时区不一致会导致:
|
||
❌ 日志时间对不上(前端14:00,后端06:00)
|
||
❌ pg-boss定时任务在错误时间触发
|
||
❌ 用户看到的时间戳错误
|
||
❌ 排查故障极为痛苦
|
||
```
|
||
|
||
#### 解决方案
|
||
|
||
**所有服务统一使用 `Asia/Shanghai` 时区**
|
||
|
||
```dockerfile
|
||
# backend/Dockerfile - Node.js后端
|
||
FROM node:22-alpine
|
||
RUN apk add --no-cache tzdata
|
||
ENV TZ=Asia/Shanghai
|
||
# ... 其他配置
|
||
|
||
# extraction_service/Dockerfile - Python微服务
|
||
FROM python:3.11-slim
|
||
RUN apt-get update && apt-get install -y tzdata
|
||
ENV TZ=Asia/Shanghai
|
||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||
# ... 其他配置
|
||
|
||
# frontend-v2/Dockerfile - 前端(已配置)
|
||
FROM nginx:1.25-alpine
|
||
RUN apk add --no-cache tzdata
|
||
ENV TZ=Asia/Shanghai
|
||
# ... 其他配置
|
||
```
|
||
|
||
```sql
|
||
-- RDS PostgreSQL 时区配置
|
||
-- RDS控制台 > 参数设置 > timezone
|
||
timezone = Asia/Shanghai
|
||
```
|
||
|
||
**验证时区:**
|
||
```bash
|
||
# 查看容器时区
|
||
docker exec <container-id> date
|
||
# 应该显示:Sat Dec 14 14:30:00 CST 2024
|
||
|
||
# 查看RDS时区
|
||
psql -c "SHOW timezone;"
|
||
# 应该显示:Asia/Shanghai
|
||
```
|
||
|
||
#### 影响范围
|
||
|
||
- ✅ Node.js后端:需要更新Dockerfile
|
||
- ✅ Python微服务:需要更新Dockerfile
|
||
- ✅ 前端Nginx:已正确配置
|
||
- ✅ RDS PostgreSQL:需要修改参数
|
||
|
||
#### 修复步骤
|
||
|
||
```bash
|
||
# 1. 修改Node.js后端Dockerfile
|
||
cd backend
|
||
# 在Dockerfile中添加时区配置(见上方示例)
|
||
|
||
# 2. 修改Python微服务Dockerfile
|
||
cd extraction_service
|
||
# 在Dockerfile中添加时区配置(见上方示例)
|
||
|
||
# 3. 修改RDS时区
|
||
# RDS控制台 > 参数设置 > timezone > Asia/Shanghai
|
||
# 需要重启RDS实例
|
||
|
||
# 4. 验证
|
||
docker exec backend-container date
|
||
docker exec python-container date
|
||
psql -h rds-host -c "SHOW timezone;"
|
||
```
|
||
|
||
---
|
||
|
||
### 9. 镜像拉取策略 - 防止代码不更新 ⭐⭐⭐⭐⭐
|
||
|
||
**问题严重度:P2(重要)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
场景:
|
||
开发者修改代码 → 构建镜像 → 推送到ACR(覆盖v1.0.0)
|
||
→ SAE部署 → 发现代码没更新???
|
||
|
||
原因:
|
||
SAE默认镜像拉取策略可能是 IfNotPresent
|
||
如果本地已有 v1.0.0,不会重新拉取
|
||
```
|
||
|
||
#### 解决方案
|
||
|
||
**方案A:每次部署使用新版本号(强烈推荐)**
|
||
|
||
```bash
|
||
# 使用语义化版本号
|
||
v1.0.0 → v1.0.1 → v1.0.2 ...
|
||
|
||
# 或使用时间戳
|
||
v20251214-1430 → v20251214-1530 ...
|
||
|
||
# 或使用Git SHA
|
||
v-a1b2c3d → v-b2c3d4e ...
|
||
|
||
# 构建示例
|
||
docker build -t backend:v1.0.1 .
|
||
docker tag backend:v1.0.1 registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.1
|
||
docker push registry.cn-hangzhou.aliyuncs.com/aiclinical/backend:v1.0.1
|
||
|
||
# SAE部署时选择新版本号
|
||
```
|
||
|
||
**方案B:配置SAE镜像拉取策略(测试环境)**
|
||
|
||
```bash
|
||
# SAE控制台 > 应用配置 > 镜像设置
|
||
镜像拉取策略:Always
|
||
|
||
# ⚠️ 注意:
|
||
# - 每次重启都会拉取镜像(启动稍慢)
|
||
# - 适合测试环境,不推荐生产环境
|
||
```
|
||
|
||
#### 最佳实践
|
||
|
||
| 环境 | 推荐方案 | 理由 |
|
||
|------|---------|------|
|
||
| **生产环境** | 方案A(版本号管理) | 版本可追溯,稳定 |
|
||
| **测试环境** | 方案B(Always拉取) | 始终最新,方便 |
|
||
| **开发环境** | 方案A | 避免混乱 |
|
||
|
||
**❌ 不要:**
|
||
```bash
|
||
# 始终使用 latest 标签(无法追溯版本)
|
||
docker tag backend:latest ...
|
||
```
|
||
|
||
---
|
||
|
||
### 10. Python服务内存管理 - 防止OOM ⭐⭐⭐⭐
|
||
|
||
**问题严重度:P2(重要)**
|
||
|
||
#### 问题描述
|
||
|
||
```
|
||
Python服务(PyMuPDF/Nougat)内存密集,容易OOM
|
||
❌ 单个PDF OCR可能占用500MB-1GB内存
|
||
❌ 多个并发请求会导致内存溢出
|
||
❌ SAE默认2GB内存可能不够
|
||
```
|
||
|
||
#### 解决方案
|
||
|
||
**规格建议:**
|
||
|
||
| 场景 | CPU | 内存 | Workers | 适用情况 |
|
||
|------|-----|------|---------|---------|
|
||
| **基础版** | 1核 | 2GB | 2 | 简单PDF解析 |
|
||
| **标准版** | 2核 | 4GB | 3 | 包含OCR(Nougat) |
|
||
| **增强版** | 2核 | 8GB | 4 | 大量OCR + 高并发 |
|
||
|
||
**Dockerfile优化(已应用):**
|
||
|
||
```dockerfile
|
||
# extraction_service/Dockerfile
|
||
CMD ["gunicorn", "main:app", \
|
||
"--bind", "0.0.0.0:8000", \
|
||
"--workers", "2", \ # ⚠️ 限制并发
|
||
"--timeout", "120", \ # ⚠️ 2分钟超时
|
||
"--max-requests", "100", \ # ⚠️ 处理100个请求后重启worker
|
||
"--max-requests-jitter", "10"] # ⚠️ 随机抖动,避免同时重启
|
||
```
|
||
|
||
**监控与告警:**
|
||
|
||
```bash
|
||
# SAE控制台 > 监控告警 > 创建告警规则
|
||
指标:内存使用率
|
||
阈值:> 80%
|
||
动作:发送通知 + 自动扩容(可选)
|
||
```
|
||
|
||
#### 如果遇到OOM
|
||
|
||
**方案1:升级内存(推荐)**
|
||
```bash
|
||
# SAE控制台 > 应用配置 > 规格调整
|
||
1核2GB → 2核4GB(增加¥100/月)
|
||
```
|
||
|
||
**方案2:限制并发(临时)**
|
||
```dockerfile
|
||
# 修改Dockerfile
|
||
CMD ["gunicorn", "main:app", \
|
||
"--workers", "1", \ # 降低并发
|
||
"--threads", "2"] # 使用线程而非进程
|
||
```
|
||
|
||
---
|
||
|
||
### 11. OSS签名URL - 安全的文件访问 ⭐⭐⭐⭐
|
||
|
||
**用途:安全最佳实践**
|
||
|
||
#### 问题
|
||
|
||
```
|
||
如果OSS Bucket设置为Public:
|
||
❌ 任何人都可以访问所有文件
|
||
❌ 无法追踪谁访问了哪些文件
|
||
❌ 无法控制访问时效
|
||
```
|
||
|
||
#### 解决方案
|
||
|
||
**OSS Bucket配置**
|
||
|
||
```bash
|
||
# OSS控制台 > Bucket列表 > aiclinical-data-prod
|
||
# 读写权限:私有(Private)
|
||
```
|
||
|
||
**后端生成签名URL**
|
||
|
||
```typescript
|
||
// backend/src/common/storage/OSSAdapter.ts
|
||
import OSS from 'ali-oss'
|
||
|
||
export class OSSAdapter {
|
||
private client: OSS
|
||
|
||
constructor() {
|
||
this.client = new OSS({
|
||
region: process.env.OSS_REGION!,
|
||
accessKeyId: process.env.OSS_ACCESS_KEY_ID!,
|
||
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!,
|
||
bucket: process.env.OSS_BUCKET!
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 生成签名URL(1小时有效期)
|
||
*/
|
||
async getSignedUrl(objectKey: string, expiresIn: number = 3600): Promise<string> {
|
||
try {
|
||
const url = this.client.signatureUrl(objectKey, {
|
||
expires: expiresIn, // 秒,默认1小时
|
||
method: 'GET'
|
||
})
|
||
|
||
return url
|
||
} catch (error) {
|
||
console.error('[OSS] 生成签名URL失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 上传文件
|
||
*/
|
||
async uploadFile(objectKey: string, filePath: string): Promise<string> {
|
||
try {
|
||
const result = await this.client.put(objectKey, filePath)
|
||
console.log(`[OSS] 文件上传成功: ${objectKey}`)
|
||
|
||
// 返回签名URL(不是公开URL)
|
||
return this.getSignedUrl(objectKey)
|
||
} catch (error) {
|
||
console.error('[OSS] 文件上传失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**API返回签名URL**
|
||
|
||
```typescript
|
||
// backend/src/modules/pkb/documentController.ts
|
||
router.get('/documents/:id/download', async (req, res) => {
|
||
const { id } = req.params
|
||
|
||
// 1. 查询文档元数据
|
||
const document = await prisma.document.findUnique({
|
||
where: { id }
|
||
})
|
||
|
||
if (!document) {
|
||
return res.status(404).json({ error: '文档不存在' })
|
||
}
|
||
|
||
// 2. 权限校验(确保用户有权访问)
|
||
if (document.userId !== req.user.id) {
|
||
return res.status(403).json({ error: '无权访问此文档' })
|
||
}
|
||
|
||
// 3. 生成签名URL(1小时有效)
|
||
const ossAdapter = new OSSAdapter()
|
||
const signedUrl = await ossAdapter.getSignedUrl(document.ossKey, 3600)
|
||
|
||
// 4. 返回签名URL
|
||
res.json({
|
||
url: signedUrl,
|
||
expiresIn: 3600,
|
||
filename: document.filename
|
||
})
|
||
})
|
||
```
|
||
|
||
**前端使用签名URL**
|
||
|
||
```typescript
|
||
// frontend-v2/src/api/documents.ts
|
||
export async function downloadDocument(documentId: string) {
|
||
// 1. 调用后端API获取签名URL
|
||
const response = await fetch(`/api/v1/pkb/documents/${documentId}/download`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${getToken()}`
|
||
}
|
||
})
|
||
|
||
const { url, filename } = await response.json()
|
||
|
||
// 2. 使用签名URL下载文件
|
||
const link = document.createElement('a')
|
||
link.href = url // 签名URL,1小时有效
|
||
link.download = filename
|
||
link.click()
|
||
}
|
||
```
|
||
|
||
#### 优势
|
||
|
||
```
|
||
✅ 安全:只有授权用户才能获取签名URL
|
||
✅ 时效:URL自动过期(1小时后失效)
|
||
✅ 审计:可以记录谁访问了哪些文件
|
||
✅ 灵活:可以动态调整过期时间
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 总结
|
||
|
||
### 必须立即修复的问题
|
||
|
||
| # | 问题 | 严重度 | 影响 | 修复时间 |
|
||
|---|------|--------|------|---------|
|
||
| 1 | **NAT网关缺失** | P0 | 所有AI功能不可用 | 15分钟 |
|
||
| 2 | **Dify API Key死锁** | P1 | PKB模块不可用 | 10分钟(分阶段部署) |
|
||
| 3 | **HTTP超时未配置** | P1 | 连接泄漏,系统崩溃 | 5分钟(代码修改) |
|
||
| 4 | **ECS端口对外开放** | P0 | 安全风险,可被攻击 | 5分钟(docker-compose修改) |
|
||
| 5 | **Python Workers过多** | P1 | OOM,服务崩溃 | 2分钟(Dockerfile修改) |
|
||
| 6 | **Nginx文件大小限制** | P2 | 大文件上传失败 | 2分钟(nginx.conf修改) |
|
||
|
||
### 推荐但非必需的优化
|
||
|
||
| # | 优化 | 价值 | 实施时间 |
|
||
|---|------|------|---------|
|
||
| 7 | **SSH隧道** | 开发便利性 | 10分钟 |
|
||
| 8 | **OSS签名URL** | 安全最佳实践 | 30分钟(代码修改) |
|
||
|
||
### 下一步行动
|
||
|
||
```
|
||
☐ 1. 创建NAT网关(必需,15分钟)⭐⭐⭐⭐⭐
|
||
☐ 2. 修改docker-compose.yaml(ECS端口安全,5分钟)⭐⭐⭐⭐⭐
|
||
☐ 3. 修改Dockerfile(Python workers限制,2分钟)⭐⭐⭐⭐
|
||
☐ 4. 修改nginx.conf(文件大小限制,2分钟)⭐⭐⭐⭐
|
||
☐ 5. 修改后端代码(HTTP超时,5分钟)⭐⭐⭐⭐
|
||
☐ 6. 修改后端代码(Dify容错,5分钟)⭐⭐⭐⭐
|
||
☐ 7. 更新部署流程(分阶段部署,文档更新)⭐⭐⭐⭐
|
||
☐ 8. 统一时区配置(必需,15分钟)⭐⭐⭐⭐⭐
|
||
☐ 9. 配置镜像拉取策略(必需,5分钟)⭐⭐⭐⭐⭐
|
||
☐ 10. Python内存管理(必需,10分钟)⭐⭐⭐⭐
|
||
☐ 11. (可选)配置SSH隧道(开发便利,10分钟)
|
||
☐ 12. (可选)实现OSS签名URL(安全,30分钟)
|
||
|
||
总计:必需修复约70分钟,可选优化约40分钟
|
||
```
|
||
|
||
---
|
||
|
||
**文档创建人:** AI助手
|
||
**最后更新:** 2025-12-14
|
||
**版本:** v1.0
|
||
|
||
**核心理念:安全第一、稳定第二、便利第三** ⭐⭐⭐
|
||
|