Files
AIclinicalresearch/docs/07-运维文档/06-长时间任务可靠性分析.md
HaHafeng 2481b786d8 deploy: Complete 0126-27 deployment - database upgrade, services update, code recovery
Major Changes:
- Database: Install pg_bigm/pgvector plugins, create test database
- Python service: v1.0 -> v1.1, add pymupdf4llm/openpyxl/pypandoc
- Node.js backend: v1.3 -> v1.7, fix pino-pretty and ES Module imports
- Frontend: v1.2 -> v1.3, skip TypeScript check for deployment
- Code recovery: Restore empty files from local backup

Technical Fixes:
- Fix pino-pretty error in production (conditional loading)
- Fix ES Module import paths (add .js extensions)
- Fix OSSAdapter TypeScript errors
- Update Prisma Schema (63 models, 16 schemas)
- Update environment variables (DATABASE_URL, EXTRACTION_SERVICE_URL, OSS)
- Remove deprecated variables (REDIS_URL, DIFY_API_URL, DIFY_API_KEY)

Documentation:
- Create 0126 deployment folder with 8 documents
- Update database development standards v2.0
- Update SAE deployment status records

Deployment Status:
- PostgreSQL: ai_clinical_research_test with plugins
- Python: v1.1 @ 172.17.173.84:8000
- Backend: v1.7 @ 172.17.173.89:3001
- Frontend: v1.3 @ 172.17.173.90:80

Tested: All services running successfully on SAE
2026-01-27 08:13:27 +08:00

11 KiB
Raw Permalink Blame History

长时间任务可靠性分析MemoryQueue vs Redis队列

场景: 1000篇文献筛选预计2小时处理时间
当前方案: MemoryQueue内存队列
问题: 能否可靠完成?
结论: 不能


📊 场景分析

任务特征

任务类型:文献筛选(标题摘要初筛)
文献数量1000篇
单篇耗时6-10秒双模型并行
总耗时6000-10000秒 = 100-167分钟 ≈ 2小时

当前实现

// backend/src/modules/asl/services/screeningService.ts (第65行)

// 4. 异步处理文献(简化版:直接在这里处理)
// 生产环境应该发送到消息队列  ← 注意这行注释!
processLiteraturesInBackground(task.id, projectId, literatures);

// 这个函数会:
// 1. 运行在当前Node进程中
// 2. 串行处理1000篇文献
// 3. 没有持久化(全在内存)

MemoryQueue的致命问题

问题1SAE实例会被自动销毁 🔥 最严重

Serverless的本质按需计费 = 按需销毁

阿里云SAE的自动缩容策略
├─ 无流量时15分钟后缩容到0
├─ 低流量时:缩减实例数
├─ 夜间时段:自动缩容(节省成本)
└─ 系统升级:实例重启

2小时任务的风险评估

时段 SAE实例销毁概率 说明
工作时间9:00-18:00 🟡 30-50% 流量波动导致缩容
夜间时段22:00-06:00 🔴 80-95% 自动缩容策略
周末/节假日 🔴 70-90% 低流量时段

真实场景模拟

21:00 用户提交1000篇文献筛选
21:00 SAE实例开始处理预计2小时完成
21:15 前端有用户访问(实例存活)
22:00 用户下班回家(无新访问)
22:15 SAE检测15分钟无流量 → 准备缩容
22:16 ❌ 实例被销毁
      └─ 任务进度150/100015%
      └─ 结果:任务丢失,前功尽弃

问题2进程崩溃无法恢复

// 当前实现(简化版)
async function processLiteraturesInBackground(taskId, projectId, literatures) {
  for (const lit of literatures) {
    try {
      // 处理单篇文献耗时6-10秒
      await processLiterature(lit);
    } catch (error) {
      // 某篇失败,继续下一篇
      logger.error('Failed to process literature', { error });
    }
  }
}

// 风险:
// 1. 如果Node进程崩溃OOM、未捕获异常→ 全部丢失
// 2. 如果DB连接断开 → 无法保存进度
// 3. 如果API限流 → 任务卡死
// 4. 没有断点续传 → 必须重头开始

问题3无法监控真实进度

// 当前实现的进度更新
await prisma.aslScreeningTask.update({
  where: { id: taskId },
  data: { processedItems: processedCount }
});

