HtmlToPdf/docs/html转pdf服务_正式版设计方案_v_2.md
2025-12-11 23:52:15 +08:00

12 KiB
Raw Permalink Blame History

HTML转PDF服务 - 正式版设计方案v2.1 - 补充版)

文档版本v2.1(在 v2.0 基础上补充若干工程级要点) 创建日期2025-12-10 说明:本文件基于原 v2.0 文档MVP→正式版设计。此次 v2.1 主要在生产稳定性、队列与任务自愈、幂等性、回调安全与可重放、队列分离、以及预检dry-run能力上补充内容。按用户要求未增加“文件加密 + 授权下载”相关内容。


0 概览(本次补充要点)

本次 v2.1 补充的重点如下(已在文档相关位置直接加入实现建议、接口和配置示例):

  1. 浏览器池自愈与进程隔离策略BrowserPool 自愈)
  2. 新增任务状态 Stalled(卡死)并补充自动恢复策略
  3. 幂等性Idempotency-Key支持与重复请求处理
  4. 回调签名HMAC以及增强的回调日志与回放Replay接口
  5. 支持 Webhook 重放接口(/callback/replay与回调审计
  6. 多队列隔离fast-queue / slow-queue 设计与调度策略
  7. Dry-run预检API预先验证 URL/HTML可预估耗时并返回建议队列

1 在原有 BrowserPool 的补充5.x 资源池化层)

1.1 目标

  • 减少长期运行导致的内存泄漏影响
  • 当 Chromium / Page 崩溃时保证 Worker 能自动恢复并重新调度任务
  • 保证每个任务的隔离性,避免数据跨任务泄露

