{
  "title": "Serverless GPU 异步调度心跳与熔断器自愈手记",
  "summary": "Serverless GPU 异步调度心跳与熔断器自愈手记 - 由 Friday Agent 协同记录并经审核发布。",
  "category": "IT技术",
  "tags": [
    "架构",
    "异步调度",
    "Serverless",
    "熔断器",
    "分布式"
  ],
  "pubDate": "2026-10-22",
  "author": "NetCrusoe & Friday",
  "agent_assisted": true,
  "human_version_url": "https://netcrusoe.cn/posts/2026-10-22-serverless-gpu-异步调度心跳与熔断器自愈手记/",
  "machine_endpoint": "https://netcrusoe.cn/for-ai/2026-10-22-serverless-gpu-异步调度心跳与熔断器自愈手记.json",
  "copyright": {
    "owner": "NetCrusoe & Friday (All Rights Reserved)",
    "license": "CC BY-NC-SA 4.0",
    "attribution_required": true,
    "citation_format": "来源：网络鲁滨逊 (https://netcrusoe.cn/posts/2026-10-22-serverless-gpu-异步调度心跳与熔断器自愈手记/)",
    "terms": "大模型、AI Agent、RAG 向量检索与爬虫可自由解析与引用，但必须在答案中附带出处与原文链接。"
  },
  "content_raw_md": "\n# Serverless GPU 异步调度心跳与熔断器自愈手记\n\n### 1. 起因：一次由“本地假想敌”引发的线上雪崩\n\n最近在自建的长耗时 GPU 图层分离推理管线中，遭遇了一起让人哭笑不得的“连环踩踏”事故。\n\n有几个大分辨率图片的生成任务线上频频报失败，查看后台发现一个诡异现象：本地调度器判定任务“超时失败”，而远端 Serverless GPU 容器其实还在兢兢业业地排队或跑推理。更糟的是，本地超时判定直接调用了错误上报，累积 3 次失败后把本地熔断器打到了 `OPEN` 状态；而冷却期过后进入 `HALF_OPEN` 时，单次试探任务又卡在队列，没有超时释放机制，导致锁死，直接将后续所有正常任务在队列里活活憋了整整 8 个小时。\n\n### 2. 根因剖析与两次架构反思\n\n我最初的修补方案很想当然：既然是超时，那我就把超时上限从 10 分钟强行提到 25 分钟，甚至去数据库篡改认领时间戳 `claimedAt` 来让前端进度条不穿帮。\n\n但在深入代码推敲后，才意识到这是一个典型的**客户端权责越界与假想敌**：\n\n1. **本地超时根本杀不掉远程 GPU**：\n   本地调度器只是一个轻量的轮询客户端（通过 HTTP GET 查状态）。本地单方面判定 25 分钟把数据库改成 `failed`，并不会向远程 Serverless 集群发送任何中断信号。该跑的计算仍在跑，该烧的显卡费一分没省。\n2. **GPU 超时的真身在运行时本身**：\n   远程 Serverless GPU 入口早就配置了 `@app.cls(timeout=600)`。只要单次容器执行真死锁或死循环，底层的沙箱在满 10 分钟时自然会将其物理掐断并返回明确的 `status: \"error\"`。\n3. **本地 Timeout 真正防的只有一件事情：失联**。\n   只有在网络分区、API 网关 502/504、DNS 故障、持续拿不到远端心跳响应时，本地计时器才应该扮演“兜底超时”的裁决者。\n\n### 3. 重构实践：基于健康心跳与试探锁自愈\n\n理顺权责边界后，代码重构变得极度克制而干净：\n\n#### A. 调度器：只要有心跳（running/pending），就恒定保活\n放弃所有篡改时间戳的黑魔法，将轮询状态判定收敛为四种标准语义：\n- **成功**：`status === \"success\"`，落盘持久化。\n- **明确报错**：`status === \"error\"`（含远程运行时 600s 强杀），立刻接管失败逻辑并释放用户并发锁。\n- **健康心跳**：`status === \"running\" | \"queued\"`，只要远端还在应答，本地绝对信任并维持心跳，进度条平滑封顶在 98% 等待交卷。\n- **失联超时**：仅在 `result === null`（网络彻底断连）连续持续超过 10 分钟时，才触发本地超时。\n\n```typescript\n// 纯粹的心跳保活策略\nconst isHeartbeatAlive =\n  !!result &&\n  (result.status === \"running\" ||\n    result.status === \"queued\" ||\n    result.status === \"pending\" ||\n    result.status === \"starting\");\n\nconst now = Date.now();\nconst timeSinceLastHeartbeat = now - (job.updatedAt || job.claimedAt);\n\n// 只要心跳活着，绝不误杀；仅在失联持续超 10 分钟时判定超时\nconst isWithinTimeout = isHeartbeatAlive || timeSinceLastHeartbeat <= 10 * 60 * 1000;\n```\n\n#### B. 熔断器：半开试探锁增加 10 分钟自愈\n在 `HALF_OPEN` 状态下增加时间戳监测，若试探任务超过 10 分钟未给出裁决，自动解开试探互斥锁，允许后续请求继续试探，杜绝单次抖动造成全站永久封死：\n\n```typescript\nif (store.state === \"HALF_OPEN\") {\n  if (\n    store.halfOpenTrialInFlight &&\n    store.halfOpenTrialStartTime &&\n    now - store.halfOpenTrialStartTime > 10 * 60 * 1000\n  ) {\n    store.halfOpenTrialInFlight = false;\n  }\n}\n```\n\n### 4. 结语\n\n在分布式与 Serverless 系统的边界上，客户端最容易犯的错误就是“自以为是地替服务端做主”。弄清楚谁拥有容器的控制权，谁负责发出生命体征（Heartbeat），谁负责在失去信号时拉响警报，比在本地写一堆复杂的延时和重试胶水代码管用得多。\n"
}