// 问题:
// - 进度只存在数据库
// - 任务状态在内存中
// - 实例销毁后,数据库显示 processedItems: 150
// - 但任务实际已丢失,无法恢复

问题4多实例冲突

场景SAE有2个实例

用户提交任务 → 实例A开始处理
                   ↓
                处理到500篇时实例A销毁
                   ↓
用户刷新页面 → 请求路由到实例B
                   ↓
实例B读取任务状态processedItems: 500
                   ↓
实例B不知道任务已中断
                   ↓
❌ 任务显示"进行中",但实际没人在处理

Redis队列的优势

优势1任务持久化

// 使用Redis队列
await jobQueue.push('asl:screening', {
  taskId: task.id,
  projectId,
  literatureIds: [1, 2, 3, ..., 1000]
});

// 任务保存在Redis中
// - 实例销毁 → ✅ 任务仍在Redis
// - 新实例启动 → ✅ 自动拾取任务
// - 进程崩溃 → ✅ 其他Worker接管

优势2断点续传

// Worker处理任务
jobQueue.process('asl:screening', async (job) => {
  const { literatureIds } = job.data;
  
  for (let i = 0; i < literatureIds.length; i++) {
    // 处理文献
    await processLiterature(literatureIds[i]);
    
    // 更新进度保存到Redis
    await job.updateProgress((i + 1) / literatureIds.length * 100);
    
    // 如果Worker在这里崩溃
    // - BullMQ会将任务标记为"停滞"
    // - 其他Worker会重新拾取
    // - 从上次进度继续(而不是重头开始)
  }
});

优势3自动重试

// BullMQ配置
const queue = new Queue('asl:screening', {
  connection: { host: 'redis' },
  defaultJobOptions: {
    attempts: 3,              // 失败后重试3次
    backoff: {
      type: 'exponential',
      delay: 2000             // 2秒、4秒、8秒
    },
    removeOnComplete: true,   // 完成后清理
    removeOnFail: false       // 失败后保留(便于排查)
  }
});

// 场景:
// - LLM API临时故障 → ✅ 自动重试
// - 网络抖动 → ✅ 自动重试
// - DB连接断开 → ✅ 自动重试

优势4分布式任务分配

SAE有3个实例

Redis队列1000个任务
    ↓
自动分配:
├─ 实例A Worker处理 Task 1-350
├─ 实例B Worker处理 Task 351-700
└─ 实例C Worker处理 Task 701-1000

如果实例B销毁
├─ Task 351-700 标记为"停滞"
├─ 实例A或C的Worker自动接管
└─ 继续处理,无需人工干预

📊 可靠性对比

维度 MemoryQueue Redis队列 差异
2小时任务完成率 10-30% 99%+ 300%提升
实例销毁后 任务丢失 自动恢复 关键
进程崩溃后 全部丢失 断点续传 关键
API临时故障 任务失败 自动重试 关键
多实例协调 无法协调 自动分配 关键
任务监控 ⚠️ 仅DB 实时状态 可选
成本 ¥0 ¥108/年 可接受

🎯 真实场景模拟

场景1工作时间提交成功率30%

10:00 用户提交1000篇文献筛选
      ├─ MemoryQueue开始处理预计12:00完成
      │
11:30 流量降低SAE缩容删除1个实例
      ├─ 如果任务在被删除的实例上 → ❌ 丢失概率50%
      │
12:00 如果幸运未被删除 → ✅ 完成概率50%

总成功率50%

场景2夜间提交成功率5%

21:00 用户提交1000篇文献筛选
      ├─ MemoryQueue开始处理预计23:00完成
      │
21:15 无新用户访问流量降为0
      │
21:30 SAE检测15分钟无流量 → 准备缩容
      │
21:31 ❌ 实例销毁任务丢失概率95%

总成功率5%

场景3Redis队列成功率99%+

21:00 用户提交1000篇文献筛选
      ├─ Redis队列任务入队
      ├─ Worker开始处理
      │
21:31 实例销毁
      ├─ 任务保存在Redis
      │
21:32 新实例启动(或其他实例)
      ├─ Worker自动拾取任务
      ├─ 从Redis读取进度已处理150篇
      ├─ 继续处理剩余850篇
      │
