本页的边界
CONTRACT.md §一 · CONVENTION.md 第三条
⚠️ 本页只统计下载器自己的请求。重抓周期(next_fetch_at)、优先级、熔断状态、异常信号判定全部属于调度器,CONTRACT.md §一 没有下载器读取它们的边,本页不显示、也不推断。
⚠️ 本页不下任何结论。XHR 比例是否意味着后端挂了(DEV-scheduler.md §9.4)、状态码该不该熔断(§9.3)、域名该不该复核(§五),判定权都在调度器。下载器只算好数字随 report 回传。
抓取频次 · 逐小时
入口 A + 入口 B 的渲染请求 · 2026-08-14T08:00:00Z ~ 2026-08-15T08:00:00Z
08:00Z17:00Z 峰值 18,24006:00Z 谷值 9,84008:00Z
- 24h 合计
- 372,140
- 均值
- 4.3 页/秒
- 峰值小时
- 5.1 页/秒
- 谷值小时
- 2.7 页/秒
瓶颈在哪一侧
DEV-downloader-v1.md §十一 · 域名限速侧上限 vs 渲染侧上限
| 侧 | 算式 | 理论上限 | 说明 |
|---|---|---|---|
| 域名限速侧 | 128,540 域名 × (1 / 1,000ms) | 128,540 页/秒 | 几乎所有域名都早已过了 min_interval_ms,限速根本不触发 |
| 渲染侧 | 4 核 | 3~5 页/秒 | 实际瓶颈。无状态,可水平扩展 |
| 差距 | — | 约 4 个数量级 | 瓶颈彻底转到渲染 —— 正好是架构最想要的形状 |
⚠️ 这个结论在小规模时是反的。早期几百个域名时真实吞吐 ≈ 队列里的活跃域名数,限速才是瓶颈;到 10 万域名规模结论反转。看到本表就意味着已经进入反转后的区间,扩容思路应是加渲染机器,不是调限速。
入口分布与限速触发
downloader:domain_runtime:{domain} · 唯一写入方 下载器 · 近 24 小时
入口调用量
| 入口 | 调用数 | 占比 | 是否入库 |
|---|---|---|---|
| A · lease 循环 | 365,222 | 98.1% | 是 |
| B · POST /v1/render | 6,918 | 1.9% | 依 should_store |
| C · POST /v1/probe | 12,405 | — | 否,不计入渲染量 |
限速触发
| 入口 | 次数 | 超限时的行为 |
|---|---|---|
| A | 1,204 | 延迟执行 |
| B | 96 | 返回 429 + retry_after_seconds |
| C | 41 | 返回 has_reached_origin = false |
⚠️ 入口 C 超限不返回 429,而是 has_reached_origin = false —— 探针被我方自己的限速挡住,不是对方的问题。调度器收到后不退避、不计探针次数。少了这个字段,我方一抖,全库暂停域名的恢复时间集体翻倍。
⚠️ 本表的触发次数来自 downloader:domain_runtime。它与 scheduler:domain_runtime 是两个独立计数器,故意重复,不同步,两边数字对不上是设计如此,不是 bug。本层的作用是堵住入口 B 这条绕过调度器出队路径的通道。
⚠️ min_interval_ms 由采样器算好(已取 crawl_delay 与语种默认值的较大者),本服务直接用,禁止再取一次 max。入口 B 无此值时才落到全局默认。
抓取时间 · render_duration_ms
网页对象 render_duration_ms · 分母 340,492(done + empty),不含 not_modified 与 failed
- P50
- 780 ms
- P95
- 2,410 ms
- 脚本执行预算
- 30,000 ms 默认
- ≥ 10s 的页
- 2,043 0.6%
| render_duration_ms | 页数 | 占比 | 占比 | 说明 |
|---|---|---|---|---|
| < 500 | 71,503 | 21.0% | ||
| 500 ~ 1,000 | 149,816 | 44.0% | 主体,P50 落在此档 | |
| 1,000 ~ 2,000 | 88,528 | 26.0% | ||
| 2,000 ~ 3,000 | 20,430 | 6.0% | P95 落在此档 | |
| 3,000 ~ 10,000 | 8,172 | 2.4% | 重型 SPA | |
| ≥ 10,000 | 2,043 | 0.6% | 长尾坏站,v2 硬超时的主要目标 |
⚠️ 分母只含 done 与 empty。not_modified 与 failed 的统计字段全为 null,把 null 当 0 参与分位数计算,P50 会被拉低约 8%,且看不出是错的。
⚠️ 本页 P95 是 v2 硬超时阈值的唯一依据(DEV-downloader-v2.md §二)。阈值定下来之前,≥10s 那 0.6% 会一直占着 worker —— 长尾里总有坏站,卡 30 秒不值得。
⚠️ 渲染超时不丢弃:记录 console_errors 与当时 DOM 状态,report empty 或 failed。
等待策略与条件请求
DEV-downloader-v1.md §五 · 分母 347,950(进入渲染的页,已扣除 304 跳过的 24,190)
等待策略命中
| 策略 | 页数 | 占比 | 可靠性 |
|---|---|---|---|
| wait_selector | 261,412 | 75.1% | 最可靠,队列消息里带 |
| networkidle0 | 69,590 | 20.0% | 通用,广告埋点会拖长 |
| domcontentloaded + 固定延时 | 16,948 | 4.9% | 兜底 |
条件请求
| 口径 | 值 |
|---|---|
| 带 conditional_request 的任务 | 96,412 |
| 304 命中 | 24,190 |
| 命中率 | 25.1% |
| 省下的渲染 CPU | ≈ 5.2 小时 |
⚠️ wait_selector 由采样器推导、经调度器 lease 下发,是采样器最有价值的产出。命中率下滑意味着采样器推不出选择器的域名变多了,但该不该复核由采样器与调度器决定,本页不触发任何动作。
⚠️ ETag 由调度器从 scheduler:url_result 下发,下载器不查库。304 时跳过渲染,report fetch_status = not_modified,所有统计字段与 health_signal 留空 —— 留空是 null,不是 0,也不是空数组。
抓取异常 · 我方还是对方
判据只有一条:这次请求有没有真的到达对方服务器 · 近 24 小时 failed 合计 7,458
| 归属 | 现象 | 次数 | 占 failed | 计入域名熔断 |
|---|---|---|---|---|
| 我方 | 渲染超时(Obscura) | 2,914 | 39.1% | 否 —— 我方问题,不是站点的 |
| 我方 | Obscura 崩溃 / OOM | 388 | 5.2% | 否 —— 计入 v1 门槛二统计 |
| 对方 | 限速信号(429 / 503 + Retry-After) | 2,059 | 27.6% | 否 —— 不是失败,见下 |
| 对方 | 5xx | 1,134 | 15.2% | 按状态码分,见下 |
| 对方 | 4xx | 752 | 10.1% | 按状态码分,见下 |
| 对方 | 网络层(无状态码) | 211 | 2.8% | 是 |
⚠️ 前两行合计 3,302 次,占 failed 的 44.3%,绝不能计入域名熔断。否则 Obscura 一抖,我们会自己停掉一批健康站,且完全查不出原因 —— 这是状态码处置全表最易错的一处。
⚠️ 入口 B 的 96 次 429 与入口 C 的 41 次 has_reached_origin = false 不在本表:它们是我方限速拒绝,压根没发出请求,不构成一次抓取失败。
对方侧状态码
网页对象 http_status · 「调度器处置」列引用 DEV-scheduler.md §9.3,本服务不执行
| http_status | 次数 | 粒度 | 熔断 | 调度器处置(引用,本服务不执行) |
|---|---|---|---|---|
| 404 | 502 | URL | 否 | 不重试,永久失败。CSR 站真 404 罕见 → 拿到即高置信 |
| 502 | 372 | 域名 | 是 | transient_server_error,网关够不到源站,必然整域 |
| 503 无 Retry-After | 259 | 域名 | 是 | transient_server_error |
| 504 | 236 | 域名 | 是 | transient_server_error |
| 500 | 194 | URL | 否 | 重试一次。单页 bug 极常见,不代表整站挂 |
| 403 | 168 | 域名 | 是 | 连续 3 次暂停,access_denied,起 30 分钟退避到 24 小时 |
| 520 ~ 527 | 55 | 域名 | 是 | Cloudflare 系,521 / 523 是源站挂 |
| 410 | 44 | URL | 否 | 永久失败,并从布隆与队列彻底移除。比 404 更硬 |
| 400 | 21 | URL | 否 | 永久失败并记录告警 —— 大概率是我方规范化 bug |
| 507 / 508 | 18 | 域名 | 是 | transient_server_error,退避加倍 |
| 405 | 9 | URL | 否 | 不重试 |
| 451 | 8 | 域名 | 否 | legal_block,30 天探针。与 jurisdiction 联动,不做永久拉黑 |
⚠️ 500 与 502 / 503 / 504 在本表必须分开看。500 是应用层抛异常,某条 URL 触发了 bug,其他 URL 好好的;按域名熔断会因一条坏 URL 停掉整个域名。
⚠️ 403 是全表唯一一处慢恢复优于快恢复。5xx 是对方自己有问题,多试无害;403 是对方在拒绝我们,每多试一次都在加深它的判断。
⚠️ 全系统 HTTP 状态只有一个名字:http_status。网页对象、report、探针响应、导出项一律相同,曾用的 status_code 已废弃。
限速信号(不是失败)
DEV-scheduler.md §9.5 · 429 与 503 + Retry-After 不进熔断体系 · 近 24 小时
| 情况 | 次数 | 调度器动作(引用,本服务不执行) |
|---|---|---|
| 429,带 retry_after | 1,203 | throttle_until_at = now + Retry-After,严格遵守,不打折 |
| 429,无 retry_after | 639 | throttle_until_at = now + min_interval_ms × 4,指数递增,封顶 10 分钟 |
| 503,带 retry_after | 217 | 严格按头处理 |
⚠️ 三条铁律:不计入 consecutive_failure_count;不计入 retry_count;不缩短 Retry-After。最后一条是「不把对方干崩」的唯一硬承诺。把 429 计入熔断,会把限速问题升级成可用性问题。
⚠️ throttle_until_at 是临时叠加,不修改 min_interval_ms,不写回规则副本。本页展示的 retry_after 取自网页对象的 response_headers,是原样记录,未做任何折算。
网络层异常(无状态码)
DEV-scheduler.md §9.3 网络层 · 近 24 小时
| 现象 | 次数 | 粒度 | 熔断 | 调度器的 suspend_reason(引用) |
|---|---|---|---|---|
| DNS 解析失败 | 78 | 域名 | 是 | dns_failure —— 疏探,域名可能已过期,封顶 24 小时 |
| 连接被拒 / 连接超时 | 74 | 域名 | 是 | transient_server_error |
| 读超时 | 33 | URL → 域名 | 连续才计 | transient_server_error —— 单次可能是大页面 |
| TLS 证书过期 / 无效 | 26 | 域名 | 是 | tls_failure —— 密探,证书续期通常 1 小时内完成 |
⚠️ DNS 与 TLS 必须分开。TLS 过期通常 1 小时内续上,密探有意义;DNS 无解析可能永远不回来,每 5 分钟探到天荒地老没有意义。合成一个 suspend_reason 就丢掉了这个区别。
health_signal 全局汇总
下载器侧算好、随 report 与探针响应回传 · 分母 340,492(done + empty)
| 字段 | 24h 合计 | 占比 | 计算方式 |
|---|---|---|---|
| xhr_total_count | 4,128,663 | 12.1 / 页 | network_log 中 type = xhr 的条数 |
| xhr_error_count | 96,402 | 2.33% | 其中 http_status ≥ 500 |
| xhr_throttled_count | 11,847 | 0.29% | 其中 http_status = 429 |
| xhr_denied_count | 8,206 | 0.20% | 其中 http_status ∈ {401, 403} |
| has_console_error | 74,908 | 22.0% | console_errors 非空(布尔,不传内容) |
| redirect_target_hash | 340,492 | 100% | final_url 的 url_hash |
⚠️ 不识别「取正文的那个 XHR」。一个 CSR 页面几十个 XHR,判据定不下来。下载器只回四个计数,比例判断由调度器做(§9.4)。本页同样只呈现比例,不给出「后端挂了」这类结论。
⚠️ 这六项在下载器侧算,约 60 行代码,数据零额外采集。让调度器去 GET /v1/objects/{object_id} 自己算,单机每天数十万次额外读,且会把调度器与网页对象 schema 绑死 —— 拆分的意义就没了。
⚠️ fetch_status 为 not_modified 或 failed 时 health_signal 整体为 null,不参与本表任何分母;should_render = false 时只有 redirect_target_hash。
待补项
CONVENTION.md 第二条「为什么失败」类字段 · 需三方确认后写入 DEV 文档
⚠️ fail_reason 的取值集尚未定义。同类的另外三个字段都有明确取值表:suspend_reason 见 DEV-scheduler.md §9.6,undetermined_reason 见 DEV-sampler.md §8.4,verdict_reason 是自由文本。只有 fail_reason 没有。
本页「我方 / 对方」与「网络层异常」两表按 DEV-scheduler.md §9.3 与 §十一 的现象分类展示,不是 fail_reason 的正式取值。正式取值定下来之前,前端不得把这些中文分类当枚举写死 —— 枚举取值全小写 snake_case,禁止缩写,且不得与字段名重名。