Files
AIclinicalresearch/docs/01-平台基础层/02-存储服务/OSS存储架构规划与最佳实践V4.2.md

250 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.
# **OSS 存储架构规划与最佳实践 (V4.3)**
**文档状态:** 已发布 (v4.3 \- 含安全红线准则)
**适用项目:** AIclinicalresearch
**核心原则:** 后端统一管控、内网高速传输、数据绝对私有
**最后更新:** 2026-01-21
## **1\. 总体架构设计**
鉴于我们\*\*全线采用“后端流式上传”\*\*策略,架构重心从“端到端交互”转移至“服务器端流处理”。
### **1.1 核心优势**
* **开发极简**:前端只需普通的 Form Data 上传,无需引入阿里 SDK无需处理签名。
* **权限收敛**OSS 的写权限仅由后端 Node.js 掌握,前端 0 权限,杜绝 Key 泄露风险。
* **处理原子化**:上传 OSS \+ 写入数据库 \+ 触发 AI 解析,这三个动作在后端同一个事务流程中完成,不再有“孤儿文件”问题。
### **1.2 Bucket 规划矩阵**
继续沿用 **“四 Bucket 物理隔离”** 策略。
| 环境 | Bucket 名称 (示例) | 权限 (ACL) | 用途 | 生命周期策略 |
| :---- | :---- | :---- | :---- | :---- |
| **生产 (Prod)** | **ai-clinical-data** | **私有 (Private)** | **(核心资产)** 存放病历、文献、稿件、统计结果、RAG索引。 | 智能分层 (30天转低频) |
| **生产 (Prod)** | **ai-clinical-static** | **公共读 (Public)** | **(静态资源)** 存放系统头像、**Admin上传资源**、RAG解析图片。 | 永久保存 |
| **开发 (Dev)** | **ai-clinical-data-dev** | **私有** | **(开发测试)** 存放开发调试产生的垃圾数据。 | **30天自动清空** (防止成本失控) |
| **开发 (Dev)** | **ai-clinical-static-dev** | **公共读** | **(开发资源)** 存放开发环境的静态资源。 | 永久保存 |
### **1.3 关键配置规范 (针对后端流式优化)**
1. **服务端加密 (Encryption)**:所有 **Data Bucket** 必须开启 **SSE-OSS (AES256)**
2. **跨域 (CORS) \- 仅需读权限**
* **场景**:仅用于前端直接通过 URL (签名或公开) 预览/下载文件。**上传不需要 CORS**。
* **来源**http://localhost:3000 (Dev), https://你的生产域名.com (Prod)
* **允许 Methods**:仅需 **GET**, **HEAD**。 (严禁开启 PUT/POST/DELETE)
* **允许 Headers**\*
* **暴露 Headers**ETag, x-oss-request-id
3. **内网加速**
* **SAE 环境变量**:必须配置 OSS\_ENDPOINT 为 oss-cn-beijing-internal.aliyuncs.com。
* **效果**SAE \-\> OSS 流量免费且速度极快(千兆级),这是后端流式上传可行的物理基础。
## **2\. 目录结构设计 (Object Key)**
即使上传方式改变,清晰的目录结构依然是多租户隔离的基础。
### **2.1 核心数据目录 (ai-clinical-data / ...-dev)**
\# 1\. 个人用户 (To C)
users/{userId}/pkb/{kbId}/documents/{fileId}.pdf \# PKB 原始文献
users/{userId}/asl/projects/{projId}/pdfs/{fileId}.pdf \# ASL 文献PDF
users/{userId}/rvw/tasks/{taskId}/source.{ext} \# RVW 原始稿件
users/{userId}/rvw/tasks/{taskId}/report.docx \# RVW 审稿报告
users/{userId}/dc/tasks/{taskId}/source.xlsx \# Tool B 数据清洗任务
\# 2\. 统计模块 (SSA/ST)
users/{userId}/ssa/projects/{projId}/data/{fileId}.xlsx \# SSA 原始数据
users/{userId}/ssa/projects/{projId}/outputs/{runId}/... \# SSA 结果
users/{userId}/st/tasks/{taskId}/source.xlsx \# ST 工具数据
users/{userId}/st/tasks/{taskId}/plot.png \# ST 结果图表 (私有\!)
\# 3\. 机构租户 (To B)
tenants/{tenantId}/ekb/{kbId}/documents/{year}/{id}.pdf
tenants/{tenantId}/asl/projects/{projId}/pdfs/{id}.pdf
tenants/{tenantId}/emr/original/{patientId}.json
\# 4\. 临时数据 (自动清理)
temp/dc/sessions/{sessionId}/source.xlsx \# Tool C 原始文件 (1天删除)
temp/dc/sessions/{sessionId}/clean\_data.json \# Tool C 中间缓存 (1天删除)
temp/asl/imports/{date}/{uuid}.xlsx \# ASL 导入临时表 (1天删除)
\# 5\. 系统级数据 (Platform Assets)
system/kb/guidelines/{year}/{fileId}.pdf \# 系统内置知识库(GCP/法规/指南)
system/docs/manuals/{version}/{fileId}.pdf \# 用户操作手册/帮助文档
system/samples/datasets/{fileId}.xlsx \# 系统预置演示数据(Demo Data)
### **2.2 静态资源目录 (ai-clinical-static / ...-dev)**
\# RAG 图片 & 系统资源
images/rag/{YYYY}/{MM}/{long-random-hash-uuid}.png \# RAG 解析插图
assets/avatars/{userId}.png \# 用户头像
assets/system/logo.png \# 租户 Logo
## **3\. 后端流式上传:统一实施方案**
我们放弃复杂的 STS全平台统一使用 **Node.js Stream Pipe** 模式。
### **3.1 基础上传流程 (ASL / RVW / SSA / ST)**
适用于只需存储、后续异步处理的场景。
**流程:**
1. **前端**FormData POST 文件到后端。
2. **后端**
* 使用 @fastify/multipart 获取文件流 (part.file)。
* **限制大小**:在 Fastify 层配置 limits: { fileSize: 100MB }。
* **流式传输**:使用 pipeline 将流直接对接 OSS SDK 的 putStream。
* **零内存**:文件流不经过 RAM Buffer直接走内网 I/O。
**代码范例:**
import { pipeline } from 'stream/promises';
// Controller
const part \= await req.file();
const stream \= part.file;
const key \= \`users/${userId}/asl/.../${filename}\`;
// Service (common/storage)
await ossClient.putStream(key, stream);
// 成功后写入 DB
### **3.2 高级流程:流式分发 (RAG / Tool C / Tool B)**
适用于\*\*“既要存备份,又要立即解析”\*\*的场景。这是后端流式上传的最大优势——可以“一鱼两吃”。
**问题**Stream 只能被消费一次。如果发给了 OSSPython 微服务就拿不到了。 **解决**:使用 Node.js 的 PassThrough 创建流的分支。
**流程:**
1. **前端**:上传文件。
2. **后端 (Node.js)**
* 创建两个 PassThrough 流streamA (给 OSS), streamB (给 Python)。
* 源流 .pipe(streamA), 源流 .pipe(streamB)。
* **并行执行**Promise.all(\[ 上传OSS(streamA), 调用Python(streamB) \])。
3. **结果**文件存下了Markdown/清洗结果也拿到了,且只消耗了一次上传带宽。
**代码范例:**
import { PassThrough } from 'stream';
// 1\. 创建分支
const uploadStream \= new PassThrough();
const processStream \= new PassThrough();
// 2\. 分流
part.file.pipe(uploadStream);
part.file.pipe(processStream);
// 3\. 并行处理
await Promise.all(\[
// 任务A存 OSS (备份)
storage.uploadStream(key, uploadStream),
// 任务B发给 Python 解析 (业务)
extractionService.extract(processStream)
\]);
## **4\. 权限与安全管理 (后端主导)**
由于前端不再直接接触 OSS安全边界收缩至后端服务器。
### **4.1 RAM 角色配置 (SAE 实例身份)**
* **不再需要**:为每个用户签发 STS Token 的复杂逻辑。
* **只需要**:为 SAE 实例绑定一个 RAM Role。
* **权限策略**
* AliyunOSSFullAccess (简单粗暴,仅限 MVP)。
* 或者自定义策略:仅允许对 ai-clinical-\* 开头的 Bucket 进行 PutObject, GetObject, DeleteObject。
### **4.2 下载/预览安全 (Presigned URL)**
对于私有 Bucket (ai-clinical-data) 的文件,前端如何访问?
* **API**GET /api/storage/url?key=...
* **后端**:调用 ossClient.signatureUrl(key, { expires: 3600 })。
* **前端**:拿到带签名的长 URL直接 window.open 或 \<img src="..."\>。
* **安全**:签名 URL 默认 1 小时过期,且无法被枚举。
## **5\. 生命周期与成本控制**
利用 OSS 规则自动兜底,防止垃圾文件堆积。
| Bucket | 目录前缀 | 策略 | 目的 |
| :---- | :---- | :---- | :---- |
| **Data (Prod)** | temp/ | **1天后删除** | **Tool C Session、ASL 导入 Excel统统自动清理。** |
| **Data (Prod)** | users/\*/st/ | **90天后转归档** | ST 小工具历史记录。 |
| **Data (Prod)** | business/dc/ | **30天后转低频** | Tool B 长期任务。 |
| **Data (Dev)** | **整个 Bucket** | **30天后删除** | 开发环境自动复位。 |
## **6\. 实施 Checklist (V4.3)**
* ![][image1]**1\. Bucket 创建与配置**
* **![][image1]**创建 4 个 Bucket (Prod/Dev x Data/Static)。
* ![][image1]**Prod Data Bucket**:开启 SSE-OSS 加密。
* ![][image1]**OSS CORS**:仅配置 GET 方法 (Allow \* Headers)。
* ![][image1]**生命周期**:配置 temp/ 1天删除规则。
* ![][image1]**2\. 环境变量配置 (SAE & Local)**
* **![][image1]**OSS\_ENDPOINT: 生产环境务必填 oss-cn-beijing-internal.aliyuncs.com。
* ![][image1]OSS\_ACCESS\_KEY\_ID: 使用 SAE 绑定的 RAM Role (最佳) 或 专用 AK。
* ![][image1]**3\. 后端代码实现**
* **![][image1]**完善 common/storage/OssAdapter.ts实现 uploadStream 方法。
* ![][image1]完善 SessionService.ts (Tool C):使用 PassThrough 实现“边存边算”。
* ![][image1]完善 ReviewService.ts (RVW):实现报告生成的流式上传。
* ![][image1]**4\. 移除旧代码** (清理债)
* ![][image1]删除所有 STS Token 获取接口 (get-sts-token)。
* ![][image1]删除前端所有 ali-oss 依赖。
## **7\. 逆向审计:潜在风险与应对策略 (Red Teaming)**
从**逆向思维**角度审视“全后端流式上传”方案,梳理出物理层与逻辑层的 7 大隐患。
### **7.1 物理网络风险 (Physical Layer)**
#### **风险 1带宽“发卡弯”效应 (The Bandwidth Hairpin)**
* **原理**:流量路径为 用户 \-\> SAE (公网入) \-\> OSS (内网出)。
* **隐患**SAE 到 OSS 虽快,但 **用户到 SAE 的入网带宽 (Ingress)** 是受限的。如果 SAE 公网带宽仅购买了 5Mbps所有用户上传总速度将被卡死在 600KB/s。
* **✅ 应对策略**
* **SAE 计费模式**:务必选择 **"按流量计费"** (Pay-By-Traffic),将带宽峰值拉高至 **100Mbps**。这样既保证了速度,又只在有上传时扣费,成本可控。
#### **风险 2浏览器连接槽位耗尽 (Connection Slot Exhaustion)**
* **场景**ASL 模块批量上传 100 个文件。
* **隐患**:浏览器对同一域名的并发连接数有限制(通常 6 个)。如果前端并发 100 个请求,后续请求会处于 Pending 状态,甚至超时失败。
* **✅ 应对策略**
* **前端队列**前端上传组件必须实现队列机制Queueing严格控制 concurrency: 3。
### **7.2 Runtime 风险 (Application Layer)**
#### **风险 3Node.js 流背压 (Stream Backpressure)**
* **场景**:使用 PassThrough 分流时,用户网速极快 (5G),但 Python 微服务处理较慢。
* **隐患**Node.js 会在内存中缓存积压的数据。如果文件很大且 Python 处理很慢,可能导致 Node.js 内存飙升 (OOM)。
* **✅ 应对策略**
* **流式接收**:确保 Python 微服务支持流式接收 (StreamingResponse),而不是等接收完整个 Body 再处理。
* **熔断机制**:如果 Python 响应超时Node.js 应主动切断该分支但不影响主上传流OSS
#### **风险 4大文件内存溢出 (OOM)**
* **场景**:开发者误用了 await part.toBuffer()。
* **后果**:大文件直接进堆内存,服务卡死。
* **✅ 应对策略**
* **Code Review**:严禁在上传接口使用 toBuffer(),必须使用 pipe。
### **7.3 注意事项
我们统一用 Node.js 的 Stream Pipeline水管直接接通 OSS 内网,数据不进内存。代码我已经让人整理好了,就参照 common/storage 模块写,这套逻辑全平台复用。”
这 3 条红线,请遵守:
红线 1SAE 的环境变量 OSS_ENDPOINT 必须填内网地址 (-internal),否则流量费会让你肉疼。
红线 2后端代码里严禁出现 await file.toBuffer() 这种写法,必须用 stream.pipeline (新流式)。这是 2 人团队服务器不崩的关键。
红线 3ai-clinical-data Bucket 绝对不能开公有读,不管测试有多方便都不行。