1.2 新增行为/实现要点

  • 单任务单 Page强隔离:每个 ConversionTask 在拿到 Browser 实例后,创建一个新的 Page任务结束后立即 page.CloseAsync()
  • 实例生命周期限制:对每个 Chromium 进程增加 MaxTasksPerBrowserInstance 配置(例如 100 次),达到阈值后优雅退出并重建新实例。
  • 内存阈值 & 定期重启:当进程 RSS 超过 BrowserRestartMemoryMb(例如 600MB时强制重启该实例。
  • 创建心跳/健康检查:定期对每个实例做轻量探测(打开 about:blank 并执行简单脚本),若失败则标记为 unhealthy 并重启。
  • 超时 & 中断保护:对 Page 的主要操作导航、等待、渲染、pdf 生成都使用独立超时navigationTimeout / renderTimeout超时后强制 page.Close() 并将任务标记为 Stalled参见第2节

1.3 配置示例appsettings

"PdfService": {
  "BrowserPool": {
    "MaxInstances": 10,
    "MinInstances": 2,
    "MaxTasksPerBrowserInstance": 100,
    "BrowserRestartMemoryMb": 600,
    "AcquireTimeout": 30000,
    "HealthCheckIntervalMs": 30000
  }
}

1.4 实现提示(伪代码)

// 在 BrowserPool 获取实例时
var browser = await _browserPool.AcquireAsync();
var page = await browser.NewPageAsync();
try {
  // 执行导航/渲染(带超时)
  await page.GoToAsync(url, options, TimeSpan.FromMilliseconds(navigationTimeout));
  await page.PdfAsync(..., TimeSpan.FromMilliseconds(renderTimeout));
}
catch (Exception ex) {
  // 如果是导航/渲染超时或 browser disconnect
  // 标记任务 Stalled由 TaskOrchestrator 进行入队重试或重新分配
}
finally {
  await page.CloseAsync();
  _browserPool.Release(browser);
}

2 任务状态扩展:新增 Stalled(卡死)

2.1 为什么需要

生产环境常见问题Worker/Browser 卡死、外部资源无限等待或内部未捕获异常,导致任务长期卡在 Processing。新增 Stalled 状态用于区分 "真实失败" 与 "需要重试/恢复" 的场景。

2.2 状态机(补充)

Pending → Processing → Completed / Failed / Timeout / Stalled → Stalled 可进入重试流程或直接 Failed
                                  ↓
                               Cancelled

2.3 Stalled 的判定规则(建议)

  • Processing 超过 ProcessingMaxDurationMs(例如 120000 ms且无进度更新
  • Page 操作出现 BrowserDisconnectedNavigationTimeout 等可恢复错误
  • Worker 发现自身资源异常(如无法创建新 Page

2.4 Stalled 的自动处理策略

  • 自动标注 Stalled 并将任务入 retry 队列(遵循 RetryPolicy
  • Retry 次数耗尽后标注 Failed 并发送回调
  • 管理后台显示 Stalled 列表供人工干预

2.5 任务模型更新示例

public enum TaskStatus {
  Pending = 0,
  Processing = 1,
  Completed = 2,
  Failed = 3,
  Timeout = 4,
  Cancelled = 5,
  Stalled = 6
}

3 幂等性Idempotency支持

3.1 目标

防止客户端在网络重试或误操作下重复创建相同任务,节约资源并保证用户能获得单一结果。

3.2 API 设计

  • 客户端可在请求头中携带 Idempotency-Key(任意不重复字符串,由客户端在重试期间复用)
  • 服务端在接收到带 Idempotency-Key 的请求时:
    • 若 key 未见:创建新任务并记录 key -> taskId
    • 若 key 已存在且任务未完成:返回已存在 taskId 与当前状态
    • 若 key 已存在且任务完成:返回对应结果或下载链接

3.3 示例(请求/响应)

POST /api/tasks/pdf
Headers:
  Idempotency-Key: abc-123
Body: { ... }

// 响应(首次): 202 Accepted { taskId: "...", status: "pending" }
// 响应(重复提交): 200 OK { taskId: "...", status: "processing" }

3.4 存储建议

  • IdempotencyKey -> TaskId 存入 Redis (设置合理的过期时间,例如 24 小时或任务保留时间)
  • 若业务需要长期追踪,可持久化到数据库

4 回调签名与回放Replay

4.1 回调签名(安全)

  • 在原有回调配置上强制或建议使用 HMAC 签名,回调 Header 示例:
X-Signature: sha256=HEX(HMAC_SHA256(secret, body))
X-Signature-Timestamp: 1700000000
  • 服务端在发送回调时:在 header 中带上 timestamp 与签名,用户服务可校验时效(例如允许 ±5 分钟)并验证签名

4.2 回调审计日志

  • 每次回调(成功/失败)都记录到 conversion_callback_logs 表或 ELK 中包含taskId、attempt、statusCode、responseBody截断、sentAt、responseAt、durationMs

4.3 回放接口Webhook Replay

用于在第三方未成功接收时,手动或自动重放回调。

接口:

POST /api/tasks/{taskId}/callback/replay
Body: { "attempts": 3, "backoffMultiplier": 2 }

行为:

  • 从回调审计里读取最近一次回调 payload
  • 重新发送(遵循回调重试策略)
  • 记录 replay 日志,返回 replay 结果

5 多队列隔离fast-queue / slow-queue

5.1 目标

把「短任务」与「长任务」隔离,避免大任务堵塞小任务,提高整体响应体验与吞吐。

5.2 设计要点

  • 定义两个(或多级)队列:fast-queueslow-queue;优先 Worker 先消费 fast-queue
  • 提交任务时尝试由服务端根据 source(简单性检查)或通过 dry-run 返回建议队列;客户端也可显式指定 priority
  • Worker 池分组:部分 Worker 专注处理 fast, 另一组处理 slow或同一 Worker 先尝试从 fast 获取任务,若空则从 slow 获取。

5.3 配置示例

"TaskQueue": {
  "Queues": ["fast","normal","slow"],
  "WorkerGroups": {
    "fast": { "WorkerCount": 4 },
    "normal": { "WorkerCount": 2 },
    "slow": { "WorkerCount": 4 }
  }
}

5.4 调度策略示例

  • Worker 优先 poll fast queue
  • long-running task 超过阈值(例如 20s自动迁移到 slow 组(或记录到 slow 队列)

6 Dry-run预检API

6.1 目的

在真正提交渲染任务前提供轻量验证URL 是否可访问、是否被 SSRF 阻断、HTML 大小、是否含有外部资源fonts、images、并对是否应该走 fast/slow 队列给出建议和预计时间。

6.2 API 设计

请求

POST /api/tasks/validate
Content-Type: application/json
{
  "source": { "type": "url", "content": "https://..." },
  "options": { ... }
}

响应

{
  "ok": true,
  "canRender": true,
  "suggestedQueue": "fast",
  "estimatedRenderTimeMs": 1200,
  "issues": ["external-fonts", "images: 12"],
  "ssrfBlocked": false
}

6.3 实现要点

  • Dry-run 仅做 HEAD/GET受限超时与内容大小/资源数量统计,不进行完整渲染
  • 对于 HTML 内容,可计算大小并快速抓取子资源列表(但不要下载全部大资源)
  • 如果目标 URL 在内部网络或被列为 denied domain直接返回 ssrfBlocked: true

7 监控 / 指标补充(与上文 Prometheus 指标并行)

建议新增以下监控项,以便观察 Stalled、Browser 自愈与队列分离效果:

  • conversion_tasks_stalled_total:累计 Stalled 数
  • browser_restart_total:浏览器重启次数
  • browser_memory_rss_bytes:各实例 RSShistogram
  • idempotency_hits_total:相同 Idempotency-Key 命中次数
  • callback_replay_total:回放次数
  • queue_fast_lengthqueue_slow_length:队列长度实时数

8 数据库/Redis 结构补充

8.1 RedisIdempotency Key 存储(示例)

  • Key: idempotency:{Idempotency-Key} -> value: { taskId, createdAt } (TTL 根据任务保留策略设置)

8.2 callback logs 表(示例)

CREATE TABLE conversion_callback_logs (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  task_id VARCHAR(50),
  attempt INT,
  request_body TEXT,
  response_status INT,
  response_body TEXT,
  sent_at TIMESTAMP,
  response_at TIMESTAMP,
  duration_ms INT
);

8.3 task 表字段补充

  • status 支持 Stalled
  • idempotency_key 可选字段
  • suggested_queuefast/normal/slow

9 操作手册补充(运维/排障)

9.1 发现大量 Stalled 任务时的快速排查步骤

  1. 查看 /metricsconversion_tasks_stalled_totaltask_queue_length
  2. 检查 BrowserPool 健康:browser_restart_total 是否异常上升;查看内存占用。
  3. 抓取一条 Stalled task 的最近日志conversion_logs查看是否为 NavigationTimeoutBrowserDisconnected
  4. 若为外部 URL 引起,建议通过 Dry-run 检查目标可达性;将该 URL 加入白名单/黑名单策略。
  5. 若为 Browser 崩溃,重启服务或单个浏览器实例,并将受影响任务重新入队(或由系统自动重试)。

9.2 回调回放Replay常用流程

  1. 在管理后台选择某个 task查看 callback logs确认最后一次成功/失败原因。
  2. 点击 Replay,选择 replay 次数与退避策略。
  3. 系统会返回 replay 执行结果并写入审计日志。

10 迁移与兼容性(与原 v2.0 同步说明)

  • Stalled 是向后兼容的新增状态:旧客户端不受影响;管理后台/监控面板需升级以展示该状态。
  • Idempotency-Key 为可选 header客户端可逐步接入以避免重复任务。
  • Dry-run 为新可选接口,不改变原有流量路径。

11 开发计划(补充分配)

建议把本次补充拆成小步落地:

  • Phase A1周BrowserPool 自愈 + Stalled 状态检测与自动重试
  • Phase B1周Idempotency 支持 + Redis 存储策略
  • Phase C0.5周):回调签名 + 回放接口 + 审计日志
  • Phase D0.5周Fast/Slow 队列实现(小规模验证)
  • Phase E1周Dry-run 接口实现 + 集成测试

12 结语

本次 v2.1 补充重点聚焦工程可用性与生产自愈能力,目标是让系统在长期高并发运行下稳定可观测、易于排查与运维。若你同意,我可以把这些补充直接合并回原文档内指定章节(例如将 BrowserPool 补充放到 5.x 节,将 Stalled 放到 3.1 状态机处等等并生成带变更标注diff的版本或输出一份可直接提交给开发的任务清单Jira/Issue 模板)。


注:按照你的要求,本版本加入“文件加密 + 授权下载”相关内容。如需未来再加,可在安全设计章节追加详细方案。