bigywater.tech
爬虫工程 · 65

爬虫指标:成功率与延迟分位数

爬虫上线后要看指标:成功率多少、延迟多少。但平均值、分位数、成功率各有几种算法,算错了会得出完全不同的结论。这篇用一个本机服务器把这几个坑逐个量出来。

12.8 对 266.5 ms
2000 个请求的延迟中位数与 p99(平均值 23.7 ms)
64–629 ms
只有 100 个样本时 p99 的 5%–95% 范围(同一个分布,真实 p99 约 305 ms)
595 对 366 ms
各 worker 的 p99 直接平均 与 合在一起算的 p99(合成数据)

平均值会骗人

实测 · 本机执行输出
  总耗时 3.55 秒,平均 23.7 ms,中位数 12.8 ms,p90 26.2 ms,p95 63.0 ms,p99 266.5 ms,最大 1532 ms
  平均值比中位数高 1.8 倍;低于平均值的请求占 90%

延迟分布通常是一堆很快的请求加一小撮很慢的。平均值被长尾拉高:实验里平均 23.7 ms,中位数 12.8 ms,90% 的请求低于平均值。看 p95、p99 才能看到长尾。

分位数的算法

实测 · 本机执行输出
  取前  100 个:p99 最近排名    83.4、statistics.quantiles inclusive(线性插值)    83.5、exclusive    96.9;p95 最近排名   50.3、inclusive   50.6、exclusive   55.8
  取前 2000 个:p99 最近排名   266.5、statistics.quantiles inclusive(线性插值)   266.5、exclusive   270.6;p95 最近排名   63.0、inclusive   63.0、exclusive   63.1

同一批 100 个数据,p99 用最近排名法和 statistics.quantiles 的 inclusive 差不多(83 ms),exclusive 是 96.9 ms;2000 个数据时三种差在 4 ms 以内。样本少时要写明算法。

样本量

实测 · 本机执行输出
  用 40 万个样本估计的"真实" p99 ≈ 305 ms
  每次   100 个样本:p99 的 5%–95% 范围     64 –    629 ms,中位    172 ms
  每次  1000 个样本:p99 的 5%–95% 范围    160 –    462 ms,中位    297 ms
  每次 10000 个样本:p99 的 5%–95% 范围    260 –    358 ms,中位    305 ms

样本少时分位数本身就不稳定。同一个分布,100 个样本的 p99 在 64–629 ms 之间,1000 个样本是 160–462 ms,10000 个样本才收敛到 260–358 ms。统计窗口太短、请求太少时,p99 的抖动大多是噪声。

不能平均分位数

实测 · 本机执行输出
  各 worker 的 p99:[322, 327, 623, 1109] ms;平均 595 ms;合在一起算 p99 366 ms;按请求数加权平均 387 ms
  每个 worker 只上报直方图(桶边界 [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000] ms),合并之后估计 p99 = 500 ms(桶的上边界);换成更细的 19 个桶估计 p99 = 400 ms;精确值 366 ms

多个 worker 各自的 p99 求平均没有意义:负载小的 worker 权重被放大(实验里平均出 595 ms,真实是 366 ms)。要上报原始值或直方图再合并;直方图的桶越粗,合并后的估计误差越大(500 ms 对 366 ms)。这一节是合成数据,不是真实请求。

超时的请求怎么算

实测 · 本机执行输出
  超时 28 个,错误类型 {'ReadTimeout': 28};超时请求记下的耗时 中位 201 ms(都被 timeout 截在 200 ms 附近)
  全部 2000 个请求(含超时)p99 201 ms;只统计成功请求 1847 个 p99 78 ms;真实耗时的 p99(不设 timeout,第一节的数据)是 267 ms

被 timeout 截断的请求,耗时就是 timeout 本身。含上它们,p99 被封顶在 201 ms;只算成功的,把最慢的请求都丢了,p99 只有 78 ms;真实值(不设 timeout)是 267 ms。报告里要同时给出超时的比例。

成功率

实测 · 本机执行输出
  状态码分布 {200: 1895, 503: 105};只看状态码 200:1895/2000 = 94.8%;200 且正文非空:1874/2000 = 93.7%
  按窗口看:前 200 个 96.5%,后 200 个 95.5%;各 200 个一窗口的 10 个窗口:[96, 95, 92, 96, 90, 95, 93, 92, 92, 96]
  对失败的 126 个 URL 各重试 1 次:重试成功 57 个;最终成功率 96.5%(第一次 93.7%);一共发出 2126 个请求(多发 6.3%);重试仍失败的 69 个里,按 URL 固定会失败的有 67 个(这是我在服务器里设定的,真实站点上看不到这个标签)

只看状态码 200 会把“200 但正文为空”算成功(94.8% 对 93.7%)。成功率还要说清是“第一次请求成功率”还是“重试之后每个 URL 的最终成功率”:重试一次把 93.7% 提到 96.5%,多发 6.3% 的请求,而固定失败的 URL 重试也救不回来(第 30 篇的退避、第 32 篇的分类讲过重试策略)。

分步演示

爬虫指标
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务器是本机的 Srv(延迟按 URL 编号用固定随机数确定;2% 的 URL 固定 503、1% 的 URL 固定返回 200 加空正文,每次请求另有 3% 的随机 503),客户端是 requests Session + 16 个线程;第一、二、五、六节用真实请求(各一次);第三、四节是用同一个延迟分布生成的合成数据(第三节每个样本量重复 1000 次)
    • 没有验证:真实站点的延迟分布(它们常常比这个更偏、有多个峰);更高的并发下客户端自身排队造成的延迟(coordinated omission:延迟从请求发出算起,没有包含等待发出的时间);Prometheus、HdrHistogram 等工具的分位数实现;滑动窗口里的 p99 怎么高效计算;时间序列上的告警阈值
    京ICP备2026067502号-1京公网安备11010502063487号