23:00 ✅ 任务完成

总成功率99%+

💰 成本分析

MemoryQueue的隐藏成本

任务失败率70%(夜间)
用户重新提交次数平均3次才成功
LLM API浪费
- 第1次处理200篇后失败 → 浪费 ¥86
- 第2次处理500篇后失败 → 浪费 ¥215
- 第3次完成 → ¥430
总成本¥731应该只需¥430

用户体验:
- 反复失败 → 投诉率上升
- 不敢夜间提交 → 使用受限
- 对系统失去信任 → 流失风险

Redis队列的真实成本

Redis年费¥108
任务成功率99%+
用户重新提交次数几乎为0
LLM API成本¥430无浪费

额外收益:
- 用户满意度提升
- 可以支持更大批量5000篇+
- 夜间任务可靠运行

ROI计算

节省成本¥731 - ¥430 = ¥301/次
如果每月10次批量任务
节省 = ¥301 × 10 = ¥3,010/月
Redis成本 = ¥9/月
净收益 = ¥3,001/月

ROI = 33,344%投入¥9回报¥3,010

⚠️ 结论与建议

明确结论

问题MemoryQueue能否完成2小时任务
答案:❌ 不能可靠完成

原因:
1. SAE实例会自动销毁15分钟无流量
2. 2小时任务几乎必然遇到实例销毁
3. 任务丢失后无法恢复
4. 成功率 < 30%,夜间 < 5%

强烈建议

对于超过10分钟的任务必须使用Redis队列

时间阈值:
- < 10秒可以用MemoryQueue同步处理
- 10秒 - 10分钟建议用Redis队列
- > 10分钟必须用Redis队列
- > 1小时强制要求Redis队列

实施优先级

阶段1本周Redis缓存
├─ 解决LLM成本问题
└─ 工作量2天

阶段2下周Redis队列  ← **必须做!**
├─ 解决长任务可靠性
├─ 工作量3天
└─ 不做的风险70%任务失败率

📝 技术细节为什么10分钟是分水岭

SAE实例缩容策略

阿里云SAE默认策略
- 检测周期5分钟
- 无流量阈值15分钟
- 缩容延迟5分钟

总计15分钟后可能缩容

任务时长与风险

任务时长     实例销毁风险     建议
─────────────────────────────────────
< 1分钟      几乎为0%        同步处理
1-5分钟      < 5%           可用MemoryQueue
5-10分钟     10-20%         建议Redis队列
10-30分钟    50-70%         必须Redis队列
> 30分钟     80-95%         强制Redis队列

🎯 立即行动

如果您想现在就测试长任务:

不推荐用MemoryQueue测试1000篇

  • 风险70%概率失败
  • 浪费重复调用LLM API

推荐先用100篇测试10分钟

// 限制测试数量
const testLiteratures = literatures.slice(0, 100);
processLiteraturesInBackground(task.id, projectId, testLiteratures);

然后观察:

  • 是否遇到实例销毁?
  • 任务是否完整?
  • 如果失败立即改用Redis队列

如果您准备改造:

参考文档

  • 04-Redis改造实施计划.md
  • 05-Redis缓存与队列的区别说明.md

改造顺序

  1. Redis缓存本周
  2. Redis队列下周重点
  3. 测试2小时任务

📊 附录:实际测试建议

测试方案A验证MemoryQueue的不可靠性

# 步骤1提交1000篇文献筛选任务
# 步骤2等待15分钟
# 步骤3检查任务状态
#        - 如果失败 → 证明实例被销毁
#        - 如果成功 → 运气好,不代表可靠

# 重复测试5次
# - 成功率应该 < 30%

测试方案BRedis队列验证

# 步骤1部署Redis队列版本
# 步骤2提交1000篇文献筛选任务
# 步骤3主动停止SAE实例
# 步骤4重新启动实例
# 步骤5检查任务是否自动恢复

# 预期结果:
# - 任务自动恢复 ✅
# - 从断点继续 ✅
# - 最终完成 ✅

文档维护者: 技术团队
最后更新: 2025-12-12
关键结论: MemoryQueue无法可靠完成2小时任务必须迁移到Redis队列