-
Notifications
You must be signed in to change notification settings - Fork 0
Performance
一请求一线程,阻塞 IO。 [SERVER].max_workers(默认 100)同时决定三件事:
- 能同时处理多少个请求
-
SendBatch的并发度上限 —— 批量不会再开一个自己的池,否则总并发会变成max_workers × 批量数,把下游打爆 - gRPC 的
maximum_concurrent_rpcs设为它的两倍,超出的请求排队而不是无限堆积
调它之前要知道的:按 host 限流的等待也占着线程。per_host_max_concurrent 设成 2、
同时有 200 个请求打向同一个 host 时,198 个线程在排队——其他 host 的请求会跟着饿死。
两者要一起调。
两道闸门,可以单独用也可以叠加。
[DOWNLOADER.concurrency]
per_host_max_concurrent = 0 # 0 = 不限制
per_host_wait_timeout = 30 # 等不到额度就返回 RESOURCE_EXHAUSTED
per_host_idle_ttl = 300 # host 条目空闲多久回收
max_tracked_hosts = 10000 # 同时跟踪的 host 数上限严格限制:超出的请求在服务端排队,等不到就返回 RESOURCE_EXHAUSTED,
而不是无限期占着线程。
per_host_idle_ttl 和 max_tracked_hosts 是必需的:爬虫会碰到无穷多域名,
不回收就是内存泄漏。
[DOWNLOADER.rate_limit]
per_host_qps = 0 # 0 = 不限制;可以是小数,0.5 = 两秒一个
per_host_burst = 0 # 桶容量;0 = ceil(per_host_qps),即最多攒一秒的量两者的区别:并发闸门管同时有几个在飞,速率闸门管每秒发起几个。 对方限的是 QPS 就用后者,对方扛不住并发就用前者。
限额只在本进程内计算。
服务端转发模式下任务统一从入口节点进来,在那一台上算就是全局的——所以不需要 Redis 之类的中间件(0.3 起已移除 redis 后端)。
客户端分发模式下每台各算各的,实际总量是 节点数 × 限额。需要全局精确控制就用转发模式。
gRPC 消息级 gzip,服务端透明解压,无需协议改动。
[CLIENT]
compression = "auto" # auto / gzip / none
compression_threshold = 1024 # auto 的体积门槛(字节)| 值 | 行为 |
|---|---|
auto(默认) |
够大且看起来可压缩才压 |
gzip |
一律压。链路带宽紧、CPU 富余时用 |
none |
一律不压。同机通信、或链路上已有压缩时用 |
主要收益在自动化脚本上:automation_script 传的是整个脚本文件,
实测 8158 字节压到 350 字节。批量发几百个带脚本的请求时差出一个数量级。
普通 GET 请求约 200~400 字节,压它省不下什么——所以有 1024 字节的门槛。
auto 还会跳过已经压过的二进制(图片、gzip 包、PDF 等)——再压一遍只会变大还费 CPU。
判断顺序:魔数前缀 → 含 NUL 字节 → UTF-8 合法性 → 控制字符占比。
早期版本只用"控制字符占比 10%"这一条判定,而随机字节的期望占比是 10.9%—— 正好压在阈值上,同一份数据有时判可压有时判不可压。改成以 UTF-8 合法性为主判据、 阈值降到 2% 之后稳定了(500 次重复无一误判)。
| 路径 | 耗时 |
|---|---|
| 热路径(浏览器已起来) | 200~300 ms |
| 冷启动(首次拉起浏览器) | ~1.5 s |
| 永久性导航错误(域名不存在等) | 0.2 s |
0.3.0 之前实测有单次点击耗时 296 秒的情况。修的是九处互相叠加的放大器, 其中影响最大的四个:
-
浏览器实例没有真正复用 —— 每次使用前用
is_connected()检查; 死了就丢弃重建,而不是拿着一个死连接反复超时。 - 超时预算一刀切 —— 现在按任务算:冷启动额外给 60s 宽限、常规开销给 15s。 固定值会让冷启动被误杀、或者热路径等太久。
-
永久性导航错误照样重试 ——
ERR_NAME_NOT_RESOLVED、ERR_CONNECTION_REFUSED重试三次还是同样结果,白白多花 16 秒。 -
缓存键用的是
browser而不是解析后的引擎 ——adapter="browser"和adapter="camoufox"会各自起一个浏览器,内存翻倍。
[BROWSER]
block_resources = ["image", "media", "font"] # 默认已开,最有效
wait_until = "load" # 别用 networkidle,除非确实需要
max_pages = 4
headless = true-
block_resources是收益最大的一项。只要 HTML 的话,图片/字体/媒体全是纯浪费。 -
wait_until = "networkidle"是最大的隐形开销。页面有 WebSocket、SSE 或定时轮询时 网络永远不会静默,每个请求都会耗满page_load才返回。 -
wait_for_selector优于拉长wait_for_timeout—— 前者一出现就继续。
| 组成 | 大约 |
|---|---|
| 纯 HTTP 的服务端 | 几十 MB |
| Chromium 系引擎(playwright / patchright / DrissionPage) | ~600 MB / 实例 |
| camoufox | 更高(Firefox + 一整套扩展) |
| 每个额外页面 | 几十~上百 MB |
集群里每个会执行渲染的节点各占一份。 三节点集群开浏览器就是 3 × 600 MB—— 这是转发架构的固有开销,不是 bug:每台都要能自己渲染,才谈得上分担和故障转移。
≤4 GB 内存的机器上用 camoufox 请把 max_pages 设成 1~2。超了会开始换页,
请求从几秒变成几分钟——看起来完全像卡死,但其实是在换页。
链路记录一条约 200~400 字节,见链路记录 · 磁盘占用。
浏览器本体:camoufox ~1 GB,chromium ~150 MB。
max_workers × 每线程栈(Linux 默认 8 MB 虚拟、实际按需)。100 个 worker 不是问题,
上千个就要考虑改成多进程部署。
timeout 是单次尝试的上限,不是总时长。
最坏总耗时 ≈ timeout × (max_retries + 1) + 退避总和
退避总和 = initial_backoff × (1 + exponent + exponent² + …),每项封顶 max_backoff。
默认值(timeout=300、max_attempts=3、initial=1、exponent=2)下最坏约
300 × 4 + (1 + 2 + 4) = 1207 秒。客户端的 deadline 要留够,否则会在服务端还在重试时
就先超时。
集群转发的默认超时就是按这个公式推的(任务 timeout × (重试+1) + 15s),
浏览器适配器另加冷启动宽限。见集群。
[CLIENT]
rpc_max_retries = 2
rpc_retry_backoff = 0.5 # 实际等待 = 基数 × 2^已重试次数这跟适配器内部的重试是两回事:适配器重试解决"目标站点抖了",这里解决"我们自己的 服务端抖了",互不覆盖。
只有 UNAVAILABLE 会重试(连接压根没建起来、请求没到过服务端)。
DEADLINE_EXCEEDED 刻意不重试——请求可能已经在服务端执行了,只是回复没赶上,
这时重发一个 POST 就是重复下单。
用 stream=True 或 d.stream(),响应体不进内存:
s = d.stream("https://example.com/big.zip")
with open("big.zip", "wb") as f:
for chunk in s:
f.write(chunk)断点续传(自动 Range + If-Range):
from ipclick.resume import download_to_file
download_to_file(d, url, "big.zip")流式请求在集群里不转发,永远由收到请求的节点自己执行——把每个分片再中转一次会让 入口带宽翻倍。要让子节点出流量就用客户端分发模式直连它。
tasks = [DownloadTask(uuid=u, url=u) for u in urls]
for resp in d.batch(tasks):
print(resp.request_uuid, resp.status_code)一次 RPC 发全部,服务端并发执行,按完成顺序返回(不是提交顺序)——
所以必须靠 request_uuid 对应,不能靠下标。
并发度受 max_workers 约束,且不会为批量另开一个池——否则总并发会变成
max_workers × 并发批量数,把下游打爆。
0.4 修了批量路径上的两处开销:
-
完成检测从 O(N²) 降到 O(N)。 旧实现每提交一个任务就把全部在途任务扫一遍找
已完成的,一千个任务就是约五十万次状态检查,而且全压在推任务的那个线程上,反而
拖慢了提交本身。改成完成方主动报到(
add_done_callback+ 队列),两边都是 O(1)。 -
线程池改为共用。 旧实现每次批量调用都新建再销毁最多
max_workers个线程 (默认 100)。批量本来就是"高频、每次很多任务"的用法,这份创建开销是白付的。
「边收边发」的行为不变:不等客户端把所有任务推完就开始返回结果,否则一个超长的批次 会让首个结果迟迟不到。