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

12 KiB
Raw Permalink Blame History

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*
    • 暴露 HeadersETag, 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) 的文件,前端如何访问?

  • APIGET /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 绝对不能开公有读,不管测试有多方便都不行。