# Python 微服务 SAE 容器部署完全指南 **文档版本**: v1.1 (修复内网地址和临时文件问题) **创建时间**: 2025-12-13 **最后修订**: 2025-12-13 **适用范围**: AIclinicalresearch 平台 - Python 微服务(extraction_service) **目标读者**: 运维工程师、后端开发工程师 **v1.1 更新日志**: - ✅ 修复:内网地址使用 SAE 控制台显示的真实 IP(不猜测域名) - ✅ 优化:Dockerfile 系统依赖说明(libmupdf-dev 可选) - ✅ 新增:确保 /tmp 目录可写(大文件临时存储) - ✅ 完善:功能验证流程和监控指南 --- ## 📋 文档目录 1. [为什么选择 SAE 容器部署](#为什么选择-sae-容器部署) 2. [部署架构图](#部署架构图) 3. [前置准备清单](#前置准备清单) 4. [Python 服务分析](#python-服务分析) 5. [依赖优化策略](#依赖优化策略) 6. [构建 Docker 镜像](#构建-docker-镜像) 7. [部署到 SAE](#部署到-sae) 8. [测试与验证](#测试与验证) 9. [监控与维护](#监控与维护) 10. [故障排查](#故障排查) 11. [注意事项与禁忌](#注意事项与禁忌) --- ## 为什么选择 SAE 容器部署 ### ✅ SAE 容器部署 vs. SAE Python 运行时 | 对比维度 | SAE Python 运行时 | SAE 容器部署 (推荐) | |---------|-----------------|------------------| | **系统依赖** | ❌ 无法安装系统库 | ✅ 完全可控 | | **复杂依赖** | ❌ PyMuPDF/OpenCV 报错 | ✅ 完美支持 | | **环境一致性** | ⚠️ 云上和本地可能不同 | ✅ 本地跑通 = 云上跑通 | | **Nougat (Torch)** | ❌ 版本冲突风险高 | ✅ 轻松支持 | | **部署方式** | 上传 ZIP 包 | 推送 Docker 镜像 | | **启动速度** | 快(< 5秒) | 较快(10-20秒) | | **运维复杂度** | 低 | 中 | | **推荐度** | ❌ 不推荐 | ✅ 强烈推荐 | ### 🎯 核心原因 #### 1. **系统级依赖缺失(致命问题)** ```python # 您的代码使用了这些库: import fitz # PyMuPDF → 依赖 libmupdf.so, libfreetype.so import cv2 # OpenCV → 依赖 libGL.so.1, libgthread-2.0.so import polars # Polars → 依赖 libgomp.so ``` **SAE Python 运行时**: ```bash ❌ 只提供标准 Python 环境 ❌ 无法执行 apt-get install ❌ 运行时报错:ImportError: libGL.so.1: cannot open shared object file ``` **SAE 容器部署**: ```dockerfile ✅ 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元 | 数据库 | ### ✅ 软件准备 ```bash # 本地开发机器需要安装 - 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) ``` **关键端点**: ```python POST /extract/pdf # PDF 提取 POST /extract/docx # Word 提取 POST /extract/txt # 文本提取 POST /operations/fillna # 数据清洗 ``` ### 📊 依赖分析 #### 当前 `requirements.txt` 内容: ```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`**: ```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 基础镜像) **代码修改**: ```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`**: ```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`: ```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:本地构建镜像 ```bash # 进入 extraction_service 目录 cd d:\MyCursor\AIclinicalresearch\extraction_service # 构建镜像(本地测试) docker build -t extraction-service:latest . # 查看镜像大小 docker images extraction-service ``` ### 步骤 4:本地测试镜像 ```bash # 启动容器(本地测试) 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 创建镜像仓库(首次部署) 1. **登录阿里云控制台** → **容器镜像服务 ACR** 2. **创建个人实例**(免费版): ``` 实例名称: extraction-service 地域: 华东1(杭州) ``` 3. **创建命名空间**: ``` 命名空间: clinical-research ``` 4. **创建镜像仓库**: ``` 仓库名称: extraction-service 代码源: 本地仓库 ``` #### 5.2 推送镜像 ```bash # 1. 登录阿里云容器镜像服务 # 获取登录命令:阿里云控制台 → 容器镜像服务 → 访问凭证 → 设置Registry登录密码 docker login --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 应用 1. **登录阿里云控制台** → **Serverless 应用引擎 SAE** 2. **创建应用**: ``` 应用名称: extraction-service 命名空间: 选择后端所在的命名空间(同 VPC) 部署方式: 镜像 ``` 3. **镜像配置**: ``` 镜像地址: registry.cn-beijing.aliyuncs.com/clinical-research/extraction-service:latest 镜像版本: latest 镜像拉取策略: Always(每次部署都拉取最新镜像) ``` 4. **规格配置**: ``` CPU: 1核 内存: 2GB 实例数: 1(初始) 弹性扩缩容: - 最小实例数: 1 - 最大实例数: 3 - CPU 触发阈值: 70% ``` 5. **网络配置**: ``` 专有网络 VPC: 选择后端所在的 VPC vSwitch: 选择后端所在的交换机 安全组: 允许 VPC 内访问 ``` ### 步骤 2:配置环境变量 在 SAE 应用配置中添加以下环境变量: ```bash # ========= 数据库配置 ========= 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= OSS_ACCESS_KEY_SECRET= # ========= 服务配置 ========= SERVICE_NAME=extraction-service SERVICE_VERSION=v1.0 LOG_LEVEL=INFO # ========= 性能配置 ========= WORKERS=2 TIMEOUT=300 MAX_FILE_SIZE=52428800 # ========= 时区 ========= TZ=Asia/Shanghai ``` ### 步骤 3:配置健康检查 ```bash 健康检查路径: /health 健康检查端口: 8000 健康检查协议: HTTP 初始延迟: 30秒 检查间隔: 10秒 超时时间: 5秒 健康阈值: 2次 不健康阈值: 3次 ``` ### 步骤 4:配置日志 ```bash 日志目录: /app/logs 日志文件: extraction-service.log 日志级别: INFO 日志保留天数: 7天 ``` ### 步骤 5:配置 SLB(可选,如果需要公网访问) ```bash # 通常 Python 微服务只需要内网访问(被后端调用) # 如果需要公网访问(如:调试、第三方集成): 负载均衡类型: 公网 监听端口: 80 后端端口: 8000 健康检查: 启用 ``` ### 步骤 6:部署应用 1. **点击"部署应用"** 2. **等待部署完成**(约 2-3 分钟) 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 提供的内网地址** #### 获取真实内网地址的正确方法: 1. **登录 SAE 控制台** → **应用列表** → **点击 extraction-service 应用** 2. **在应用详情页,找到"应用访问配置"或"VPC 内网访问"部分** 3. **查看并复制"内网访问地址"**,通常是以下格式之一: ``` # 格式 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 ``` 4. **❌ 错误做法(会导致连接失败)**: ```bash # ❌ 不要猜测或假设域名(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 ``` 5. **✅ 推荐做法(按优先级排序)**: ```bash # ⭐⭐⭐⭐⭐ 方案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 后端应用的环境变量中添加: ```bash # ⚠️ 使用 SAE 控制台显示的真实内网地址 EXTRACTION_SERVICE_URL=http://172.17.x.x:8000 # 注意: # 1. 不要使用猜测的域名 # 2. 必须从 SAE 控制台的"应用访问配置"中获取 # 3. 如果 IP 变化(如重新部署),需要同步更新这个环境变量 ``` **配置后重启后端应用**: - SAE 控制台 → 后端应用 → 重启 ### 步骤 3:从后端服务测试 在您的 Node.js 后端服务中添加测试端点: ```typescript // 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 → 返回前端 ``` **测试步骤**: 1. 在前端上传一个 PDF 文档(建议 < 5MB 的简单文档) 2. **查看 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..." ``` 3. **查看 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) ``` 4. **在 Dify Web UI 中确认文档已上传** **如果失败,检查**: - 后端日志是否显示 "Connection refused" → 检查 EXTRACTION_SERVICE_URL 配置 - Python 日志是否显示 "ImportError" → 检查 Dockerfile 系统依赖 - 提取超时(> 300s)→ 文件太大或需要增加超时配置 #### 场景 2: ASL 深度阅读 ``` 用户点击"深度阅读" → 后端调用 Python 服务提取全文 → 返回 LLM 分析结果 ``` **测试步骤**: 1. 在 ASL 模块点击"深度阅读" 2. 查看后端日志(确认调用 Python 服务) 3. 查看 Python 服务日志(确认提取成功) 4. 前端显示分析结果 #### 场景 3: DC 数据清洗 ``` 用户上传 Excel → 后端调用 Python 服务 fillna → 返回清洗后数据 ``` **测试步骤**: 1. 在 DC 模块上传 Excel 文件 2. 执行 fillna 操作 3. 查看 Python 服务日志 4. 验证清洗结果 --- ## 监控与维护 ### 📊 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 异常详情 **关键日志示例**: ```bash # ✅ 正常启动 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 会自动负载均衡,无需手动配置 ### 📊 应用内监控 #### 添加健康检查端点 ```python # 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 } } ``` #### 添加请求日志 ```python # 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 ``` ### 🔄 定期维护任务 #### 每周任务 ```bash # 1. 检查日志大小 du -sh /app/logs # 2. 查看错误日志 tail -n 100 /app/logs/extraction-service.log | grep ERROR # 3. 重启应用(如果有内存泄漏) # SAE 控制台 → 应用详情 → 重启 ``` #### 每月任务 ```bash # 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 # 在 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 ``` **排查步骤**: ```bash # 1. 检查文件大小 # 如果文件 > 50MB,考虑分块处理 # 2. 增加超时时间 # SAE 控制台 → 应用配置 → 环境变量 TIMEOUT=600 # 3. 优化提取逻辑 # 跳过图片页、压缩图片等 ``` #### 问题 3:内存溢出(OOM) **症状**: ``` 容器自动重启 日志显示:Killed (signal 9) ``` **解决**: ```bash # 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:内网地址配置错误(最常见)** ```bash # ❌ 错误配置(猜测的域名) EXTRACTION_SERVICE_URL=http://extraction-service.internal:8000 # ✅ 正确配置(SAE 控制台显示的真实地址) EXTRACTION_SERVICE_URL=http://172.17.x.x:8000 ``` **解决方法**: ```bash # 1. 获取真实内网地址 # SAE 控制台 → extraction-service 应用 → 应用详情 → 应用访问配置 # 复制显示的"VPC 内网访问地址" # 2. 更新后端环境变量 # SAE 控制台 → 后端应用 → 应用配置 → 环境变量 EXTRACTION_SERVICE_URL=http://<真实内网IP>:8000 # 3. 重启后端应用 # SAE 控制台 → 后端应用 → 重启 ``` **原因 2:Python 服务未启动** ```bash # 检查 Python 服务状态 # SAE 控制台 → extraction-service 应用 → 实例列表 # 确认实例状态为"运行中" # 查看启动日志 # SAE 控制台 → extraction-service 应用 → 日志 # 应该看到: # INFO: Application startup complete. # INFO: Uvicorn running on http://0.0.0.0:8000 ``` **原因 3:安全组规则限制** ```bash # SAE 默认同 VPC 内应用可互相访问 # 如果仍无法连接,检查: # SAE 控制台 → extraction-service 应用 → 网络配置 → 安全组 # 确认入站规则允许 VPC 内访问 8000 端口 ``` **测试内网连通性**: ```bash # 方法 1:在 SAE 控制台的"Webshell"中测试(如果支持) curl http://:8000/health # 方法 2:在后端应用的启动脚本中添加测试 echo "Testing extraction service connectivity..." curl -f http://:8000/health || echo "❌ Cannot connect to extraction service" # 方法 3:使用 telnet 测试端口 telnet 8000 ``` --- ## 注意事项与禁忌 ### ✅ 最佳实践 #### 1. **镜像优化** ```dockerfile # ✅ 使用多阶段构建 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. **版本管理** ```bash # ✅ 使用语义化版本 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. **安全加固** ```python # ✅ 文件大小限制 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. **性能优化** ```python # ✅ 异步处理大文件 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. **禁止猜测或假设内网地址(致命错误)** ```bash # ❌ 错误做法(会导致连接失败) 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. **禁止在镜像中硬编码敏感信息** ```dockerfile # ❌ 错误示例 ENV DATABASE_PASSWORD=my-secret-password # ✅ 正确做法:在 SAE 环境变量中配置 ``` #### 3. **禁止使用本地文件持久化存储** ```python # ❌ 错误示例(容器重启后丢失) 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 标签在生产环境** ```bash # ❌ 错误做法(无法回滚) image: extraction-service:latest # ✅ 正确做法(语义化版本) image: extraction-service:v1.0.0 ``` #### 5. **禁止在容器内修改代码** ```bash # ❌ 错误操作(容器重启后丢失) # SAE Webshell → vi /app/main.py # ✅ 正确流程: # 1. 本地修改代码 # 2. 重建镜像 # 3. 推送到 ACR # 4. SAE 中更新镜像版本 ``` #### 6. **禁止使用无限增长的全局变量** ```python # ❌ 错误示例(内存泄漏) 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 目录的大小限制** ```python # ⚠️ 注意: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) ```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 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](#步骤-1创建优化的-dockerfile) ### C. 本地测试脚本 ```bash #!/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. 相关文档链接 - [阿里云 SAE 文档](https://help.aliyun.com/product/134532.html) - [Docker 文档](https://docs.docker.com/) - [FastAPI 文档](https://fastapi.tiangolo.com/) - [PyMuPDF 文档](https://pymupdf.readthedocs.io/) - [Polars 文档](https://pola-rs.github.io/polars/) --- ## 🎯 快速参考 ### 常用命令 ```bash # 构建镜像 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