12 KiB
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 关键配置规范 (针对后端流式优化)
- 服务端加密 (Encryption):所有 Data Bucket 必须开启 SSE-OSS (AES256)。
- 跨域 (CORS) - 仅需读权限:
- 场景:仅用于前端直接通过 URL (签名或公开) 预览/下载文件。上传不需要 CORS。
- 来源:http://localhost:3000 (Dev), https://你的生产域名.com (Prod)
- 允许 Methods:仅需 GET, HEAD。 (严禁开启 PUT/POST/DELETE)
- 允许 Headers:*
- 暴露 Headers:ETag, x-oss-request-id
- 内网加速:
- 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)
适用于只需存储、后续异步处理的场景。
流程:
- 前端:FormData POST 文件到后端。
- 后端:
- 使用 @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 只能被消费一次。如果发给了 OSS,Python 微服务就拿不到了。 解决:使用 Node.js 的 PassThrough 创建流的分支。
流程:
- 前端:上传文件。
- 后端 (Node.js):
- 创建两个 PassThrough 流:streamA (给 OSS), streamB (给 Python)。
- 源流 .pipe(streamA), 源流 .pipe(streamB)。
- 并行执行:Promise.all([ 上传OSS(streamA), 调用Python(streamB) ])。
- 结果:文件存下了,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)
风险 3:Node.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 条红线,请遵守:
红线 1:SAE 的环境变量 OSS_ENDPOINT 必须填内网地址 (-internal),否则流量费会让你肉疼。
红线 2:后端代码里严禁出现 await file.toBuffer() 这种写法,必须用 stream.pipeline (新流式)。这是 2 人团队服务器不崩的关键。
红线 3:ai-clinical-data Bucket 绝对不能开公有读,不管测试有多方便都不行。