Fantoccini高并发优化:连接池与任务队列实战指南
1. 项目概述:当Fantoccini遇上高并发
如果你正在用Fantoccini(一个基于WebDriver协议的Python异步浏览器自动化库)做大规模数据抓取、UI自动化测试或者监控任务,那么“性能”和“稳定性”这两个词,大概率已经让你头疼过不止一次了。我最近就刚从一个坑里爬出来:一个用Fantoccini搭建的分布式爬虫,在任务量上去之后,不是浏览器实例莫名崩溃,就是内存悄悄涨到几个G,最后整个进程僵死。排查下来,根子都出在并发控制和资源管理上。
Fantoccini本身是个很棒的工具,它用异步(asyncio)的方式驱动浏览器(比如Chrome或Firefox),理论上能高效处理多个页面任务。但“能处理”和“能稳定、高效地处理”完全是两码事。浏览器实例本身就是重量级资源,每个标签页、每个网络请求、每段JavaScript执行都在消耗CPU和内存。当你同时发起几十上百个任务时,如果不对这些浏览器“工人”进行精细化管理,它们很快就会因为资源争抢而陷入混乱,或者因为资源泄漏而拖垮整个系统。
所以,这次我们不谈Fantoccini的基础用法,直接切入最硬核、也最能体现工程能力的部分:如何构建一个健壮的、能承受高并发压力的Fantoccini应用。核心就是两件事:第一,并发控制,即如何科学地安排任务,避免一拥而上把系统压垮;第二,资源管理,即如何确保浏览器实例、页面、网络连接等资源能够被正确地创建、使用和释放,不留后患。这两者相辅相成,缺一不可。
2. 核心架构与设计思路拆解
在动手写代码之前,我们必须先想清楚架构。一个高性能的Fantoccini应用,绝不能是简单地在循环里await一堆browser.new_page()。我们需要一个具备调度能力和生命周期管理能力的中间层。
2.1 为什么需要连接池与任务队列?
直接为每个并发任务创建一个浏览器实例是最糟糕的做法。Chrome每个进程的内存开销可能在百MB级别,创建和销毁的成本极高。我们的设计核心是资源复用和压力缓冲。
连接池(Browser Pool)的思想来源于数据库连接池。我们预先创建(或按需懒创建)固定数量的浏览器实例(fantoccini.Client),并将它们维护在一个池子里。当有任务需要执行时,从池中借用一个浏览器实例(更精确地说,是借用其新建页面的能力),任务完成后,将实例归还池中,而不是关闭它。这带来了几个关键好处:
- 资源限制:池的大小就是并发浏览器实例数的硬上限,防止系统资源被耗尽。
- 性能提升:避免了频繁启动/关闭浏览器的巨大开销。
- 状态管理:池可以统一管理浏览器的健康状态(如心跳检测),自动重启异常的实例。
任务队列(Task Queue)则是控制并发度的另一道闸门。即使我们限制了浏览器实例数,如果一个实例同时处理太多页面(通过多个标签页),性能也会急剧下降。因此,我们需要一个队列来存放待执行的任务(例如,要访问的URL列表)。工作协程从队列中取出任务,然后向连接池申请浏览器资源来执行。这样,系统的总并发压力(浏览器实例数 × 每个实例的页面并发数)就是完全可控的。
2.2 异步模式下的协同挑战
Fantoccini基于asyncio,这要求我们的池和队列也必须是异步友好的。我们不能用普通的queue.Queue,而要用asyncio.Queue。同样,在多个协程争抢池中资源时,需要使用asyncio.Lock或asyncio.Semaphore来实现同步,避免竞态条件。
这里有一个关键设计点:“借”和“还”。当工作协程从池中获取一个浏览器客户端时,如果池空且未达上限,则应创建新实例;如果池空且已达上限,则协程应等待(await)直到有实例被归还。这个“等待”必须是异步的,不能阻塞事件循环。我们通常会用一个asyncio.Condition或结合了asyncio.Queue的机制来实现。
注意:浏览器实例本身不是线程安全的。Fantoccini的
Client对象必须在同一个事件循环(即同一个线程)中使用。我们的连接池也必须确保这一点,所有对池的操作都必须在主事件循环中进行。
3. 实现浏览器连接池与资源管理器
理论说完了,我们来看代码。下面是一个精简但功能完整的异步浏览器连接池实现。它包含了基本的获取、归还、健康检查和优雅关闭逻辑。
import asyncio import logging from typing import Optional, List import fantoccini class AsyncBrowserPool: """异步浏览器连接池""" def __init__(self, browser_args: list = None, pool_size: int = 5, health_check_url: str = "about:blank"): """ 初始化连接池 :param browser_args: 传递给fantoccini.Client的启动参数,如['--headless', '--no-sandbox'] :param pool_size: 连接池最大容量 :param health_check_url: 用于健康检查的URL """ self._browser_args = browser_args or ['--headless', '--disable-gpu'] self._pool_size = pool_size self._health_check_url = health_check_url # 可用实例队列 self._available_clients: asyncio.Queue[fantoccini.Client] = asyncio.Queue(maxsize=pool_size) # 所有已创建实例的列表,用于最终清理 self._all_clients: List[fantoccini.Client] = [] # 控制创建实例的锁,防止超额创建 self._creation_lock = asyncio.Lock() # 当前已创建的实例数 self._created_count = 0 # 池是否已关闭 self._closed = False self.logger = logging.getLogger(__name__) async def _create_new_client(self) -> fantoccini.Client: """内部方法:创建一个新的浏览器客户端""" # 这里假设使用本地Chrome,并通过WebDriver协议连接 # 实际部署时,browser_args可能需要指定远程WebDriver地址 client = await fantoccini.Client('http://localhost:4444/wd/hub', desired_capabilities={ 'browserName': 'chrome', 'goog:chromeOptions': { 'args': self._browser_args } }) self._all_clients.append(client) self._created_count += 1 self.logger.debug(f"创建新的浏览器客户端,当前总数:{self._created_count}") return client async def _health_check(self, client: fantoccini.Client) -> bool: """对浏览器客户端进行简单的健康检查""" try: # 快速打开一个空白页并关闭,测试客户端是否响应 page = await client.new_page() await page.goto(self._health_check_url, wait_until='domcontentloaded') await page.close() return True except Exception as e: self.logger.warning(f"浏览器客户端健康检查失败: {e}") return False async def acquire(self) -> fantoccini.Client: """从池中获取一个可用的浏览器客户端""" if self._closed: raise RuntimeError("连接池已关闭") client = None # 首先尝试从可用队列中直接获取 if not self._available_clients.empty(): client = await self._available_clients.get() # 获取后立即进行健康检查 if await self._health_check(client): return client else: # 不健康的实例,丢弃并递归调用acquire self.logger.info("丢弃不健康的浏览器实例") await self._dispose_client(client) return await self.acquire() # 队列为空,需要创建或等待 async with self._creation_lock: # 再次检查,防止在获取锁期间其他协程已创建 if not self._available_clients.empty(): client = await self._available_clients.get() elif self._created_count < self._pool_size: # 未达上限,创建新实例 client = await self._create_new_client() else: # 已达上限,等待其他协程归还实例 # 这里我们释放锁,然后等待队列(这是异步等待,不阻塞事件循环) pass # 如果上面因为已达上限而没创建,就会走到这里,等待可用实例 if client is None: client = await self._available_clients.get() # 同样进行健康检查 if not await self._health_check(client): await self._dispose_client(client) return await self.acquire() return client async def release(self, client: fantoccini.Client): """将使用完毕的浏览器客户端归还到池中""" if self._closed: # 如果池已关闭,直接销毁客户端 await self._dispose_client(client) return # 简单清理:关闭所有非必需的标签页(只保留一个) try: pages = await client.windows() if len(pages) > 1: # 保留第一个页面,关闭其他 for page in pages[1:]: await page.close() except Exception as e: self.logger.error(f"清理浏览器页面时出错: {e}") # 清理出错,客户端可能已不稳定,直接销毁 await self._dispose_client(client) return # 放回可用队列 await self._available_clients.put(client) async def _dispose_client(self, client: fantoccini.Client): """安全地关闭并清理一个浏览器客户端""" try: await client.close() if client in self._all_clients: self._all_clients.remove(client) self._created_count -= 1 self.logger.debug(f"销毁浏览器客户端,当前总数:{self._created_count}") except Exception as e: self.logger.error(f"关闭浏览器客户端时出错: {e}") async def close(self): """关闭连接池,清理所有资源""" self._closed = True self.logger.info("正在关闭浏览器连接池...") # 清空队列 while not self._available_clients.empty(): try: client = self._available_clients.get_nowait() await self._dispose_client(client) except asyncio.QueueEmpty: break # 清理可能不在队列中的客户端(例如正在被使用的) for client in list(self._all_clients): await self._dispose_client(client) self.logger.info("浏览器连接池已关闭")这个AsyncBrowserPool类已经具备了核心功能。acquire和release是主要接口。在acquire中,我们实现了“优先复用,按需创建,上限等待”的逻辑。release方法在归还前会尝试清理多余的标签页,这是一个很重要的优化,防止某些任务忘记关闭页面导致内存积累。
实操心得:健康检查不宜太复杂。这里用打开一个空白页来测试,快速且有效。过于复杂的检查(如执行JS)会增加获取资源的延迟。在生产环境中,你可能还需要定期(例如每隔30分钟)对池中所有空闲客户端做一次深度检查。
4. 构建高并发任务执行引擎
有了连接池,我们还需要一个引擎来驱动任务。这个引擎负责从任务源(如队列、列表)中取任务,向连接池借资源,执行任务,处理异常,并归还资源。
4.1 任务执行器的核心循环
下面是一个通用的任务执行器实现。它启动多个工作协程(worker),每个worker独立地从任务队列中拉取任务并执行。
import asyncio import random from typing import Callable, Any class ConcurrentTaskExecutor: """高并发任务执行引擎""" def __init__(self, browser_pool: AsyncBrowserPool, worker_count: int = 3): self.pool = browser_pool self.worker_count = worker_count self._task_queue: asyncio.Queue = asyncio.Queue() self._workers: List[asyncio.Task] = [] self._running = False async def worker_loop(self, worker_id: int): """工作协程的主循环""" while self._running: try: # 从队列获取任务。这里任务是一个 (func, args, kwargs) 的元组 task_item = await self._task_queue.get() if task_item is None: # 收到终止信号 self._task_queue.task_done() break func, args, kwargs = task_item client = None try: # 1. 申请浏览器资源 client = await self.pool.acquire() self.logger.debug(f"Worker-{worker_id} 获取到浏览器客户端") # 2. 执行用户任务,并将client作为第一个参数传入 # 用户函数签名应类似于:async def task_func(client, *args, **kwargs) result = await func(client, *args, **kwargs) # 3. 任务成功,处理结果(这里只是示例,可以存入数据库或另一个队列) self._handle_result(result, worker_id) except asyncio.CancelledError: # 任务被取消,需要清理 raise except Exception as e: # 4. 任务执行异常处理 self.logger.error(f"Worker-{worker_id} 执行任务失败: {e}", exc_info=True) self._handle_error(e, task_item, worker_id) finally: # 5. 无论如何,确保归还浏览器资源 if client is not None: await self.pool.release(client) self._task_queue.task_done() except asyncio.CancelledError: # worker 自身被取消 break except Exception as e: self.logger.error(f"Worker-{worker_id} 循环出现未知错误: {e}", exc_info=True) await asyncio.sleep(1) # 避免错误循环导致CPU飙升 def _handle_result(self, result, worker_id): """处理任务成功的结果(可被子类重写)""" # 示例:简单打印 self.logger.info(f"Worker-{worker_id} 任务完成,结果: {result}") def _handle_error(self, error, task_item, worker_id): """处理任务失败(可被子类重写)""" # 示例:记录错误,或将失败任务重新放入队列 self.logger.error(f"Worker-{worker_id} 处理任务 {task_item} 时出错") async def submit(self, func: Callable, *args, **kwargs): """提交一个任务到队列""" if not self._running: raise RuntimeError("执行器未启动") await self._task_queue.put((func, args, kwargs)) async def start(self): """启动任务执行器""" if self._running: return self._running = True self._workers = [] for i in range(self.worker_count): worker = asyncio.create_task(self.worker_loop(i+1), name=f"Worker-{i+1}") self._workers.append(worker) self.logger.info(f"任务执行器已启动,共 {self.worker_count} 个工作协程") async def stop(self, graceful: bool = True): """停止任务执行器""" self._running = False self.logger.info("正在停止任务执行器...") if graceful: # 优雅停止:等待所有已提交的任务完成 await self._task_queue.join() # 向每个worker发送终止信号 for _ in range(self.worker_count): await self._task_queue.put(None) else: # 强制停止:取消所有worker任务 for worker in self._workers: worker.cancel() # 等待所有worker结束 if self._workers: await asyncio.gather(*self._workers, return_exceptions=True) self._workers.clear() self.logger.info("任务执行器已停止")这个执行器的核心是worker_loop。它完美展示了资源管理的生命周期:acquire->执行任务->release,并且被包裹在try...finally块中,确保即使任务抛出异常,浏览器资源也一定会被归还,这是防止资源泄漏的关键。
4.2 控制并发度的双重阀门
这里有一个重要的概念:系统的总并发度由两个因素决定。
- 浏览器实例并发数:由
AsyncBrowserPool的pool_size控制。这是物理资源的硬限制。 - 任务执行并发数:由
ConcurrentTaskExecutor的worker_count控制。这是逻辑任务的并发度。
通常,worker_count可以大于pool_size。这意味着工作协程数可以多于浏览器实例数。当所有浏览器实例都被占用时,多出来的工作协程会在pool.acquire()处等待。这种设计提供了灵活性:你可以通过增加worker_count来让更多任务处于“就绪”状态,一旦有浏览器释放,立刻就能接上,提高了资源利用率。但worker_count也不宜过大,否则会产生大量等待的协程,增加调度开销。
一个经验公式是:worker_count = pool_size * N,其中N是每个浏览器实例平均可以高效处理的页面任务流数量。对于轻量级任务(如仅抓取页面标题),N可以设为2-3;对于重度交互任务,N设为1更稳妥。
5. 高级优化策略与实战技巧
基础框架搭建好后,我们可以引入一些高级策略来进一步提升性能和稳定性。
5.1 会话隔离与状态清理
浏览器是有状态的。一个任务可能会修改cookie、localStorage,或者留下一些全局变量。如果下一个任务复用了同一个浏览器实例的页面,可能会受到污染。
解决方案是会话隔离。我们不应让多个不相关的任务共享同一个标签页。在AsyncBrowserPool.release()方法中,我们只保留了一个页面。更好的做法是,在acquire之后,由工作协程主动创建一个全新的标签页来执行任务,并在release之前关闭它。这样每个任务都在一个干净的页面环境中运行。
修改worker_loop中的相关部分:
client = await self.pool.acquire() try: # 创建全新的标签页用于此任务 page = await client.new_page() # 执行用户任务,传入page而非client result = await func(page, *args, **kwargs) # 任务完成后,关闭这个专属页面 await page.close() finally: await self.pool.release(client)5.2 超时与熔断机制
网络请求和页面加载可能永远挂起。我们必须为每个操作设置超时。
- 操作超时:Fantoccini的大部分方法都支持
timeout参数。例如await page.goto(url, timeout=30000)。务必为所有网络相关操作设置合理的超时。 - 任务级超时:使用
asyncio.wait_for包裹整个任务函数,设置一个总超时时间,防止单个任务卡住整个工作协程。
try: result = await asyncio.wait_for(func(page, *args, **kwargs), timeout=task_timeout) except asyncio.TimeoutError: self.logger.warning(f"任务执行超时") # 强制关闭当前页面,因为页面可能已处于不可控状态 await page.close() # 注意:此时page已关闭,但client还在,可以归还- 熔断机制:如果某个目标网站频繁超时或无响应,可以临时将其加入黑名单,暂停对其发起请求一段时间,避免浪费资源。
5.3 内存泄漏监控与预防
浏览器自动化是内存泄漏的重灾区。主要来源有两个:未关闭的页面和JavaScript内存堆积。
- 预防未关闭的页面:如前所述,使用
try...finally确保page.close()和pool.release()被调用。可以在worker_loop中增加监控,记录每个worker处理的任务数,如果某个worker长时间未完成任务,可能意味着发生了死锁或无限循环。 - 预防JS内存堆积:避免在页面中执行会产生大量内存占用的JS代码,或者执行后不清理。对于需要长时间运行的任务,定期刷新页面(
page.reload())或导航到一个空白页,可以触发浏览器的垃圾回收。 - 主动监控:可以定期(例如每处理100个任务后)检查浏览器进程的内存占用(这需要操作系统层面的支持,如
psutil库),如果超过阈值,主动重启该浏览器实例。
import psutil import os async def monitor_memory(pid: int, threshold_mb: int = 1024): """监控指定PID进程的内存占用""" try: process = psutil.Process(pid) mem_info = process.memory_info() rss_mb = mem_info.rss / 1024 / 1024 if rss_mb > threshold_mb: return True, rss_mb except (psutil.NoSuchProcess, psutil.AccessDenied): pass return False, 0 # 在连接池的acquire或release中,可以集成检查 # 假设我们能获取到浏览器进程的PID(这通常需要从WebDriver或启动参数中获取) if await monitor_memory(browser_pid, 1024): self.logger.warning(f"浏览器进程 {browser_pid} 内存占用过高,准备重启") await self._dispose_client(client) # 销毁旧的 client = await self._create_new_client() # 创建新的5.4 配置调优实战参数
Fantoccini和底层浏览器(Chrome)有许多可调参数,对性能影响巨大。
Chrome启动参数优化:
browser_args = [ '--headless', # 无头模式,必备 '--disable-gpu', # 禁用GPU,在无头模式下通常不需要 '--no-sandbox', # 禁用沙盒,在容器环境中常需要,但有安全风险 '--disable-dev-shm-usage', # 使用/dev/shm替代/tmp,解决共享内存不足问题(Docker常见) '--disable-setuid-sandbox', '--disable-accelerated-2d-canvas', '--disable-background-networking', # 禁用后台网络,减少干扰 '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-breakpad', '--disable-client-side-phishing-detection', '--disable-component-extensions-with-background-pages', '--disable-default-apps', '--disable-extensions', # 禁用所有扩展 '--disable-features=TranslateUI,BlinkGenPropertyTrees', '--disable-hang-monitor', '--disable-ipc-flooding-protection', '--disable-popup-blocking', '--disable-prompt-on-repost', '--disable-renderer-backgrounding', '--disable-sync', '--enable-automation', # 显示自动化控制标志 '--metrics-recording-only', '--mute-audio', '--no-first-run', '--remote-debugging-port=0', # 禁用远程调试,节省端口 '--window-size=1920,1080', ]注意:
--no-sandbox参数会降低浏览器安全性,仅在你完全信任运行环境(如隔离的Docker容器)且遇到沙盒问题时使用。
Fantoccini连接与操作参数:
- 连接超时:创建
fantoccini.Client时,可以设置request_timeout。 - 页面加载策略:
page.goto()的wait_until参数。'load'等待最彻底但最慢,'domcontentloaded'更快,'networkidle0'(无网络连接)或'networkidle2'(少于2个网络连接)是性能和稳定性的较好折中,但需要根据目标网站特点调整。 - 选择器等待:使用
page.wait_for_selector(selector, timeout=5000)而非sleep,更高效可靠。
6. 典型问题排查与性能调优记录
在实际运行中,你会遇到各种各样的问题。下面是我记录的一些典型场景和解决方案。
6.1 问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 | ||
|---|---|---|---|---|
| 浏览器实例启动失败 | 1. WebDriver服务未启动或端口被占。 2. Chrome/Firefox浏览器未安装或版本不匹配。 3. 系统资源(内存/文件描述符)不足。 | 1. 检查WebDriver(如chromedriver)进程是否运行 (ps aux | grep chromedriver)。2. 确认浏览器可执行文件路径正确,版本与驱动匹配。 3. 使用 ulimit -n检查文件描述符限制,必要时增大。 | ||
| 页面加载超时 (TimeoutError) | 1. 目标网站响应慢或不可达。 2. 页面资源(如大型JS/CSS)过多。 3. 页面内有无限循环或长轮询。 | 1. 增加goto或wait_for_selector的超时时间。2. 调整 wait_until策略为'domcontentloaded',先获取HTML。3. 使用 page.set_request_interception(True)拦截并过滤非必要资源(如图片、字体)。 | ||
| 内存使用持续增长 | 1. 页面未正确关闭 (page.close()未调用)。2. 浏览器实例未释放 ( client.close()未调用)。3. JavaScript内存泄漏。 | 1. 确保所有代码路径(包括异常)都调用了page.close()和pool.release()。2. 实现连接池,限制浏览器实例总数。 3. 定期重启浏览器实例(如每处理N个任务后)。 | ||
| 任务执行速度慢 | 1. 并发控制过严(pool_size或worker_count太小)。2. 网络延迟高。 3. 单个页面操作(如JS执行)耗时过长。 | 1. 在系统资源允许下,适当增加pool_size和worker_count。2. 考虑使用代理或CDN。 3. 分析耗时操作,优化选择器或拆分复杂任务。使用 await page.evaluate()执行JS时避免返回过大对象。 | ||
| 随机性失败或元素找不到 | 1. 页面未完全加载就执行操作。 2. 动态内容导致元素选择器失效。 3. 网站反爬机制(如检测WebDriver)。 | 1. 在关键操作前使用page.wait_for_selector或page.wait_for_function。2. 使用更稳定的选择器(如 > | 任务生产速度远大于消费速度。 | 1. 增加worker_count。2. 优化单个任务执行效率。 3. 为队列设置更大容量,或实现背压机制(当队列满时,暂停生产)。 |
6.2 性能瓶颈分析与定位
当觉得速度不够快时,需要科学地定位瓶颈。
监控指标:记录关键指标。
QPS:每秒完成的任务数。浏览器实例平均利用率:(总任务执行时间 / (浏览器实例数 * 总运行时间))。如果过低,可能是worker_count不足或任务本身有大量空闲等待。任务平均耗时:分解为“等待资源时间”、“页面加载时间”、“数据处理时间”。
使用异步性能分析工具:Python的
cProfile对异步支持不好,可以使用yappi或pyinstrument来 profiling 你的异步代码,找出最耗时的协程或函数。瓶颈可能不在你的代码:网络延迟、目标网站响应速度、代理IP质量,往往是最大的瓶颈。使用工具(如
curl或浏览器开发者工具的网络面板)分析目标网站的响应时间。如果网络是瓶颈,增加并发可能只会导致更多超时。
6.3 一个完整的实战示例:并发爬虫
让我们把上面的所有组件组装起来,实现一个并发爬取文章标题的示例。
import asyncio import logging from typing import List import fantoccini from your_pool_module import AsyncBrowserPool, ConcurrentTaskExecutor logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) async def fetch_article_title(page, url): """具体的爬取任务:获取指定URL的文章标题""" try: # 设置页面超时和加载策略 await page.goto(url, timeout=45000, wait_until='networkidle2') # 等待标题元素出现 await page.wait_for_selector('h1.article-title', timeout=10000) # 获取标题文本 title_element = await page.query_selector('h1.article-title') title = await title_element.get_text() return {'url': url, 'title': title.strip()} except Exception as e: logger.error(f"抓取 {url} 失败: {e}") return {'url': url, 'title': None, 'error': str(e)} async def main(): # 1. 准备任务URL列表 urls_to_fetch = [ 'https://example.com/article/1', 'https://example.com/article/2', # ... 更多URL ] * 20 # 假设有100个任务 # 2. 初始化连接池 (假设最大5个浏览器实例) browser_pool = AsyncBrowserPool( browser_args=[ '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--no-sandbox', '--disable-extensions', ], pool_size=5 ) # 3. 初始化任务执行器 (10个工作协程) executor = ConcurrentTaskExecutor(browser_pool=browser_pool, worker_count=10) # 4. 启动执行器 await executor.start() # 5. 提交所有任务 logger.info(f"开始提交 {len(urls_to_fetch)} 个任务") for url in urls_to_fetch: # 这里使用lambda将url绑定到任务函数 await executor.submit(fetch_article_title, url) # 6. 等待所有任务完成 (优雅关闭) # 注意:这里我们模拟,实际你可能需要另一个机制来知道所有任务已提交 # 例如,可以使用一个单独的“任务完成”信号 await asyncio.sleep(2) # 等待一下,确保任务都入队了 # 更优雅的方式是使用一个计数器,这里为简化,我们直接等待队列空 # 在实际项目中,你可能会有一个独立的“生产者”协程来提交任务,提交完后通知执行器。 # 我们这里简单等待队列处理完(假设没有新任务加入了) start_time = asyncio.get_event_loop().time() while executor._task_queue.qsize() > 0: await asyncio.sleep(0.5) elapsed = asyncio.get_event_loop().time() - start_time logger.info(f"等待任务完成... 队列剩余: {executor._task_queue.qsize()}, 已耗时: {elapsed:.1f}s") # 7. 优雅停止执行器和连接池 await executor.stop(graceful=True) await browser_pool.close() logger.info("所有任务处理完毕") if __name__ == '__main__': asyncio.run(main())这个示例展示了从池化、并发执行到优雅关闭的完整流程。通过调整pool_size和worker_count,你可以找到适合你机器配置和目标网站的最佳并发参数。
最后,性能优化没有银弹。最佳实践来自于持续的监控、测试和迭代。从一个小规模的pool_size开始,逐步增加,同时密切观察系统的内存、CPU和网络IO。记录日志,分析失败原因,不断调整超时、重试和清理策略。经过这样一番打磨,你的Fantoccini应用才能真正扛得住生产环境的高并发压力。
