代理池中可能包含容量差异很大的端点。在整个应用中允许五十个操作,并不能阻止其中四十九个选中同一个代理。

如果策略是“经由每个所选代理最多四个在途操作”,就直接表达该策略。为每个代理标识保留一个信号量,并在消耗该代理容量的工作周围获取它。

limits = {proxy: asyncio.Semaphore(4) for proxy in proxy_urls}

async def fetch(session, proxy, url):
    async with limits[proxy]:
        async with session.get(url, proxy=proxy) as response:
            return await response.read()

此草图假设 asyncio 已被导入,且代理 URL 已经过验证。对于获准的公共代理路由上的公共请求,HTTP 代理列表 为 aiohttp 的代理 URL 提供候选。真实应用还应设置自己的请求超时,并决定需要多少响应数据。把一个大文件完整读入内存是另一项资源决策。

为开销大的部分保留许可

响应头一到就释放信号量,可能使预期的限制落空。流式响应体可能在此后很长时间仍继续使用该连接。应保留许可,直到响应体被消费完毕,或按应用的工作流关闭响应。

嵌套的上下文管理器在异常或取消离开操作时让清理变得可见。避免手动获取而只在成功函数的末尾释放。

连接限制回答的是另一个问题

aiohttp 的连接器限制按连接器文档化的规则限制连接数。它们与应用的信号量并存仍然有用。按目标计算的连接限制,仅凭名称并不能表达由分配给同一个代理的所有目标共享的预算。

还要把并发与请求速率区分开。四个长生命周期请求和四个极短请求使用相同的并发配额,却产生不同的每秒请求数。带速率策略的服务可能需要节奏控制或单独的速率限制器。

为日志和指标选择稳定、非机密的代理标识符。如果两个带凭据的 URL 指向同一个容量池,请决定它们是否应共享一个预算。反之,当运维方如此记录时,不同的端口或账户可以代表彼此独立的限制。

当吞吐量下降时,检查工作在哪里等待:你的信号量、连接器、代理连接建立,还是响应读取。一次性提高所有限制会使这种区分更难,并可能把队列转移到一个你无法控制的代理上。

相关文章