服务边界
CONTRACT.md §一 · 所有对外请求都经过下载器;下载器拉,不被推
- 下载器── lease ───▶调度器租用 URL,批量渲染
- 下载器── report ──▶调度器回报结果,带 health_signal
- 调度器── probe ───▶下载器域名健康探针,不入库
- 采样器── render ──▶下载器取页面,渲染或原始响应
- 下载器╌╌ hints ╌╌▶采样器接口逆向线索,v2 才启用
⚠️ 采样器与调度器都不直连目标站点。域名限速需要单点保证 —— 新域名首次判定若走直连,是 7 次以上的集中探测,封禁往往就发生在这里。
近 24 小时
来源 downloader:fetch_result:{url_hash} 汇总 · 唯一写入方 下载器
- 渲染页数
- 372,140
- 渲染吞吐
- 4.3 页/秒
- in_flight_count
- 37
- render_duration_ms
- 780 / P95 2,410
- 网页库对象数
- 8,412,663
- 网页库占用
- 641 GB
⚠️ 瓶颈在渲染 CPU,不在队列供给。当前 4 核,理论上限 3~5 页/秒;扩容加机器即可,每台用不同 worker_id 去 lease,调度器自动分配。max_concurrent_count 是唯一的成本刹车。
三个入口
DEV-downloader-v1.md §三 · 入口 A、B 写入同一网页库,入口 C 不入库
| 入口 | 路径 | 模式 | 入库 | 限速行为 | 24h 调用 | 状态 |
|---|---|---|---|---|---|---|
| A · lease 循环 | GET scheduling-url/v1/lease | 批量异步 | 是 | 超限延迟执行 | 365,222 | 运行中 |
| B · 取页 | POST /v1/render | 同步 | 依 should_store | 超限返回 429 + retry_after_seconds | 6,918 | 运行中 |
| C · 健康探针 | POST /v1/probe | 同步 | 否 | 超限返回 has_reached_origin = false | 12,405 | 运行中 |
⚠️ 入口 B、C 不经过调度器的出队路径,本服务的域名限速不是重复限速,是堵住这条绕过路径。downloader:domain_runtime 与 scheduler:domain_runtime 是两个独立计数器,故意重复,不同步。
⚠️ 入口 C 内部强制 should_store = false,且只返回精简结构 —— 返回完整网页对象会把调度器与网页对象 schema 绑死。
抓取结果分布
downloader:fetch_result:{url_hash} · fetch_status 四值,与调度器的 schedule_status 是两套枚举
| fetch_status | 页数 | 占比 | 占比 | 含义 |
|---|---|---|---|---|
| done | 318,904 | 85.7% | 渲染成功,对象已落库 | |
| not_modified | 24,190 | 6.5% | 条件请求命中 304,跳过渲染,统计字段全为 null | |
| empty | 21,588 | 5.8% | 渲染成功但正文为空,对象已落库,下游扫描应跳过 | |
| failed | 7,458 | 2.0% | 渲染失败,无对象 |
⚠️ empty 独立于 failed,not_modified 独立于 done。四值合并任意两个,下游会消费到空壳,调度器会把 304 当成 text_length 骤降并触发误报复核。
失败率 2.0% —— 按 DEV-downloader-v2.md §三,低于 5% 不做 Chrome 兜底,渲染失败 report failed 即可。该结论以门槛二的完整数据为准。
最近抓取
网页对象摘要 + 随 report 回传的 health_signal · 完整对象见网页库
| url | http_status | fetch_status | render_duration_ms | raw_text_length | text_length | xhr_error / total | has_console_error | object_id |
|---|---|---|---|---|---|---|---|---|
| https://news.example.tw/post/48213 | 200 | done | 812 | 180 | 3,456 | 0 / 12 | false | 7f2a9c… |
| https://docs.example.cn/guide/5 | 200 | done | 640 | 210 | 5,120 | 0 / 9 | false | b1d40e… |
| https://shop.example.jp/item/9910 | 200 | empty | 1,940 | 96 | 0 | 3 / 18 | true | 2c77af… |
| https://blog.example.com/p/771 | 304 | not_modified | null | null | null | null | null | null |
| https://m.example.hk/article/22 | 502 | failed | null | null | null | null | null | null |
⚠️ not_modified 与 failed 两行的 health_signal 整体为 null,不是零。把 null 当成「零个错误 XHR」,会把一个后端全挂的域名判成健康。
域名限速
downloader:domain_runtime:{domain} · 唯一写入方 下载器 · 与调度器的同名表不同步
| domain | min_interval_ms | 来源 | last_request_at | in_flight_count | 状态 |
|---|---|---|---|---|---|
| news.example.tw | 1,000 | lease 消息 | 2026-08-15T07:47:52Z | 3 | 正常 |
| shop.example.jp | 2,000 | lease 消息 | 2026-08-15T07:47:51Z | 1 | 正常 |
| docs.example.cn | 1,000 | lease 消息 | 2026-08-15T07:47:49Z | 2 | 正常 |
| blog.example.com | 1,000 | 全局默认 | 2026-08-15T07:47:44Z | 0 | 入口 B 超限 |
⚠️ min_interval_ms 由采样器算好(已取 crawl_delay 与语种默认值的较大者),本服务直接用,禁止再取一次 max。入口 B 无此值时才落到全局默认。
上线门槛
DEV-downloader-v1.md §九 · 两项都未通过前不进入正式抓取
| 门槛 | 验证内容 | 通过判据 | 状态 |
|---|---|---|---|
| 一 · Obscura 懒加载 | 四语种各 5 个已知懒加载站,比对 Obscura 与 Chrome 的正文字数与列表页链接数 | 差距 < 5% 按现方案开发;> 10% 则 v1 就要接 Stylo 或改 Chrome | 待验证 |
| 二 · 渲染稳定性 | 四语种各 50 个已知 CSR 页面连续渲染 4 小时,记内存曲线、失败率与类型分布、P50 / P95 | 失败率数据决定 v2 要不要做 Chrome 兜底 | 进行中 |
⚠️ 门槛一的第 3 项最关键:正文残缺只影响单页,列表残缺影响整棵子树的链接图谱。它同时决定吞吐系数 —— 若要接 Stylo 或改 Chrome,同样核数的吞吐降三到四成。