5.2 提速与节流:高性能优化 本节摘要:单节点性能有四个杠杆——并发度、缓存、会话复用、等待参数,每个都在"吞吐"与"礼貌"之间找平衡点。本节给出四杠杆的落地代码、基准测试的对照方法,以及"优化前先测量"的工程纪律。 性能四杠杆 优化之前先立一条纪律:没有基准的优化是玄学。同一任务单、同一时段、同一数据源,改一版配置跑三轮取中位数,前后对照才有发言权。四杠杆的效果全部应该在这样的对照里验证。 杠杆一:并发度。 2.2 节给过结论——并发收益先陡后平,拐点常在 4 到 8 会话。优化阶段要做的是找到自己任务的拐点:从 2 起步翻倍上试,吞吐增幅跌破两成就停。 杠杆二:缓存。
本节摘要:单节点性能有四个杠杆——并发度、缓存、会话复用、等待参数,每个都在"吞吐"与"礼貌"之间找平衡点。本节给出四杠杆的落地代码、基准测试的对照方法,以及"优化前先测量"的工程纪律。
优化之前先立一条纪律:没有基准的优化是玄学。同一任务单、同一时段、同一数据源,改一版配置跑三轮取中位数,前后对照才有发言权。四杠杆的效果全部应该在这样的对照里验证。
杠杆一:并发度。 2.2 节给过结论——并发收益先陡后平,拐点常在 4 到 8 会话。优化阶段要做的是找到自己任务的拐点:从 2 起步翻倍上试,吞吐增幅跌破两成就停。
杠杆二:缓存。 Crawl4AI 的 CacheMode 提供分级策略:BYPASS 全绕过(首次采集)、ENABLED 常规缓存(重跑与调试)、READ_ONLY 只读不写(复用昨天的页面做解析实验)、WRITE_ONLY 只写不读(预热)。增量任务的正确姿势是"内容指纹加缓存"组合:指纹变了才重抓。
杠杆三:会话复用。 浏览器实例与页面上下文是单节点最贵的资源——每次新建要冷启动渲染进程、重载资源。同一站点的翻页序列复用一个会话,开销能砍掉大半,代价是会话状态可能串味(登录态、分页游标),跨站必须隔离。
杠杆四:等待参数。 page_timeout 设太短,慢站点被误杀;delay_before_return_html 设太长,每页白等。两者调优的依据是页面真实加载耗时的分布(P95 定超时),而不是拍脑袋的安全感。
import asyncio from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode from crawl4ai.async_dispatcher import SemaphoreDispatcher async def tuned_batch(urls: list[str]): browser_cfg = BrowserConfig( headless=True, # 浏览器级复用:单实例多页面,避免每个任务冷启动 extra_args=["--disable-gpu", "--no-sandbox"], ) run_cfg = CrawlerRunConfig( cache_mode=CacheMode.READ_ONLY, # 复用缓存页面做解析实验,不重抓不覆盖 page_timeout=25000, # 按实测P95加载耗时定的上限 delay_before_return_html=0.5, # 页面稳定即可,不死等 mean_delay=1.0, # 平均请求间隔:礼貌频控的框架级参数 semaphore_count=6, # 并发上限停在拐点,不顶机器极限 ) dispatcher = SemaphoreDispatcher(semaphore_count=6) async with AsyncWebCrawler(config=browser_cfg) as crawler: results = await crawler.arun_many(urls, config=run_cfg, dispatcher=dispatcher) hit = sum(1 for r in results if getattr(r, "from_cache", False)) print(f"完成 {len(results)},缓存命中 {hit}") # 输出:完成 60,缓存命中 60(READ_ONLY 模式全部走缓存,零网络请求) return results asyncio.run(tuned_batch([f"https://docs.example.com/p/{i}" for i in range(60)]))
这段配置的两个参数值得放大看。mean_delay 把 2.2 节手写的礼貌间隔升级为框架参数,配合 semaphore_count 形成"并发有上限、间隔有下限"的节流结构——提速的所有努力都在这个笼子里进行。READ_ONLY 模式是解析实验的利器:2.3 节调 schema、3.3 节调清洗规则,都不必重新下载页面。
基准测试的对照骨架,任何优化都套这个模子:
import time, statistics def benchmark(fn, runs: int = 3) -> dict: """三轮取中位数:抖动大的指标用中位数而不是均值""" times, successes = [], 0 for _ in range(runs): t0 = time.monotonic() n_ok = fn() # 返回成功数 times.append(time.monotonic() - t0) successes += n_ok med = statistics.median(times) return {"中位耗时s": round(med, 1), "吞吐页每分": round(successes / sum(times) * 60, 1)} def run_with_config(concurrency: int, cache: bool) -> int: """占位:真实实现跑 tuned_batch 变体,返回成功页数""" time.sleep(0.2) return 40 print("并发2无缓存:", benchmark(lambda: run_with_config(2, False))) # 输出:并发2无缓存: {'中位耗时s': 0.2, '吞吐页每分': 12000.0}(占位数据) print("并发6无缓存:", benchmark(lambda: run_with_config(6, False))) print("并发6有缓存:", benchmark(lambda: run_with_config(6, True))) # 真实对照表的读法:并发2到6吞吐翻倍为有效;6到12增幅低于两成即拐点; # 缓存命中场景吞吐提升一个量级属正常——它省的是整个网络与渲染环节
四杠杆的投入产出排序值得背下来:缓存命中率通常是第一优先——增量任务里七八成页面没变化,缓存直接把这部分成本清零;会话复用第二——翻页序列长的任务收益巨大;等待参数第三——每页省两秒,十万页就是五十五小时;并发度最后动——它是最贵的杠杆(内存、封禁风险、频控压力),也是边际收益递减最快的。
红线在优化全程不变:任何吞吐提升不得突破礼貌频控下限。一个实际的判断框架——如果优化让单站请求速率提高了一倍,要么把并发降回去,要么把 mean_delay 翻倍,二选一必须做。提速是给自己省时间,节流是给别人留余地,两件事在调度室里同等重要,1.5 节的合规自证日志会记录这一切。
常见坑:拿生产任务调参数。基准测试应该在副本环境(缓存、镜像数据、低峰时段)做,生产流量里做实验,等于拿交付风险换实验数据。
关键直觉:优化的尽头往往是"删任务"而不是"加机器"。基准跑完常发现三成任务在抓没人消费的字段、两成页面重复入队——把浪费砍掉,比把效率提上去便宜得多也快得多。