12 KiB
HTML转PDF服务 - 正式版设计方案(v2.1 - 补充版)
文档版本:v2.1(在 v2.0 基础上补充若干工程级要点) 创建日期:2025-12-10 说明:本文件基于原 v2.0 文档(MVP→正式版设计)。此次 v2.1 主要在生产稳定性、队列与任务自愈、幂等性、回调安全与可重放、队列分离、以及预检(dry-run)能力上补充内容。按用户要求未增加“文件加密 + 授权下载”相关内容。
0 概览(本次补充要点)
本次 v2.1 补充的重点如下(已在文档相关位置直接加入实现建议、接口和配置示例):
- 浏览器池自愈与进程隔离策略(BrowserPool 自愈)
- 新增任务状态
Stalled(卡死)并补充自动恢复策略 - 幂等性(Idempotency-Key)支持与重复请求处理
- 回调签名(HMAC)以及增强的回调日志与回放(Replay)接口
- 支持 Webhook 重放接口(/callback/replay)与回调审计
- 多队列隔离:fast-queue / slow-queue 设计与调度策略
- 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 操作出现
BrowserDisconnected、NavigationTimeout等可恢复错误 - 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-queue、slow-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:各实例 RSS(histogram)idempotency_hits_total:相同 Idempotency-Key 命中次数callback_replay_total:回放次数queue_fast_length、queue_slow_length:队列长度实时数
8 数据库/Redis 结构补充
8.1 Redis:Idempotency 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支持Stalledidempotency_key可选字段suggested_queue(fast/normal/slow)
9 操作手册补充(运维/排障)
9.1 发现大量 Stalled 任务时的快速排查步骤
- 查看
/metrics中conversion_tasks_stalled_total与task_queue_length。 - 检查 BrowserPool 健康:
browser_restart_total是否异常上升;查看内存占用。 - 抓取一条 Stalled task 的最近日志(conversion_logs),查看是否为
NavigationTimeout、BrowserDisconnected。 - 若为外部 URL 引起,建议通过 Dry-run 检查目标可达性;将该 URL 加入白名单/黑名单策略。
- 若为 Browser 崩溃,重启服务或单个浏览器实例,并将受影响任务重新入队(或由系统自动重试)。
9.2 回调回放(Replay)常用流程
- 在管理后台选择某个 task,查看 callback logs,确认最后一次成功/失败原因。
- 点击
Replay,选择 replay 次数与退避策略。 - 系统会返回 replay 执行结果并写入审计日志。
10 迁移与兼容性(与原 v2.0 同步说明)
Stalled是向后兼容的新增状态:旧客户端不受影响;管理后台/监控面板需升级以展示该状态。- Idempotency-Key 为可选 header;客户端可逐步接入以避免重复任务。
- Dry-run 为新可选接口,不改变原有流量路径。
11 开发计划(补充分配)
建议把本次补充拆成小步落地:
- Phase A(1周):BrowserPool 自愈 + Stalled 状态检测与自动重试
- Phase B(1周):Idempotency 支持 + Redis 存储策略
- Phase C(0.5周):回调签名 + 回放接口 + 审计日志
- Phase D(0.5周):Fast/Slow 队列实现(小规模验证)
- Phase E(1周):Dry-run 接口实现 + 集成测试
12 结语
本次 v2.1 补充重点聚焦工程可用性与生产自愈能力,目标是让系统在长期高并发运行下稳定可观测、易于排查与运维。若你同意,我可以把这些补充直接合并回原文档内指定章节(例如将 BrowserPool 补充放到 5.x 节,将 Stalled 放到 3.1 状态机处,等等),并生成带变更标注(diff)的版本,或输出一份可直接提交给开发的任务清单(Jira/Issue 模板)。
注:按照你的要求,本版本未加入“文件加密 + 授权下载”相关内容。如需未来再加,可在安全设计章节追加详细方案。