迁移到数据 API 的第一个季度,多数团队都会有一种错觉:维护消失了。
解析器没人改了,代理池没人养了,半夜也没有告警叫醒人了。工程师确实闲下来一段。然后第三个月,运营跑过来说报表里的竞品价格「有点怪」。
这就是本文想讲的事:把采集外包给 API,消失的是解析器和代理池,没消失的是数据质量治理。 前者是显性故障,会响;后者是隐性故障,不响,它只是让你的报表慢慢变得不可信。
搜「亚马逊数据管道 API」的人,多半不是想学怎么发一个 HTTP 请求,而是想从「每周花两天修爬虫」这件事里脱身。但脱身之后你会发现,工程师并没有闲着,只是换了个东西在维护。下面从量化开始,一路推到六层架构、质量门和降级策略。
一、决策前先算一个数:周均维护工时
在做任何选型之前,先做一个统计:把过去八周团队花在数据采集维护上的工程师小时数加起来,除以八。
周均维护工时 = 过去八周采集维护工程师小时数 ÷ 8
这个数字是后面所有比较的分母。没有它,「迁移到 API 的月费」和「继续自维护的人力成本」永远不在一个尺度上,讨论会一直停留在感觉层面。
统计口径要注意:它不只是修 bug 的时间。以下四类都算在内:
- 排查误报(告警响了,查完发现是虚惊)
- 处理运营提问(「这个数为什么和上个月对不上」)
- 更新依赖与运行环境
- 救火带来的上下文切换成本(这一项通常被忽略)
多数团队第一次算出来都会愣一下,因为这个数通常比直觉高得多。而且它会直接改变结论:一个周均维护工时 6 小时的团队,和一个 0.5 小时的团队,对「要不要外包采集」的答案不同。
二、四条路线的成本结构,决定了维护责任归属
下面不点名具体供应商、不引用任何二手价格,只讲成本结构——因为结构差别是稳定的,价格数字是会变的。
| 路线 | 主要成本项 | 维护责任在谁 | 典型失败模式 | 适合场景 |
|---|---|---|---|---|
| 自建爬虫栈 | 工程师排障时间(远大于服务器) | 全部在自己 | 页面改版导致解析失效 | 极长尾页面、有专职团队 |
| 托管平台 / 自运维框架 | 计算时长 + 解析逻辑维护 | 运行环境外包,逻辑在自己 | 反爬越难账单越高 | 已有技术积累、需要定制 |
| 官方 SP-API | 授权运维与按操作限流规划 | 自己,但故障率低 | 限流与授权过期 | 自有经营数据 |
| 垂直数据 API | 按量计费 + 自建质量治理 | 采集外包,质量判断在自己 | 字段静默变空、数据变旧 | 市场与竞品数据 |
这张表里最该看的是第三列和第四列:维护责任在哪,你的工程师就得盯哪;典型失败模式是什么,你的告警就得围绕它设计。 很多团队选型时只比第一列的单价,接上之后才发现第四列的故障自己没有监控手段。
2.1 自建爬虫栈的成本项错位
你以为你在花服务器钱,但你在花工程时间。一个维护中的爬虫栈,年度真实成本里服务器通常只占小头,大头是工程师排障时间和机会成本。另一个常被低估的点:反爬对抗是持续军备竞赛,你今天绕过去的方法,不保证半年后还有效。
2.2 托管型为什么「越难越贵」
托管平台或自运维框架(Scrapy / Playwright 集群 / Actor 市场)起步确实快,生态也成熟。但维护责任没有转移——平台替你管了浏览器和运行环境,解析逻辑、选择器、失败重试、反爬策略仍然是你的代码。
更要命的是计费方式:托管型通常按计算时长计费。这意味着反爬越难,单次成功所需的重试次数、等待时间、渲染耗时就越多,账单和难度正相关。这个性质在预算模型里的后果是你没法用一个稳定单价做预测,因为你的成本曲线绑在了一个你无法控制的外部变量上。
2.3 官方 SP-API 的边界是设计出来的,不是排期问题
SP-API 稳定、合规、schema 明确,不用对抗反爬。但它的覆盖面被授权模型严格限定:它给的是卖家主动授权账户上的订单、库存、履约、自家广告数据。你拿不到竞品价格,拿不到市场级搜索排名,拿不到别人的评论。
这不是「暂不支持」,是设计上就不提供。底层授权模型的原因我之前拆过一篇《亚马逊卖家必看:官方API无法获取竞品数据的底层原因》,这里不重复。结论是:SP-API 通常是管道的一部分,不是全部。
2.4 垂直数据 API 转移了什么、没转移什么
把解析与反爬外包出去,你拿到结构化 JSON。页面改版是供应商要修的事,代理池是供应商要养的事——维护责任这一项转移了。但质量治理责任没转移:判断「这批数据还能不能用」这件事,只能是你的代码,因为你才知道什么叫「能用」。
2.5 现实终局通常是混合
划边界的判据很简单:凡是「我有没有授权」能回答的问题,走官方渠道;凡是「市场上正在发生什么」类的问题,走数据服务。
前者是授权问题,后者是采集问题,两者的失败模式、成本结构、合规要求都不同,硬塞进一条技术路径会让两边的复杂度都上升。需要保留自建爬虫的,通常只剩极长尾、且没有供应商覆盖的页面——把这部分控制在一两个对象以内,维护成本才是可接受的。
三、迁移之后:维护换了三个位置
这一节是全文的前提。如果你不接受这个判断,后面的架构会显得过度设计。
3.1 从「解析失败」变成「字段静默变空」
自建爬虫失败时很吵:抛异常、返回空、日志刷屏,你立刻知道。API 失败时很安静:HTTP 200,合法 JSON,schema 校验通过,但其中 12 个字段是 null。
区别在哪?前者的故障信号在传输层,后者的故障信号只存在于业务语义层——而业务语义层的校验,没人替你写。
3.2 从「被反爬挡住」变成「数据悄悄变旧」
爬虫被挡住,你拿到的是明确的失败。API 侧的对应故障是新鲜度退化:供应商缓存策略调整、某个市场采集频次下降、上游任务积压,都会让你拿到的值还是三天前的。数值格式合法,只是不再反映现实。
这类问题只能通过新鲜度 SLO + staleness 分布监控发现,没有别的办法——因为它在传输层、在 schema 层、在成功率指标上,全都是绿的。
3.3 从「代理成本」变成「重试放大成本」
自建时代成本主要在代理和机器,相对固定。API 时代成本是单价 × 尝试次数,而尝试次数被失败率、退避策略、幂等实现共同放大。
举例:可用记录率 70%、平均尝试 1.8 次的管道,实际单位成本是标价的 2.57 倍。这个放大系数不监控就会失控,而且它通常是缓慢上升的——没人改代码,某天你发现账单涨了 40%。
3.4 一个真实的迁移后遗症
一个六人数据团队把自建爬虫换成第三方 API,确实省下了约两个工程师日/周的维护量,团队很高兴。
但第三个月开始,运营反馈竞品价格「有点怪」。查下去发现:某两个市场的价格字段填充率从 96% 掉到 61%,这个过程持续了五周没有任何告警——因为所有请求都返回 200,成功率指标一直是 99% 以上。
修复花了两周,其中一周半在做归因:没有原始响应快照,无法确定是从哪天开始掉的。
教训不是「不该迁移」,而是迁移的同时必须把质量监控一起建起来。如果迁移时就落了原始归档和字段级填充率监控,这个退化会在第一周被发现,修起来是改个参数的事,而不是两周的考古。
三条放一起看,结论很直接:采集可以外包,判断力不能外包。
四、六层参考架构,以及每层为什么只做一件事
下面这套分层不是理论模型,是跑得稳的团队实际收敛出来的形状。原则只有一条:每层一个职责,故障能定位到具体一层。
业务目标
│ 字段契约 · 新鲜度 SLO · 成本预算
▼
┌───────────────────────────────────────┐
│ L1 采集层 取回原始响应,不做业务判断 │
├───────────────────────────────────────┤
│ L2 原始归档 原样落盘,可复现的唯一依据 │
├───────────────────────────────────────┤
│ L3 队列重试 幂等键 · 退避 · 死信分流 │
├───────────────────────────────────────┤
│ L4 标准化存储 契约校验 · 幂等 upsert │
├───────────────────────────────────────┤
│ L5 质量门 四个数字决定是否放行 │
├───────────────────────────────────────┤
│ L6 监控预算 面板 · 告警 · 周回归 · 成本归因 │
└───────────────────────────────────────┘
│
▼
下游:报表 / 模型 / Agent / 告警
第 0 层:输入不是需求,是三张契约
写代码之前先把三件事写成可判定的条款:
- 字段契约:业务依赖的字段清单,按 P0/P1/P2 分级
- 新鲜度 SLO:P0 字段的 P95 数据龄期上限(比如 6 小时)
- 成本预算:每千条可用记录的上限金额
这三张契约是所有后续自动化的依据。没有它们,质量门只能拍脑袋设阈值,而拍脑袋设的阈值会在第一次误报之后被人关掉。
写字段契约时最容易犯的错是什么都想要。一份列了 80 个字段、全部标 P0 的契约等价于没有契约——可用记录率会被压到接近零,质量门永远报警,最后被人关掉。P0 的正确定义是「缺了它这条记录就不能进下游报表」,通常不超过 15 个。P1 是「有了更好、缺了能接受」,P2 是「偶尔用得上,可以为空」。分级不是形式主义,它直接决定重试策略、降级档位和质量门阈值。
L1 采集层:只取回原始响应
职责边界要划得很硬:发请求、拿响应、交给下一层。不做字段校验、不做业务判断、不做清洗。
理由是可归因性。如果采集器里混了业务逻辑,当数据出问题时你无法判断是「取错了」还是「判错了」——这两种故障的修复路径不同。采集器只应该产出两样东西:原始响应体,以及本次调用的元数据(耗时、状态码、时间戳、尝试次数)。
L2 原始归档:可复现性的唯一依据
把原始响应原样落盘,按「对象 + 市场 + 日期」分区,保留 30 到 90 天。
这一层的价值在出事时才体现:当你怀疑某条记录有问题,能不能回到三个月前那个具体的响应体去看?如果不能,你做的所有归因分析都是猜测。 第 3.4 节那个团队多花的一周半,就是这一层的成本。
保留期的判据是:它必须长于你的问题发现周期。 团队平均三周才发现异常,那保留 30 天只够看一次,90 天才能支撑对比。压缩后的原始 JSON 按对象存储,成本通常远低于同等时间跨度的聚合指标重算成本,所以这一层宁可多留也别省。
L3 队列重试:按错误类型三分类
负责调度、并发控制、限流与重试。关键设计是分流:限流与服务端错误进重试,参数错误与鉴权错误直接进死信,返回 200 但关键字段全空进软失败队列。
把这三种混在一个重试循环里,是重试放大成本失控的主要原因——因为你会对一个永远不可能成功的参数错误重试 N 次,然后按成功计费的单价付钱。
L4 标准化存储:幂等 upsert
把原始响应映射到内部 schema,做类型强校验,幂等写入。实践上用「对象标识 + 市场 + 数据日期 + 契约版本」做唯一键,用 upsert 而非 insert。同时把 P0 字段的缺失情况记录下来,供 L5 使用。
L5 质量门:自己必须建的那一层
按批次计算四个数字(字段覆盖率、非空填充率、可用记录率、新鲜度达标率),低于阈值就不放行,或降级标记为「参考值」。
它的存在意义不是保证数据完美,而是让退化变成一个会响的警报,而不是一个慢慢被接受的事实。
L6 监控预算:五张图就够
面板上值得长期保留的只有五张:
1. 可用记录率趋势 (按对象类型分线)
2. P0 填充率趋势 (按市场分线)
3. P95 数据龄期趋势
4. 重试放大系数趋势
5. 每千条可用记录成本趋势
前三张回答「数据还能不能用」,后两张回答「代价是多少」。五张够了,再多就没人看。
这里有个常见误区:把调用成功率单独做成一张大图。 它几乎恒定为 99% 以上,提供不了任何信息,却占据面板最显眼的位置——第 3.4 节那个团队盯了五周的成功率,一直都是绿的。
除了面板,这一层还要做两件事:每周跑一次固定样本的周回归(一次性验收只能证明「现在能用」,周回归才能证明「一直在能用」),以及预算守卫——当日累计成本超阈值时自动降级采样频次,而不是等月末看账单。
贯穿多层的两件事
并发与限流要按操作类型建模。 常见错误是用一个全局 QPS 做规划。官方渠道与多数专业数据服务都采用按操作类型独立的令牌桶——不同操作配额不同、补充速率不同。你需要的不是「我们每秒发多少请求」,而是一张按操作类型列出的配额表,外加客户端侧的按操作退避。没有这张表,压测结果也不可信:用一种操作跑出的吞吐,换另一种操作会得到不同的数字。
数据血缘要能回溯到具体那次调用。 每条落地记录带上来源元数据:请求 ID、供应商响应时间戳、尝试次数、契约版本、采集批次号。字段不多,但缺了它,当用户问「这个价格是什么时候取的」你答不上来。它同时是归因分析的基础——某个字段开始退化时,你能按批次、按市场、按时间切分定位,而不是全量重跑一遍碰运气。
五、重试与幂等:最容易写错的一段
重试看起来简单,实际是亚马逊数据管道里后果最持久的一段。错误集中在四处。
5.1 三类错误,三种路由
| 类型 | 例子 | 处理 |
|---|---|---|
| 可重试 | 限流、5xx、网络超时 | 退避重试,有次数上限 |
| 不可重试 | 鉴权失败、参数错误、格式问题 | 直接进死信 + 告警 |
| 软失败 | HTTP 200 但关键字段缺失 | 进软失败队列,由质量门按批次判断 |
第二类重试一万次也是同样结果,只会放大成本。第三类最容易被当成成功——它不报错,只是让你的下游拿到一堆 null。
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
resp = fetch(item)
raw_store.save(item.id, resp, meta=build_meta(attempt)) # 先归档,再判断
return ok(resp)
except Retryable as e: # 429 / 5xx / timeout
sleep(backoff(attempt))
except NonRetryable as e: # 401 / 422 / bad params
dead_letter.push(item, reason=e.code)
return fail(e)
except EmptyButOk as e: # 200 但 P0 字段全空
soft_fail.push(item, reason="p0_empty")
return fail(e)
dead_letter.push(item, reason="attempts_exhausted")
注意 raw_store.save 的位置:先归档再判断。归档发生在任何业务判断之前,这是 L1/L2 分层原则的直接体现。
5.2 退避必须加抖动,否则会制造重试风暴
指数退避是标配,但没有抖动的指数退避会造成重试风暴:一批请求同时失败 → 等待同样长的时间 → 同时重试 → 把限流阈值瞬间打满 → 又同时失败。这是一个自激振荡。
加抖动就是把退避时长乘一个随机系数,把重试请求在时间轴上摊平:
def backoff(attempt):
# 指数退避 + 抖动:CAP 防尾部延迟失控,random 打散重试风暴
return min(CAP_SECONDS, BASE_SECONDS * 2 ** (attempt - 1)) * (0.5 + random.random())
CAP 不能省,否则尾部延迟会失控;random 也不能省,否则抖动这个设计目的就没实现。
5.3 幂等键必须包含契约版本
很多团队用「ASIN + 市场 + 日期」做幂等键,这在契约不变时没问题。但当你新增了一个 P0 字段,历史记录已经不满足新契约了——如果不带版本号,重跑时会被误判为「已处理过」而跳过。
加上契约版本后,契约升级会自动触发全量重跑,这才是正确行为。
5.4 超时设置比重试次数更重要
团队通常反复调重试次数,却把超时值设成一个随手写的数字。超时对成本和吞吐的影响更大:
- 超时太短:本来能成功的请求被判失败进入重试,既放大成本又产生重复工作。在数据服务场景下,某些复杂对象的响应时间天然偏长,一刀切的短超时会系统性抬高你的失败率。
- 超时太长:失败请求长时间占用并发槽位,队列积压,反而降低整体吞吐。
合理做法是按操作类型的历史响应时长分布来设:取 P99 作为超时基准,再留余量。这个数要从你自己的调用日志里算,不要沿用默认值。同时区分连接超时与读超时——前者应该短(几秒),后者按响应分布来定。
5.5 死信队列不是垃圾桶
死信里的内容必须被消费,否则它只是一个延迟丢弃机制。最低要求:按失败原因聚合,每天出一份清单,并对「原因分布突变」告警。
原因分布的变化往往比失败率本身更早暴露问题——比如 attempts_exhausted 突然增多,通常意味着上游成功率在下降,而这时整体成功率指标可能还没跌破阈值。
六、质量门:四个数字加一个新鲜度 SLO
质量门的核心,是把「数据能不能用」这个主观判断,拆成可自动计算的量。
6.1 覆盖率与填充率必须分开算
字段存在不等于字段有值:
- 覆盖率:schema 层面有没有这个路径
- 填充率:实际交付时取没取到值
两者都低 → 字段根本不支持。覆盖率高但填充率低 → 字段在契约上,但实际交付时取不到值。后者更危险,因为 schema 校验会放行。
这个差距在签约前就能量出来:同一批 ASIN、同一张字段清单,一家返回 12 个字段,另一家返回 58 个,官网对勾相同。字段级对比怎么跑,见这篇。
def field_present(rec, path):
cur = rec
for part in path.split("."):
if not isinstance(cur, dict) or part not in cur:
return False
cur = cur[part]
return True
def field_filled(rec, path):
if not field_present(rec, path):
return False
cur = rec
for part in path.split("."):
cur = cur[part]
if cur is None: return False
if isinstance(cur, str) and cur.strip() == "": return False
if isinstance(cur, list) and len(cur) == 0: return False
return True
def coverage(records, fields):
total = len(records) * len(fields)
return sum(1 for r in records for f in fields if field_present(r, f)) / total if total else 0.0
def fill_rate(records, fields):
total = len(records) * len(fields)
return sum(1 for r in records for f in fields if field_filled(r, f)) / total if total else 0.0
注意 field_filled 里那三个判断:空字符串、空列表、None 都算没值。"" 和 [] 在 JSON 里是合法的,很多 schema 校验器会愉快地放行。
6.2 可用记录率:唯一对应「能不能进报表」的指标
前两个是字段级的,但下游消费的是整条记录。可用记录率定义为所有 P0 字段都被填充的记录占比。这是质量门的主阈值。
def usable_rate(records, p0_fields):
if not records: return 0.0
ok = sum(1 for r in records if all(field_filled(r, f) for f in p0_fields))
return ok / len(records)
def staleness_p95(records, ts_field, now=None):
"""返回 P95 数据龄期(小时);None 表示无有效时间戳。"""
now = now or datetime.now(timezone.utc)
deltas = []
for r in records:
ts = r.get(ts_field)
if not ts: continue
t = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
deltas.append((now - t).total_seconds() / 3600)
if not deltas: return None
deltas.sort()
return deltas[max(0, int(round(0.95 * len(deltas))) - 1)]
6.3 新鲜度 SLO 要写成分布,不要写成布尔值
「数据是不是实时的」无法告警,因为「实时」没有量化定义。可执行的写法是:P0 字段的 P95 数据龄期 ≤ X 小时。
用 P95 而不是平均值,是因为平均值会掩盖长尾:一个 P50 为 20 分钟、P95 为 31 小时的分布,平均值可能很好看,但你的报表里就是有相当一部分数据是隔天的。
6.4 把五个量装进一个函数
P0 = ["asin", "title", "price.amount", "availability.status"]
P1 = ["brand", "rating.value", "review_count", "bsr.rank_main"]
FRESHNESS_FIELD, SLO_P95_HOURS = "collected_at", 6.0
def gate(records):
m = {
"n": len(records),
"coverage_p0": coverage(records, P0),
"fill_p0": fill_rate(records, P0),
"fill_p1": fill_rate(records, P1),
"usable": usable_rate(records, P0),
"staleness_p95": staleness_p95(records, FRESHNESS_FIELD),
}
m["pass"] = (
m["fill_p0"] >= 0.95
and m["usable"] >= 0.90
and m["staleness_p95"] is not None
and m["staleness_p95"] <= SLO_P95_HOURS
)
return m
特别注意 is not None 那一行。时间戳缺失时 staleness_p95 返回 None:在 Python 3 里 None <= 6.0 会直接抛异常,而在一些弱校验实现里会被当成 False 静默放行——后者更危险,它让「没有时间戳」的数据看起来像是通过了新鲜度检查。
6.5 记录级标记 + 批次级决策
一个设计选择会显著影响质量门的可用性:按记录判定还是按批次判定?
- 按记录判定:适合写入前清洗。单条不满足 P0 就不写入,干净但会丢数据,且无法区分「这批整体有问题」和「这一条碰巧有问题」。
- 按批次判定:适合放行决策。能捕捉系统性退化,但会连带拦掉批次里本来正常的记录。
实践做法是两个粒度都要:记录级做标记(这条是否满足契约),批次级做决策(这批是否放行)。把两者混成一个开关,要么丢数据,要么漏告警。
顺序上先记录级标记、再批次级判定,这样拦截发生时你能立刻说出是哪一类记录拖低了整批。
6.6 样本必须分层,聚合指标会骗你
这是质量门最隐蔽的坑。假设样本里七成是图书、三成是服饰:图书的价格字段填充率 96%,服饰因为变体多、部分变体缺价,填充率只有 58%。聚合后你看到的是 84%——一个看起来还行、却掩盖了服饰这个品类已经不可用的数字。
所以固定回归样本必须按对象特征分层:按品类、按是否有变体、按市场、按价格区间各取一定数量,分别计算指标。分哪个维度取决于你的业务,关键是让每个分层都有足够样本量单独出结论。
6.7 阈值怎么定才不会被关掉
质量门最常见的死法是阈值太严,第一次误报就有人把它关了。正确做法是先观测两周再设阈值:
第一周 只记录不拦截 → 拿到四个数字的基线分布
第二周 基线往下取一个容差区间 → 告警线
再往下取更大容差 → 拦截线
阈值应该来自你自己的数据分布,而不是某个通用最佳实践。质量门一旦被关过一次,重新建立信任要比第一次建立难得多。
七、成本:要优化的不是单价
多数团队只知道月度总账单,不知道每个业务对象花了多少,于是成本优化无从下手。
7.1 有效成本公式
有效成本 = 单价 × 每条可用记录平均尝试次数 ÷ 可用记录率
这个公式把三个变量串起来了:单价、稳定性、完整性。一个单价便宜但可用率低的供应商,算下来可能更贵;一个单价贵但字段全、重试少的,可能反而划算。
7.2 放大系数必须实测
在采集层记录每条记录的尝试次数,按对象类型与市场聚合出平均值。这个数字通常会让第一次看到它的人意外:很多团队以为自己的放大系数接近 1,实测常在 1.5 到 2.5 之间。
它上升的原因往往不是失败率变高,而是退避策略不合理导致重试被提前触发——也就是第五节讲的超时设太短。
7.3 归因到业务对象的最小实现
不需要复杂系统。在采集层把每次调用的成本估算写进记录元数据(调用次数 × 单价,加上归属的对象与市场),在存储层按「对象类型 / 市场 / 日期」三个维度聚合,就能回答:哪个市场最贵、哪类对象在烧钱、成本从哪一周开始上升。
加上可用记录数之后,可以算出每个对象、每个市场的每千条可用记录成本。这个颗粒度会直接改变优化方向——实践中经常出现的情况是,占比最大的那个对象并不是单位成本最高的,团队一直在优化错的地方。
7.4 预算守卫:把降级策略写进代码
当日累计成本超阈值时,自动降低非 P0 对象的采样频次,保留 P0 对象的完整采集。这个策略要提前写好并测过,不能等超支了再临时决定——临时决定通常只有「全停」和「继续烧」两个选项,两个都不好。
八、schema 变更:管道里的契约测试
上游字段变更是静默发生的——改名、改类型、枚举新增、字段合并。它对下游的破坏不体现在传输层,只体现在报表里某个数字突然不对。三层防御:
第一层:存在性与类型强校验。 在标准化层对每个契约字段显式断言,不只检查存在,还要检查类型。类型从字符串变成对象是最常见的破坏性变更,弱类型语言的隐式转换会让它悄悄通过。校验失败不应静默跳过,应写入软失败队列。
第二层:周度结构 diff。 每周对固定样本保存一份响应快照,与上周做结构 diff:新增了哪些路径、消失了哪些路径、哪些类型变了,把 diff 推送到告警通道。这份快照同时是归因分析的依据——回溯「这个字段从什么时候开始取不到」,没快照只能靠记忆。
第三层:契约版本触发重跑。 字段契约本身要有版本号。契约变更时幂等键跟着变,自动触发受影响对象的重跑。没有版本号机制,契约升级后历史数据会以「已处理」为由被跳过,导致新旧数据混在一起而无人察觉。
九、降级:上游不可用时管道该怎么表现
这一节几乎没人写进架构文档,但它决定了事故当天你的团队是慌乱还是有序。
任何上游都会出问题:供应商故障、配额打满、网络分区、限流收紧。区别只在于出问题的时候,你的管道是「明确地少给了一些数据」还是「假装给全了」。没有预设策略时,团队临场只有「全停」和「继续烧」两个选项。
三档降级:
| 档位 | 行为 | 触发条件 |
|---|---|---|
| P0 保真 | P0 对象频次不变,P1/P2 降频 | 成本超阈值或上游错误率上升 |
| 只保 P0 | 只采 P0 对象且降频,P1/P2 全停 | 上游持续不可用超过设定时长 |
| 只读归档 | 停止采集,下游切到最近一次通过质量门的快照 | 上游长时间不可用 |
第三档必须对外显式标记为「数据龄期已超 SLO」。三个档位的切换条件、生效范围、恢复条件都要写进配置,而不是写进某个人的记忆里。
还有一条硬性要求:降级状态必须显式暴露给下游。 降级时最危险的不是少给数据,是下游不知道数据少了。质量门放行的记录里应该带一个采集状态标记,写明这批是在哪个档位采集的、覆盖的对象范围是什么。报表层读到降级标记就该显示提示,模型层读到就该调整置信度。
把降级状态藏在管道内部,等于让下游拿降级数据当完整数据用——这正是第 3.1 节说的那类静默失败。
十、90 分钟落地清单
按下面的顺序做,每一步都有明确产出物:
- 写字段契约(20 分钟):列出业务依赖的字段,P0/P1/P2 分级,P0 控制在 15 个以内。超过说明你还没想清楚什么最关键。
- 定新鲜度 SLO(10 分钟):给 P0 字段定一个 P95 数据龄期上限,写进文档。没有这个数字,新鲜度无法告警。
- 搭采集器与原始归档(20 分钟):只发请求、只存响应,不做业务判断。代码量通常比你想象的小得多。
- 跑基线(20 分钟):取 200–500 个真实 ASIN 跑一遍,用第六节的脚本算出四个数字。这是基线,不是验收结果。
- 接质量门与告警(20 分钟):先记录不拦截,观察两周,再按基线分布设告警线与拦截线。
五步做完,你就有一条「数据退化会响」的管道。剩下的优化——成本归因、schema 契约测试、周回归——都是在这套骨架上加的。
最容易半途而废的是第四步。团队常有的冲动是跳过基线直接上告警,理由是「先跑起来再说」。但跳过基线的代价是阈值只能拍脑袋定,而拍脑袋的阈值必然在第一次误报后被关掉。宁可晚两周上线告警,也不要早两周上线一个会被关掉的告警。
十一、我们在这一架构里负责哪一层
把话说清楚,避免误解:我们(Pangolinfo)负责第 1 层,以及第 1 层带来的稳定性。第 5 层那道质量门,不管你用谁的采集服务,都得自己建。
我们的公开指标是:中位延迟约 3 秒、调用成功率 99%、日调用量 3000 万以上;SP 广告位跨 13 个市场的整体采集率为 91.4%(以上为公开指标)。如果你的消费方是 AI Agent 而不是固定报表,建一整条管道可能是过度工程——Amazon Data MCP 有 19 个工具,通过远程 HTTP 接入,零安装,让 Agent 在需要时直接取数(我们自测)。
我们不做的三件事:账户域数据(订单、库存、自家广告属于 SP-API 授权范围,我们不提供也不提供绕过方式);买家个人身份信息;任何登录后可见的数据。这三条是设计边界,不是路线图上的待办。
三种不该用我们的场景:只需要极少量低频数据,自己写几行脚本更划算;需求全部落在自有账户范围内,SP-API 更合规也更便宜;需要登录态数据或买家 PII,这个我们不做,也不该有任何供应商做。
把不合适的场景排除掉,剩下的才是能省下维护成本的部分。
最后
回到开头那个问题:不养爬虫之后还剩多少维护量?
答案是——解析器和代理池归零,质量治理从零开始。 前者是一次性的,后者是长期的,而且后者才决定你的报表可不可信。
如果你打算动手,可以从 Pangolinfo Amazon Scraper API取商品与搜索数据,评论场景走 Pangolinfo Amazon Review API;先从 Pangolinfo 控制台获取 API Key 跑一轮基线,字段清单见 Pangolinfo Amazon Data MCP 技术文档。