两条铁律
CONTRACT.md §一
| rule铁律 | why理由 |
|---|---|
| 所有对外请求都经过下载器 | 采样器与调度器都不直连目标站点,渲染、robots.txt、sitemap.xml、健康探针一律走本服务。域名限速需要单点保证 —— 一个新域名首次判定若走直连,是 7 次以上的集中探测,封禁往往就发生在这里 |
| 下载器拉,不被推 | 渲染慢(约 1 秒/页),lease 模式天然背压,横向扩容时调度器无需知道有几台机器 |
⚠️ 唯一豁免是导出给外部消费方的域名,因此必须满足两条约束:认领期间我方停抓、导出项携带限速与 robots 数据。外部消费方不是第四个服务,只能访问采样器的三个导出接口,使用独立 token,可单独吊销。
下载器提供的接口
CONTRACT.md §二
| endpoint接口 | caller调用方 | mode模式 | note说明 |
|---|---|---|---|
| POST /v1/render | 采样器、下游 | 同步 | 响应为完整网页对象。受域名限速约束,超限返回 429 + retry_after_seconds |
| POST /v1/probe | 调度器 | 同步 | 返回精简结构。内部强制 should_store = false,必须返回 has_reached_origin |
| GET /v1/objects/{object_id} | 下游 | 同步 | 取网页对象 |
POST /v1/render 请求字段
| field字段 | type类型 | note说明 |
|---|---|---|
| url请求地址 | string | |
| should_render是否渲染 | bool | false 时只发 HTTP,不进渲染队列,但走同一条限速路径 |
| wait_selector | string | 采样器推导的正文容器选择器,等待策略里排第一 |
| timeout_ms超时(毫秒) | int | 单位必须在名字里 |
| should_store是否入库 | bool | 入口 C 内部强制为 false |
⚠️ should_render = false 不是一条抓取路径。它只服务于采样器取 robots / sitemap 与健康探针。判定为 ssr 或 unknown 的域名由外部模块承担,本系统不另开纯 HTTP 抓取通道 —— 开了就同时顶翻 v1 的定位与契约的 ssr 分支。
下载器调用的接口
CONTRACT.md §三 · 调度器 API
| endpoint接口 | direction方向 | note说明 |
|---|---|---|
| GET scheduling-url/v1/lease | 下载器 → 调度器 | 查询参数 ?worker_id=w3&task_count=50&lease_ttl_seconds=120。lease_ttl_seconds 到期未 report 的任务自动回收重投,这是防止下载器崩溃导致 URL 永久卡死的唯一机制 |
| POST scheduling-url/v1/report | 下载器 → 调度器 | 必须重试到成功,否则该批 URL 要等 lease 超时才回收。幂等键是 lease_id |
| POST sampling-url/v1/api-endpoint-hints | 下载器 → 采样器 | v2 才启用 接口逆向线索。必须走采样器,不能直接告诉调度器 —— 域名级配置只能有一个所有者 |
⚠️ 过期 lease 的迟到 report:接受对象落库,但忽略 in_flight_count、统计、health_signal 与 link_list,返回 {"is_stale_lease": true}。该批 URL 可能已被重投并重新渲染,重复扣减会让计数变负,重复消费 link_list 会造成重复入队。
fetch_status 四值与字段可空性
CONTRACT.md §三 · 这张表决定调度器怎么处理每一条 report
| fetch_status抓取结果状态 | meaning含义 | object_id | statistics统计字段 | health_signal | link_list |
|---|---|---|---|---|---|
| done | 渲染成功 | 有 | 有 | 有 | 有 |
| empty | 渲染成功但正文为空 | 有 | 有 | 有 | 有 |
| not_modified | 304,未渲染 | null | 全 null | null | null |
| failed | 失败 | null | 全 null | null | null |
⚠️ health_signal 由下载器算好,不下发原始 network_log 与 console_errors。那两个字段在网页对象里,不在 report payload 里。调度器若为拿它们去 GET /v1/objects/{object_id},单机每天数十万次额外读,且会把调度器与网页对象 schema 绑死。
⚠️ link_list 必须带 rel 与 dom_position。只传 URL 会让 nofollow 无人遵守,列表页优先也判断不出来。
health_signal 字段来源
CONTRACT.md §三 · report 与探针响应共用同一个结构
| field字段 | computed from计算方式 |
|---|---|
| xhr_total_count | network_log 中 type = xhr 的条数 |
| xhr_error_count | 其中 http_status ≥ 500 |
| xhr_throttled_count | 其中 http_status = 429 |
| xhr_denied_count | 其中 http_status ∈ {401, 403} |
| has_console_error | console_errors 非空(布尔,不传内容) |
| redirect_target_hash | final_url 的 url_hash |
⚠️ 不识别「取正文的那个 XHR」,只看比例。一个 CSR 页面几十个 XHR,「哪个是主接口」的判据定不下来;后端挂通常是一片挂,比例判据更稳。等 v2 接口逆向跑起来、api_endpoint 已知后再升级为精确匹配。
跨服务约定
CONTRACT.md §五
| item事项 | rule约定 |
|---|---|
| 认证 | 服务间固定 token,Authorization: Bearer {token}。外部消费方使用独立 token,可单独吊销 |
| 重试 | 失败指数退避,上限 3 次。report 必须重试到成功 |
| 幂等 | report 用 lease_id;rules 用 domain + verified_at;domain-verdict-hints 用 claim_id + domain。三者都在 payload 里,不引用内部字段 |
| 存储隔离 | 三服务各自独立数据,不共享表。keyspace 前缀 scheduler: / sampler: / downloader: |
| 跨服务传递 | 只能通过 CONTRACT.md 里已定义的边,没有边就是不能传。需要别的服务的数据走接口拿副本,副本表名带 _replica 并注明只读 |
⚠️ crs: 不得作为任何 key 前缀 —— 它与系统名撞义。crs 一词有两个含义:整个系统,以及下载器的域名;key 前缀用 downloader: 正是为了避开这个歧义。
契约变更规则
CONTRACT.md §七 · 本文与 CONVENTION.md 同时冻结
任何一方要改契约,三方确认。字段只增不删,废弃字段标 deprecated。接收方必须容忍未知字段,不得因此报错。冻结之后改名 = 新增字段 + 旧字段标废弃 + 三方确认 + 等一个大版本。
| change变更 | trigger触发时机 | parties涉及 |
|---|---|---|
/v1/rules 与 /v1/lease 加 render_strategy + api_endpoint | 下载器 v2 启动接口逆向时 | 三方 |
采样器加 POST /v1/api-endpoint-hints | 同上 | 下载器、采样器 |
fail_reason 取值集 | 待补 当前全系统唯一没有取值表的「原因」字段 | 下载器(取值定义),三方知会 |
⚠️ render_strategy 只描述「怎么取页」(render | api),不承载租约、健康、认领等状态。一个字段两个语义轴是本系统反复在防的事 —— verdict 用布尔表三态已经付过一次学费。
⚠️ v2 的接口逆向会同时断掉链接图谱与 health_signal:直接打接口不渲染,没有 rendered_html、link_list、network_log。所以 render_strategy = api 的域名仍需按低频渲染一次只为提取 link_list,且 health_signal 只填 redirect_target_hash。这两条必须在契约变更时一并约定。
M0:第一个联调点
CONTRACT.md §六 · 三个项目各做各的会在联调时炸,M0 是唯一对齐锚点
| step步骤 | action动作 |
|---|---|
| 1 | 手写规则:POST scheduling-url/v1/rules,payload 即 §三 那个形状 |
| 2 | 投种子:POST scheduling-url/v1/seeds |
| 3 | 下载器 lease 一条 → 渲染 → report |
| 4 | 人工核对网页对象字段完整性 |
⚠️ M0 阶段采样器完全不参与 —— 规则手写。跑通这四步再接自动化。
命名规范要点
CONVENTION.md · 改本文不影响链路,改链路不影响本文
| rule规则 | detail要点 |
|---|---|
| 全称,禁止缩写 | 截断式缩写一律禁止(sched / stat / hist / conf / idx)。例外只有两类:镜像外部标准的字段名(rel / etag / lang),以及封闭的通用缩写白名单(URL / XHR / CSR / TiKV 等) |
| 单位与方向进名字 | min_interval_ms 而非 interval;priority_rank(小者优先)而非 priority |
| 布尔用前缀 | is_ / should_ / has_,不用裸名词 |
| Key 恒为三段 | {service}:{table_name}:{primary_key},单例表第三段写 current |
| 一张表只干一件事 | 任务与结果分家、配置与统计分家、副本与原本分家。选不出表名后缀,就说明这张表干了两件事 |
| 时间格式 | 2026-08-15T07:47:52Z,秒级,末尾恒为 Z。禁止本地时区、偏移量写法、Unix 秒 |
| 哈希规范 | url_hash = 全局规范化后 URL 的 SHA-256 前 16 字节,小写 hex,32 字符。content_hash 同规格 |
| 枚举是默认 | 全小写 snake_case,取值本身也禁止缩写,不得与字段名重名。禁止用布尔表三态以上的状态 |
| json tag 逐字相同 | Go 结构体的 json tag 必须与 CONTRACT.md 里的字段名逐字相同,不做任何大小写或分隔符转换。有校验脚本,不一致构建失败 |
⚠️ 命名漂移不是「难看」的问题:status 在两个服务里是两套不同枚举,JSON 反序列化不会报错,只会静默走错分支。