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
38 KiB
Python 微服务 SAE 容器部署完全指南
文档版本: v1.1 (修复内网地址和临时文件问题)
创建时间: 2025-12-13
最后修订: 2025-12-13
适用范围: AIclinicalresearch 平台 - Python 微服务(extraction_service)
目标读者: 运维工程师、后端开发工程师
v1.1 更新日志:
- ✅ 修复:内网地址使用 SAE 控制台显示的真实 IP(不猜测域名)
- ✅ 优化:Dockerfile 系统依赖说明(libmupdf-dev 可选)
- ✅ 新增:确保 /tmp 目录可写(大文件临时存储)
- ✅ 完善:功能验证流程和监控指南
📋 文档目录
为什么选择 SAE 容器部署
✅ SAE 容器部署 vs. SAE Python 运行时
| 对比维度 | SAE Python 运行时 | SAE 容器部署 (推荐) |
|---|---|---|
| 系统依赖 | ❌ 无法安装系统库 | ✅ 完全可控 |
| 复杂依赖 | ❌ PyMuPDF/OpenCV 报错 | ✅ 完美支持 |
| 环境一致性 | ⚠️ 云上和本地可能不同 | ✅ 本地跑通 = 云上跑通 |
| Nougat (Torch) | ❌ 版本冲突风险高 | ✅ 轻松支持 |
| 部署方式 | 上传 ZIP 包 | 推送 Docker 镜像 |
| 启动速度 | 快(< 5秒) | 较快(10-20秒) |
| 运维复杂度 | 低 | 中 |
| 推荐度 | ❌ 不推荐 | ✅ 强烈推荐 |
🎯 核心原因
1. 系统级依赖缺失(致命问题)
# 您的代码使用了这些库:
import fitz # PyMuPDF → 依赖 libmupdf.so, libfreetype.so
import cv2 # OpenCV → 依赖 libGL.so.1, libgthread-2.0.so
import polars # Polars → 依赖 libgomp.so
SAE Python 运行时:
❌ 只提供标准 Python 环境
❌ 无法执行 apt-get install
❌ 运行时报错:ImportError: libGL.so.1: cannot open shared object file
SAE 容器部署:
✅ Dockerfile 中自由安装:
RUN apt-get update && apt-get install -y \
libgl1-mesa-glx \
libglib2.0-0 \
libgomp1
2. 环境完全可控
本地开发环境 = Docker 镜像 = SAE 生产环境
- 您在本地 Docker 中跑通了,推到 SAE 就一定能跑通
- 没有"本地好用、云上报错"的问题
3. 扩展性强
未来需求:
├─ 添加 Nougat OCR (需要 PyTorch + GPU 支持)
├─ 添加图像预处理 (需要 OpenCV)
├─ 添加更多文档格式 (需要更多系统库)
└─ 容器部署都能轻松支持
4. 运维统一
您的整体架构:
├─ 前端 Nginx → SAE 容器
├─ 后端 Node.js → SAE 容器
└─ Python 服务 → SAE 容器 ✅ (统一管理)
部署架构图
┌─────────────────────────────────────────────────────────────┐
│ 阿里云架构 │
│ │
│ ┌─────────────┐ ┌─────────────────────────────┐ │
│ │ SAE (后端) │ ←内网→ │ SAE (Python 微服务) │ │
│ │ │ │ │ │
│ │ Node.js │ │ ┌─────────────────────┐ │ │
│ │ Backend │ │ │ Docker 容器: │ │ │
│ │ │ │ │ - FastAPI │ │ │
│ │ │ │ │ - PyMuPDF │ │ │
│ │ │ │ │ - Polars │ │ │
│ └─────────────┘ │ │ - Mammoth │ │ │
│ │ │ └─────────────────────┘ │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ ├──────────────→ RDS PostgreSQL 15 │
│ └──────────────→ OSS (文档存储) │
└─────────────────────────────────────────────────────────────┘
关键点:
- Python 微服务和 Node.js 后端都部署在 SAE 上(同一 VPC)
- 通过内网通信(延时 < 5ms)
- 共享 RDS 和 OSS 资源
前置准备清单
✅ 必需资源
| 资源类型 | 配置建议 | 预估费用 | 用途 |
|---|---|---|---|
| SAE 应用 | 1核2G / 1实例 | ~100元/月 | 运行 Python 服务 |
| 容器镜像仓库 | 阿里云 ACR 个人版 | 免费(5GB) | 存储 Docker 镜像 |
| OSS 存储 | 已有(共用) | 0元(增量) | 文档存储 |
| RDS PostgreSQL | 已有(共用) | 0元 | 数据库 |
✅ 软件准备
# 本地开发机器需要安装
- Docker Desktop
- 阿里云 CLI(可选)
# 不需要在 SAE 上安装任何东西(容器已包含)
✅ 账号与权限
- 阿里云账号(已有)
- 容器镜像仓库访问权限
- SAE 应用创建权限
Python 服务分析
📂 当前服务概览
服务 1: extraction_service(文档提取)
位置: AIclinicalresearch/extraction_service/
用途:
- PKB 模块: 上传文档到 Dify 前,先提取文本
- ASL 模块: 提取 PDF 全文用于深度阅读
- DC 模块: 提取 Excel/CSV 数据
核心文件:
extraction_service/
├── main.py # FastAPI 入口
├── requirements.txt # 依赖列表
├── services/
│ ├── pdf_extractor.py # PDF 提取(调度器)
│ ├── pymupdf_extractor.py # PyMuPDF 实现
│ ├── nougat_extractor.py # Nougat OCR 实现
│ ├── docx_extractor.py # Word 提取
│ └── txt_extractor.py # 纯文本提取
└── operations/
└── fillna_operations.py # 数据清洗(Polars)
关键端点:
POST /extract/pdf # PDF 提取
POST /extract/docx # Word 提取
POST /extract/txt # 文本提取
POST /operations/fillna # 数据清洗
📊 依赖分析
当前 requirements.txt 内容:
fastapi==0.115.5
uvicorn[standard]==0.32.1
python-multipart==0.0.20
PyMuPDF==1.24.14
pdfplumber==0.11.4
nougat-ocr==0.1.17
torch==2.1.0
torchvision==0.16.0
mammoth==1.8.0
python-docx==1.1.2
langdetect==1.0.9
chardet==5.2.0
polars==1.17.1
numpy==1.26.4
依赖大小预估:
| 包名 | 大小 | 用途 | 是否必需 |
|---|---|---|---|
| PyMuPDF | ~50MB | PDF 提取(核心) | ✅ 必需 |
| pdfplumber | ~10MB | PDF 表格提取 | ⚠️ 可选(暂未使用) |
| nougat-ocr | ~300MB | 学术论文 OCR | ⚠️ 阶段性(见下文) |
| torch | ~800MB | Nougat 依赖 | ⚠️ 阶段性 |
| torchvision | ~100MB | Nougat 依赖 | ⚠️ 阶段性 |
| mammoth | ~5MB | Word 提取 | ✅ 必需 |
| python-docx | ~3MB | Word 提取 | ✅ 必需 |
| polars | ~50MB | 数据清洗 | ✅ 必需 |
| numpy | ~20MB | 数值计算 | ✅ 必需 |
| fastapi | ~10MB | Web 框架 | ✅ 必需 |
| uvicorn | ~5MB | ASGI 服务器 | ✅ 必需 |
| 其他 | ~10MB | 辅助库 | ✅ 必需 |
| 总计(含 Nougat) | ~1.4GB | - | - |
| 总计(不含 Nougat) | ~163MB | - | - |
依赖优化策略
🎯 阶段 1:最小化部署(推荐用于首次部署)
目标: 快速上线,验证核心功能
策略:
- ✅ 保留 PyMuPDF(核心 PDF 提取)
- ✅ 保留 Mammoth/python-docx(Word 提取)
- ✅ 保留 Polars(数据清洗)
- ❌ 暂时移除 Nougat(体积大,使用频率低)
优化后的 requirements.txt:
# Web 框架
fastapi==0.115.5
uvicorn[standard]==0.32.1
python-multipart==0.0.20
# 文档提取(核心)
PyMuPDF==1.24.14
mammoth==1.8.0
python-docx==1.1.2
# 数据处理
polars==1.17.1
numpy==1.26.4
# 辅助工具
langdetect==1.0.9
chardet==5.2.0
# 日志和监控
python-json-logger==2.0.7
镜像大小预估: ~500MB(含 Python 基础镜像)
代码修改:
# services/pdf_extractor.py
# 注释掉 Nougat 相关代码
# from .nougat_extractor import extract_pdf_nougat, check_nougat_available
async def extract_pdf(pdf_path: str, filename: str):
"""PDF 提取(阶段1:仅 PyMuPDF)"""
# 检测语言和文档类型
language = detect_language(pdf_path)
is_academic = detect_academic_paper(pdf_path)
# 阶段1:直接使用 PyMuPDF
text = extract_pdf_pymupdf(pdf_path)
# 阶段2:可以加回 Nougat 降级逻辑
# if language == 'english' and is_academic:
# try:
# if check_nougat_available():
# text = extract_pdf_nougat(pdf_path)
# except:
# text = extract_pdf_pymupdf(pdf_path) # 降级
return {
'text': text,
'method': 'pymupdf',
'language': language,
'is_academic': is_academic
}
🎯 阶段 2:完整部署(未来需要时)
时机:
- 当用户反馈英文学术论文提取质量不佳时
- 有足够的 GPU 资源时
策略:
- ✅ 加回 Nougat + Torch
- ✅ 使用 GPU 实例(SAE 目前不支持 GPU,需迁移到 ECS)
完整的 requirements.txt:
# 恢复全部依赖(包括 Nougat)
fastapi==0.115.5
uvicorn[standard]==0.32.1
python-multipart==0.0.20
PyMuPDF==1.24.14
pdfplumber==0.11.4
nougat-ocr==0.1.17
torch==2.1.0
torchvision==0.16.0
mammoth==1.8.0
python-docx==1.1.2
langdetect==1.0.9
chardet==5.2.0
polars==1.17.1
numpy==1.26.4
镜像大小预估: ~2GB
构建 Docker 镜像
步骤 1:创建优化的 Dockerfile
在 extraction_service/ 目录下创建 Dockerfile:
# ========================================
# 多阶段构建:减小镜像体积
# ========================================
# 阶段 1: 构建阶段(安装依赖)
FROM python:3.11-slim as builder
# 设置工作目录
WORKDIR /app
# 安装系统依赖(构建时需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
make \
libffi-dev \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
# 安装 Python 依赖到虚拟环境
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# ========================================
# 阶段 2: 运行阶段(最小化镜像)
# ========================================
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 安装运行时依赖(系统级库 + 时区数据)
RUN apt-get update && apt-get install -y --no-install-recommends \
# PyMuPDF 依赖
# 注:libmupdf-dev 通常用于编译,pip 安装的 PyMuPDF wheel 包已自带动态库
# 保留它作为保险,如需瘦身可尝试移除后验证
libmupdf-dev \
libfreetype6 \
libjpeg62-turbo \
libopenjp2-7 \
# Polars 依赖
libgomp1 \
# 其他工具
curl \
# 时区数据
tzdata \
&& rm -rf /var/lib/apt/lists/*
# ⚠️ 统一时区:Asia/Shanghai
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 确保临时目录可写(大文件上传时需要)
RUN mkdir -p /tmp && chmod 1777 /tmp
# 从构建阶段复制虚拟环境
COPY --from=builder /opt/venv /opt/venv
# 复制应用代码
COPY . .
# 设置环境变量
ENV PATH="/opt/venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PORT=8000
# 暴露端口
EXPOSE 8000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
步骤 2:创建 .dockerignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
# 测试和文档
tests/
test_files/
*.md
README.md
# Git
.git/
.gitignore
# 日志
*.log
# 临时文件
tmp/
temp/
步骤 3:本地构建镜像
# 进入 extraction_service 目录
cd d:\MyCursor\AIclinicalresearch\extraction_service
# 构建镜像(本地测试)
docker build -t extraction-service:latest .
# 查看镜像大小
docker images extraction-service
步骤 4:本地测试镜像
# 启动容器(本地测试)
docker run -d \
--name extraction-test \
-p 8000:8000 \
-e DATABASE_URL="postgresql://user:pass@host:5432/dbname" \
extraction-service:latest
# 查看日志
docker logs -f extraction-test
# 测试健康检查
curl http://localhost:8000/health
# 测试 PDF 提取
curl -X POST \
-F "file=@test.pdf" \
http://localhost:8000/extract/pdf
# 停止并删除测试容器
docker stop extraction-test
docker rm extraction-test
步骤 5:推送到阿里云容器镜像仓库
5.1 创建镜像仓库(首次部署)
-
登录阿里云控制台 → 容器镜像服务 ACR
-
创建个人实例(免费版):
实例名称: extraction-service 地域: 华东1(杭州) -
创建命名空间:
命名空间: clinical-research -
创建镜像仓库:
仓库名称: extraction-service 代码源: 本地仓库
5.2 推送镜像
# 1. 登录阿里云容器镜像服务
# 获取登录命令:阿里云控制台 → 容器镜像服务 → 访问凭证 → 设置Registry登录密码
docker login --username=<your-username> registry.cn-beijing.aliyuncs.com
# 2. 给镜像打标签
docker tag extraction-service:latest \
registry.cn-beijing.aliyuncs.com/clinical-research/extraction-service:v1.0
# 3. 推送到阿里云
docker push registry.cn-beijing.aliyuncs.com/clinical-research/extraction-service:v1.0
# 4. 推送 latest 标签(便于后续更新)
docker tag extraction-service:latest \
registry.cn-beijing.aliyuncs.com/clinical-research/extraction-service:latest
docker push registry.cn-beijing.aliyuncs.com/clinical-research/extraction-service:latest
部署到 SAE
步骤 1:创建 SAE 应用
-
登录阿里云控制台 → Serverless 应用引擎 SAE
-
创建应用:
应用名称: extraction-service 命名空间: 选择后端所在的命名空间(同 VPC) 部署方式: 镜像 -
镜像配置:
镜像地址: registry.cn-beijing.aliyuncs.com/clinical-research/extraction-service:latest 镜像版本: latest 镜像拉取策略: Always(每次部署都拉取最新镜像) -
规格配置:
CPU: 1核 内存: 2GB 实例数: 1(初始) 弹性扩缩容: - 最小实例数: 1 - 最大实例数: 3 - CPU 触发阈值: 70% -
网络配置:
专有网络 VPC: 选择后端所在的 VPC vSwitch: 选择后端所在的交换机 安全组: 允许 VPC 内访问
步骤 2:配置环境变量
在 SAE 应用配置中添加以下环境变量:
# ========= 数据库配置 =========
DATABASE_URL=postgresql://user:password@rm-xxxx.pg.rds.aliyuncs.com:5432/clinical_research
# ========= 存储配置 =========
OSS_ENDPOINT=oss-cn-hangzhou-internal.aliyuncs.com
OSS_BUCKET=your-bucket-name
OSS_ACCESS_KEY_ID=<your-id>
OSS_ACCESS_KEY_SECRET=<your-secret>
# ========= 服务配置 =========
SERVICE_NAME=extraction-service
SERVICE_VERSION=v1.0
LOG_LEVEL=INFO
# ========= 性能配置 =========
WORKERS=2
TIMEOUT=300
MAX_FILE_SIZE=52428800
# ========= 时区 =========
TZ=Asia/Shanghai
步骤 3:配置健康检查
健康检查路径: /health
健康检查端口: 8000
健康检查协议: HTTP
初始延迟: 30秒
检查间隔: 10秒
超时时间: 5秒
健康阈值: 2次
不健康阈值: 3次
步骤 4:配置日志
日志目录: /app/logs
日志文件: extraction-service.log
日志级别: INFO
日志保留天数: 7天
步骤 5:配置 SLB(可选,如果需要公网访问)
# 通常 Python 微服务只需要内网访问(被后端调用)
# 如果需要公网访问(如:调试、第三方集成):
负载均衡类型: 公网
监听端口: 80
后端端口: 8000
健康检查: 启用
步骤 6:部署应用
-
点击"部署应用"
-
等待部署完成(约 2-3 分钟)
-
查看部署日志:
[INFO] Pulling image... [INFO] Image pulled successfully [INFO] Starting container... [INFO] Container started successfully [INFO] Health check passed [INFO] Application is running
测试与验证
步骤 1:获取内网地址(关键步骤)
⚠️ 重要:SAE 实例间是跨主机的,必须使用 SAE 提供的内网地址
获取真实内网地址的正确方法:
-
登录 SAE 控制台 → 应用列表 → 点击 extraction-service 应用
-
在应用详情页,找到"应用访问配置"或"VPC 内网访问"部分
-
查看并复制"内网访问地址",通常是以下格式之一:
# 格式 1: 内网 IP + 端口(⭐⭐⭐⭐⭐ 强烈推荐,最稳定) 172.17.x.x:8000 # 格式 2: SAE 内网 Service 域名(需要额外配置服务发现,不推荐) extraction-service-xxxxx.cn-hangzhou.sae.aliyuncs.com:8000 # 格式 3: K8s Service 域名(需要配置K8s服务发现,复杂,不推荐) extraction-service.namespace.svc.cluster.local:8000 -
❌ 错误做法(会导致连接失败):
# ❌ 不要猜测或假设域名(100%失败) http://extraction-service.sae:8000 # .sae 域名不存在 http://extraction-service.internal:8000 # .internal 域名不存在 http://extraction-service.cluster.local:8000 # 需要K8s服务发现配置 # ❌ 不要使用 localhost http://localhost:8000 # SAE 实例间是跨主机的 # ❌ 不要使用 Docker 服务名 http://extraction-service:8000 # 这不是单机 Docker Compose -
✅ 推荐做法(按优先级排序):
# ⭐⭐⭐⭐⭐ 方案A:直接使用内网IP(强烈推荐) EXTRACTION_SERVICE_URL=http://172.17.x.x:8000 # 获取方式:SAE控制台 > Python应用 > 实例列表 > 查看内网IP # ⭐⭐⭐ 方案B:使用SAE服务发现(需要额外配置,不推荐初期使用) # 需要在SAE控制台配置"微服务注册中心" EXTRACTION_SERVICE_URL=http://extraction-service-xxxxx.cn-hangzhou.sae.aliyuncs.com:8000
步骤 2:配置后端环境变量
在 SAE 后端应用的环境变量中添加:
# ⚠️ 使用 SAE 控制台显示的真实内网地址
EXTRACTION_SERVICE_URL=http://172.17.x.x:8000
# 注意:
# 1. 不要使用猜测的域名
# 2. 必须从 SAE 控制台的"应用访问配置"中获取
# 3. 如果 IP 变化(如重新部署),需要同步更新这个环境变量
配置后重启后端应用:
- SAE 控制台 → 后端应用 → 重启
步骤 3:从后端服务测试
在您的 Node.js 后端服务中添加测试端点:
// backend/src/tests/test-extraction-service.ts
import axios from 'axios';
import FormData from 'form-data';
import fs from 'fs';
const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://extraction-service.internal:8000';
export async function testExtractionService() {
try {
// 1. 健康检查
console.log('Testing health endpoint...');
const healthRes = await axios.get(`${EXTRACTION_SERVICE_URL}/health`);
console.log('Health check:', healthRes.data);
// 2. 测试 PDF 提取
console.log('Testing PDF extraction...');
const form = new FormData();
form.append('file', fs.createReadStream('./test.pdf'));
const pdfRes = await axios.post(
`${EXTRACTION_SERVICE_URL}/extract/pdf`,
form,
{ headers: form.getHeaders() }
);
console.log('PDF extraction result:', pdfRes.data);
// 3. 测试 Word 提取
console.log('Testing Word extraction...');
const form2 = new FormData();
form2.append('file', fs.createReadStream('./test.docx'));
const docxRes = await axios.post(
`${EXTRACTION_SERVICE_URL}/extract/docx`,
form2,
{ headers: form2.getHeaders() }
);
console.log('Word extraction result:', docxRes.data);
console.log('✅ All tests passed!');
} catch (error) {
console.error('❌ Test failed:', error.message);
if (error.response) {
console.error('Response:', error.response.data);
}
}
}
步骤 4:验证端到端流程(完整业务场景)
测试以下业务流程:
场景 1: PKB 文档上传
业务流程:
用户上传 PDF
→ Node.js 后端接收
→ HTTP POST 转发文件流到 Python 服务 (EXTRACTION_SERVICE_URL)
→ Python 服务解析 PDF,返回 JSON 文本
→ Node.js 后端收到文本
→ 上传到 Dify
→ 返回前端
测试步骤:
-
在前端上传一个 PDF 文档(建议 < 5MB 的简单文档)
-
查看 Node.js 后端日志(SAE 控制台 → 后端应用 → 日志):
[INFO] Calling extraction service: http://172.17.x.x:8000/extract/pdf [INFO] Extraction completed in 2.3s [INFO] Extracted text preview: "This is a test document..." -
查看 Python 服务日志(SAE 控制台 → extraction-service 应用 → 日志):
INFO: Request: POST /extract/pdf INFO: File size: 1.2MB, filename: test.pdf INFO: Using PyMuPDF extraction INFO: Response: 200 (took 2.10s) -
在 Dify Web UI 中确认文档已上传
如果失败,检查:
- 后端日志是否显示 "Connection refused" → 检查 EXTRACTION_SERVICE_URL 配置
- Python 日志是否显示 "ImportError" → 检查 Dockerfile 系统依赖
- 提取超时(> 300s)→ 文件太大或需要增加超时配置
场景 2: ASL 深度阅读
用户点击"深度阅读" → 后端调用 Python 服务提取全文 → 返回 LLM 分析结果
测试步骤:
- 在 ASL 模块点击"深度阅读"
- 查看后端日志(确认调用 Python 服务)
- 查看 Python 服务日志(确认提取成功)
- 前端显示分析结果
场景 3: DC 数据清洗
用户上传 Excel → 后端调用 Python 服务 fillna → 返回清洗后数据
测试步骤:
- 在 DC 模块上传 Excel 文件
- 执行 fillna 操作
- 查看 Python 服务日志
- 验证清洗结果
监控与维护
📊 SAE 自带监控
1. 查看应用监控
SAE 控制台 → 应用详情 → 监控
关键指标:
- CPU 使用率(< 70%):PDF 提取是 CPU 密集型任务
- 内存使用率(< 80%):大文件处理时会占用较多内存
- 请求 QPS(每秒查询数):了解负载情况
- 平均响应时间(< 1000ms):小文件应 < 2s,大文件 < 30s
- 错误率(< 1%):监控文件解析失败率
性能基准(参考):
小文件(< 1MB PDF):响应时间 1-3s
中等文件(1-10MB PDF):响应时间 5-15s
大文件(10-50MB PDF):响应时间 20-60s
超大文件(> 50MB):建议限制或拒绝
2. 实时日志查看
SAE 控制台 → 应用详情 → 日志 → 实时日志
日志类型:
- 应用日志(stdout/stderr):uvicorn 启动信息、请求日志
- 访问日志(HTTP 请求):请求路径、响应时间、状态码
- 错误日志(异常堆栈):Python 异常详情
关键日志示例:
# ✅ 正常启动
INFO: Started server process [1]
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000
# ✅ 正常请求
INFO: Request: POST /extract/pdf
INFO: File: test.pdf (1.2MB)
INFO: Response: 200 (took 2.10s)
# ❌ 错误日志(需关注)
ERROR: ImportError: libGL.so.1: cannot open shared object file
ERROR: Timeout: PDF extraction took > 300s
ERROR: Memory error: Cannot allocate memory
3. 弹性伸缩配置
SAE 控制台 → 应用详情 → 弹性伸缩
推荐配置:
最小实例数: 1(确保服务不中断)
最大实例数: 3(根据实际负载调整)
触发条件:
- CPU 使用率 > 70% 持续 3 分钟 → 扩容 1 个实例
- CPU 使用率 < 30% 持续 5 分钟 → 缩容 1 个实例
注意事项:
- PDF 提取是 CPU 密集型,扩容主要看 CPU
- 如果经常扩容,考虑直接增加实例规格(2核 → 4核)
- SAE 会自动负载均衡,无需手动配置
📊 应用内监控
添加健康检查端点
# main.py
from fastapi import FastAPI
import psutil
import os
app = FastAPI()
@app.get("/health")
async def health_check():
"""健康检查端点"""
return {
"status": "healthy",
"service": "extraction-service",
"version": os.getenv("SERVICE_VERSION", "unknown")
}
@app.get("/metrics")
async def metrics():
"""性能指标端点"""
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/app')
return {
"cpu": {
"percent": cpu_percent,
"count": psutil.cpu_count()
},
"memory": {
"total": memory.total,
"available": memory.available,
"percent": memory.percent
},
"disk": {
"total": disk.total,
"used": disk.used,
"free": disk.free,
"percent": disk.percent
}
}
添加请求日志
# main.py
import logging
from fastapi import Request
import time
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/app/logs/extraction-service.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
@app.middleware("http")
async def log_requests(request: Request, call_next):
"""请求日志中间件"""
start_time = time.time()
# 记录请求
logger.info(f"Request: {request.method} {request.url}")
# 执行请求
response = await call_next(request)
# 记录响应
process_time = time.time() - start_time
logger.info(
f"Response: {response.status_code} "
f"(took {process_time:.2f}s)"
)
return response
🔄 定期维护任务
每周任务
# 1. 检查日志大小
du -sh /app/logs
# 2. 查看错误日志
tail -n 100 /app/logs/extraction-service.log | grep ERROR
# 3. 重启应用(如果有内存泄漏)
# SAE 控制台 → 应用详情 → 重启
每月任务
# 1. 更新 Python 依赖
pip list --outdated
# 2. 重建镜像(包含安全更新)
docker build -t extraction-service:v1.1 .
docker push registry.cn-beijing.aliyuncs.com/clinical-research/extraction-service:v1.1
# 3. 在 SAE 中更新镜像版本
故障排查
🔥 常见问题
问题 1:容器启动失败
症状:
SAE 显示:应用启动失败
日志显示:ImportError: libXXX.so: cannot open shared object file
原因:缺少系统依赖
解决:
# 在 Dockerfile 中添加缺失的库
RUN apt-get update && apt-get install -y \
libgl1-mesa-glx \ # OpenCV
libglib2.0-0 \ # OpenCV
libgomp1 \ # Polars
libmupdf-dev \ # PyMuPDF
&& rm -rf /var/lib/apt/lists/*
问题 2:PDF 提取超时
症状:
请求超时(> 300秒)
日志显示:Timeout error
排查步骤:
# 1. 检查文件大小
# 如果文件 > 50MB,考虑分块处理
# 2. 增加超时时间
# SAE 控制台 → 应用配置 → 环境变量
TIMEOUT=600
# 3. 优化提取逻辑
# 跳过图片页、压缩图片等
问题 3:内存溢出(OOM)
症状:
容器自动重启
日志显示:Killed (signal 9)
解决:
# 1. 增加内存配置
# SAE 控制台 → 应用配置 → 规格
内存: 2GB → 4GB
# 2. 优化代码(流式处理)
# 不要一次性加载整个文件到内存
with open(pdf_path, 'rb') as f:
# 分块处理
for chunk in read_in_chunks(f):
process(chunk)
问题 4:后端无法连接 Python 服务(高频错误)
症状:
后端日志:Connection refused
或
ECONNREFUSED: connect ECONNREFUSED 172.17.x.x:8000
或
Error: getaddrinfo ENOTFOUND extraction-service.internal
根本原因排查:
原因 1:内网地址配置错误(最常见)
# ❌ 错误配置(猜测的域名)
EXTRACTION_SERVICE_URL=http://extraction-service.internal:8000
# ✅ 正确配置(SAE 控制台显示的真实地址)
EXTRACTION_SERVICE_URL=http://172.17.x.x:8000
解决方法:
# 1. 获取真实内网地址
# SAE 控制台 → extraction-service 应用 → 应用详情 → 应用访问配置
# 复制显示的"VPC 内网访问地址"
# 2. 更新后端环境变量
# SAE 控制台 → 后端应用 → 应用配置 → 环境变量
EXTRACTION_SERVICE_URL=http://<真实内网IP>:8000
# 3. 重启后端应用
# SAE 控制台 → 后端应用 → 重启
原因 2:Python 服务未启动
# 检查 Python 服务状态
# SAE 控制台 → extraction-service 应用 → 实例列表
# 确认实例状态为"运行中"
# 查看启动日志
# SAE 控制台 → extraction-service 应用 → 日志
# 应该看到:
# INFO: Application startup complete.
# INFO: Uvicorn running on http://0.0.0.0:8000
原因 3:安全组规则限制
# SAE 默认同 VPC 内应用可互相访问
# 如果仍无法连接,检查:
# SAE 控制台 → extraction-service 应用 → 网络配置 → 安全组
# 确认入站规则允许 VPC 内访问 8000 端口
测试内网连通性:
# 方法 1:在 SAE 控制台的"Webshell"中测试(如果支持)
curl http://<Python服务内网IP>:8000/health
# 方法 2:在后端应用的启动脚本中添加测试
echo "Testing extraction service connectivity..."
curl -f http://<Python服务内网IP>:8000/health || echo "❌ Cannot connect to extraction service"
# 方法 3:使用 telnet 测试端口
telnet <Python服务内网IP> 8000
注意事项与禁忌
✅ 最佳实践
1. 镜像优化
# ✅ 使用多阶段构建
FROM python:3.11-slim as builder
# ... 构建 ...
FROM python:3.11-slim
COPY --from=builder /opt/venv /opt/venv
# ✅ 清理缓存
RUN apt-get update && apt-get install -y ... \
&& rm -rf /var/lib/apt/lists/*
# ✅ 使用 .dockerignore
# 避免将不必要的文件打包到镜像
2. 版本管理
# ✅ 使用语义化版本
v1.0.0 # 主版本.次版本.补丁版本
# ✅ 保留多个版本
docker tag ... extraction-service:v1.0.0
docker tag ... extraction-service:v1.0
docker tag ... extraction-service:latest
# ✅ 记录变更
# CHANGELOG.md
## v1.0.1 (2025-12-20)
- 修复: PDF 提取超时问题
- 优化: 减小镜像体积 30%
3. 安全加固
# ✅ 文件大小限制
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
@app.post("/extract/pdf")
async def extract_pdf(file: UploadFile):
if file.size > MAX_FILE_SIZE:
raise HTTPException(
status_code=413,
detail="File too large"
)
# ✅ 文件类型验证
ALLOWED_TYPES = {'application/pdf', 'application/msword'}
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=415,
detail="Unsupported file type"
)
4. 性能优化
# ✅ 异步处理大文件
import asyncio
async def extract_large_pdf(pdf_path: str):
# 使用异步 I/O
async with aiofiles.open(pdf_path, 'rb') as f:
content = await f.read()
# 在线程池中执行 CPU 密集型任务
loop = asyncio.get_event_loop()
text = await loop.run_in_executor(None, pymupdf_extract, content)
return text
# ✅ 连接池
from sqlalchemy.pool import NullPool
engine = create_engine(
DATABASE_URL,
poolclass=NullPool, # SAE 环境推荐
echo=False
)
❌ 绝对禁止
1. 禁止猜测或假设内网地址(致命错误)
# ❌ 错误做法(会导致连接失败)
EXTRACTION_SERVICE_URL=http://extraction-service.internal:8000
EXTRACTION_SERVICE_URL=http://localhost:8000
EXTRACTION_SERVICE_URL=http://extraction-service:8000
# ✅ 正确做法:从 SAE 控制台获取真实地址
# SAE 控制台 → extraction-service 应用 → 应用访问配置
# 复制显示的"VPC 内网访问地址"
EXTRACTION_SERVICE_URL=http://172.17.x.x:8000
原因:
- SAE 实例间是跨主机的,不能使用 Docker 服务名
- SAE 的 K8s Service 域名格式因配置而异,不能假设
- 最稳妥的是使用 SAE 控制台显示的 IP 地址
2. 禁止在镜像中硬编码敏感信息
# ❌ 错误示例
ENV DATABASE_PASSWORD=my-secret-password
# ✅ 正确做法:在 SAE 环境变量中配置
3. 禁止使用本地文件持久化存储
# ❌ 错误示例(容器重启后丢失)
output_path = '/app/output/result.txt'
with open(output_path, 'w') as f:
f.write(result)
# ✅ 正确做法:使用 /tmp 存临时文件,结果上传到 OSS
import tempfile
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write(result)
# 上传到 OSS(使用 oss2 库)
# 最后删除临时文件
4. 禁止使用 :latest 标签在生产环境
# ❌ 错误做法(无法回滚)
image: extraction-service:latest
# ✅ 正确做法(语义化版本)
image: extraction-service:v1.0.0
5. 禁止在容器内修改代码
# ❌ 错误操作(容器重启后丢失)
# SAE Webshell → vi /app/main.py
# ✅ 正确流程:
# 1. 本地修改代码
# 2. 重建镜像
# 3. 推送到 ACR
# 4. SAE 中更新镜像版本
6. 禁止使用无限增长的全局变量
# ❌ 错误示例(内存泄漏)
CACHE = {} # 全局缓存,无限增长
@app.post("/extract/pdf")
async def extract_pdf(file: UploadFile):
key = file.filename
if key not in CACHE:
CACHE[key] = extract(file) # 内存会持续增长!
return CACHE[key]
# ✅ 正确做法:使用有限容量的缓存
from functools import lru_cache
@lru_cache(maxsize=100) # 最多缓存 100 个结果
def extract_with_cache(file_hash: str):
return extract(file_hash)
7. 禁止忽略 /tmp 目录的大小限制
# ⚠️ 注意:SAE 容器的 /tmp 目录通常有大小限制(如 1-2GB)
# 处理大文件后必须清理临时文件
import os
import tempfile
async def extract_large_pdf(file: UploadFile):
# 保存到临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp:
content = await file.read()
tmp.write(content)
tmp_path = tmp.name
try:
# 处理文件
result = extract_pdf_pymupdf(tmp_path)
return result
finally:
# ✅ 关键:必须清理临时文件
if os.path.exists(tmp_path):
os.unlink(tmp_path)
📚 附录
A. 完整的 requirements.txt(阶段1)
# Web 框架
fastapi==0.115.5
uvicorn[standard]==0.32.1
python-multipart==0.0.20
# 文档提取
PyMuPDF==1.24.14
mammoth==1.8.0
python-docx==1.1.2
# 数据处理
polars==1.17.1
numpy==1.26.4
# 辅助工具
langdetect==1.0.9
chardet==5.2.0
aiofiles==23.2.1
# 数据库
sqlalchemy==2.0.25
asyncpg==0.29.0
# 阿里云 OSS
oss2==2.18.3
# 日志和监控
python-json-logger==2.0.7
psutil==5.9.8
B. Dockerfile 完整版
参见前文 构建 Docker 镜像 - 步骤 1
C. 本地测试脚本
#!/bin/bash
# test-local.sh
echo "Building Docker image..."
docker build -t extraction-service:test .
echo "Starting container..."
docker run -d \
--name extraction-test \
-p 8000:8000 \
-e DATABASE_URL="postgresql://user:pass@host:5432/db" \
extraction-service:test
echo "Waiting for service to start..."
sleep 10
echo "Testing health endpoint..."
curl http://localhost:8000/health
echo "Testing PDF extraction..."
curl -X POST \
-F "file=@test.pdf" \
http://localhost:8000/extract/pdf
echo "Cleaning up..."
docker stop extraction-test
docker rm extraction-test
echo "Done!"
D. 相关文档链接
🎯 快速参考
常用命令
# 构建镜像
docker build -t extraction-service:v1.0 .
# 推送镜像
docker push registry.cn-beijing.aliyuncs.com/clinical-research/extraction-service:v1.0
# 查看 SAE 日志
# SAE 控制台 → 应用详情 → 日志
# 重启 SAE 应用
# SAE 控制台 → 应用详情 → 重启
# 测试内网连通性
curl http://extraction-service.internal:8000/health
# 查看容器资源
docker stats extraction-service
关键配置
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| CPU | 1核 | 初始配置 |
| 内存 | 2GB | 不含 Nougat |
| 实例数 | 1-3 | 自动弹性伸缩 |
| 超时时间 | 300秒 | 大文件处理 |
| 健康检查 | 30秒 | 初始延迟 |
| Worker 数量 | 2 | Uvicorn workers |
文档维护:
- 如有问题或建议,请联系技术负责人
- 最后更新:2025-12-13
- 下次审查:2025-03-13