bigywater.tech
爬虫工程 · 21

解析放进进程池:什么时候值得

上一篇发现线程池帮不了 CPU 密集的解析。进程池能帮多少、什么时候值得、要付出什么代价?这篇在本机 10 个核心上量:启动成本、不同页面大小下的加速比、传输页面的开销、返回值的限制,以及批量提交的影响。

5.3–6.2 倍
8 个进程相对直接解析的加速比(页面 6 KB 到 2.5 MB)
0.141 ms
预热后的进程池,一个空任务来回一趟的中位数
1.29 对 6.15 倍
小页面用 chunksize=1 与 chunksize=8 的加速比

固定成本

实测 · 本机执行输出
  创建 4 进程的池并跑完第一个空任务(冷启动):83 ms
  池预热之后,一个空任务来回一趟:中位数 0.141 ms,p95 0.193 ms(300 次)

进程池冷启动(4 个进程)约 83 ms,预热之后每个任务来回要 0.141 ms。池要全局建一次反复用。

什么时候值得

实测 · 本机执行输出
        页面    个数      单页解析       直接     线程×8     进程×4     进程×8       进程×8 相对直接
       6KB  4000    0.14 ms    468 ms    485 ms    123 ms     77 ms          6.06 倍
      25KB  1500    0.47 ms    664 ms    595 ms    180 ms    107 ms          6.22 倍
     125KB   300    2.25 ms    655 ms    559 ms    183 ms    112 ms          5.83 倍
     635KB    60   11.27 ms    657 ms    557 ms    180 ms    118 ms          5.57 倍
    2570KB    20   47.64 ms    915 ms    763 ms    253 ms    173 ms          5.30 倍

在我的页面(一个表格页,解析结果只是数一数链接)上,8 个进程在 6 KB 到 2.5 MB 都快 5.3–6.2 倍,线程池基本不加速(0–1.2 倍)。这个收益依赖于每个任务都足够重、并且我批量提交了;核心数只有 10,所以 8 个进程接近用满。

传输页面贵不贵

实测 · 本机执行输出
       5 KB:提交 + 返回一趟 0.137 ms;其中 pickle.dumps 本身 0.001 ms
     100 KB:提交 + 返回一趟 0.154 ms;其中 pickle.dumps 本身 0.003 ms
    2000 KB:提交 + 返回一趟 0.652 ms;其中 pickle.dumps 本身 0.044 ms
   20000 KB:提交 + 返回一趟 9.482 ms;其中 pickle.dumps 本身 2.532 ms

把页面传给子进程要序列化:2 MB 来回约 0.65 ms,20 MB 约 9.5 ms,相对解析的耗时(2.5 MB 页面 47.64 ms)是小头。

返回什么

实测 · 本机执行输出
  让子进程返回解析好的 lxml 树:TypeError: cannot pickle 'HtmlElement' object
  返回一个整数(链接数 1077):3.03 ms;返回全部 1077 个链接字符串的列表:4.32 ms(列表序列化后约 19 KB)

子进程的返回值必须能 pickle,lxml 的元素树不行(TypeError: cannot pickle 'HtmlElement' object)。应该在子进程里就提取出需要的数据,只返回字符串、数字、列表之类。

批量提交

实测 · 本机执行输出
  直接解析:479 ms
  chunksize=1   :371 ms(相对直接解析 1.29 倍)
  chunksize=8   :78 ms(相对直接解析 6.15 倍)
  chunksize=125 :76 ms(相对直接解析 6.28 倍)

对 6 KB 这种小页面,解析一个只要 0.15 ms,和来回一趟的 0.14 ms 差不多。map(..., chunksize=1) 只快 1.29 倍,chunksize=8 以上才到 6 倍。任务越小,越要批量。

分步演示

进程池解析
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。页面是同一种表格结构按目标大小生成(实际 6 KB、25 KB、125 KB、635 KB、2570 KB);解析函数是 lxml.html.fromstring 加 xpath 数链接;进程池和线程池都预热后计时,3 次取中位数;进程池 map 的 chunksize 取 n//32;进程启动方式为 macOS 默认的 spawn;实验脚本用 __main__ 保护
    • 没有验证:Linux 上 fork 启动方式的成本;核心数更少的机器上的加速比;解析结果更大(需要返回整棵数据)时的传输成本;共享内存、mmap 等免拷贝传递页面的办法;进程池与 asyncio 结合时 run_in_executor 的额外开销(上一篇用过,没拆开测);不同解析库(如 selectolax)的表现;内存占用(每个进程各自加载 lxml)
    京ICP备2026067502号-1京公网安备11010502063487号