利用Python aiohttp和tenacity实现高效的异步HTTP请求和重试机制

571 阅读5分钟

1. 背景介绍

在Web服务中,频繁的HTTP请求是常见的操作。当需要同时处理大量请求时,传统的同步编程方式会导致程序的执行效率低下,甚至可能出现超时或资源耗尽的情况。为了解决这些问题,我们可以使用异步编程,通过并发处理请求来提高程序的执行效率。

2. 异步编程与asyncio

asyncio是Python标准库中的异步I/O框架,它提供了一套用于编写单线程并发代码的工具。通过使用asyncio,我们可以在单个线程中处理多个并发任务,从而避免了多线程带来的复杂性和开销。

3. 使用aiohttp进行异步HTTP请求

aiohttp是一个基于asyncio的HTTP客户端和服务器库。它支持异步的HTTP请求和响应处理,能够高效地处理大量的并发请求。在我们的代码中,aiohttp被用来发送HTTP请求并处理响应。

4. 使用tenacity进行重试机制

在网络请求中,可能会遇到各种临时性的错误,如网络波动、服务器短暂不可用等。为了增强程序的健壮性,我们可以使用tenacity库来实现自动重试机制。tenacity提供了一系列的重试策略,如固定间隔重试、指数退避等,能够帮助我们在遇到错误时自动重试请求。

5. 代码实现

5.1 设置重试参数

首先,我们定义了重试次数(retry_count)、重试间隔(wait_time)和请求超时时间(time_out)。这些参数将在后续的代码中用于配置重试机制和请求超时处理。

# 重试次数
retry_count = 3
# 重试间隔(秒)
wait_time = 2
# 请求超时(秒)
time_out = 300

5.2 处理HTTP响应

handle_response函数中,我们检查HTTP响应的状态码,并尝试将响应内容解析为JSON格式。如果遇到任何异常,我们将打印错误信息并重新抛出异常。

async def handle_response(resp):
    """
    处理HTTP响应的函数。
    :param resp: aiohttp的响应对象
    :return: 解析后的JSON数据
    """
    try:
        # 检查响应的状态码,如果不是2xx,抛出异常
        resp.raise_for_status()
        # 将响应内容解析为JSON格式
        res = await resp.json()
        return res
    except Exception as e:
        # 打印错误信息并重新抛出异常
        print(f"Error: {e}")
        raise e

5.3 使用tenacity实现重试机制

call_api函数中,我们使用tenacity装饰器来实现重试机制。如果请求失败,tenacity会根据我们设定的重试策略自动重试请求。

@tenacity.retry(wait=tenacity.wait_fixed(wait_time), stop=tenacity.stop_after_attempt(retry_count), reraise=True)
async def call_api(session, method, api, header, data):
    """
    发送HTTP请求的函数,支持GET和POST方法,并使用tenacity进行重试。
    :param session: aiohttp的ClientSession对象
    :param method: HTTP方法,支持"GET"和"POST"
    :param api: 请求的URL
    :param header: 请求头
    :param data: 请求参数
    :return: 响应的JSON数据
    """
    try:
        if method == "POST":
            # 发送POST请求
            async with session.post(api, headers=header, data=data, timeout=time_out) as resp:
                return await handle_response(resp)
        elif method == "GET":
            # 发送GET请求
            async with session.get(api, headers=header, data=data, timeout=time_out) as resp:
                return await handle_response(resp)
        else:
            # 不支持的HTTP方法
            raise ValueError("Unsupported HTTP method")
    except asyncio.TimeoutError:
        # 处理请求超时的情况
        raise "TimeoutError"

5.4 并发请求处理

pr_main函数负责管理并发请求。我们使用asyncio.Semaphore来控制并发请求的数量,并通过aiohttp.TCPConnector来限制每个主机的连接数量。这样可以避免因过多的并发请求导致服务器过载或网络拥塞。

async def pr_main(url, header, data, method, limit_per_host=10, concurrent_quantity=10):
    """
    主函数,负责管理并发请求。
    :param url: 请求的URL,可以是单个URL或URL列表
    :param header: 请求头
    :param data: 请求参数,可以是单个数据或数据列表
    :param method: HTTP方法,支持"GET"和"POST"
    :param limit_per_host: 每个主机的最大连接数
    :param concurrent_quantity: 并发请求数量
    :return: 所有请求的结果列表
    """
    tasks = []
    # 使用Semaphore控制并发请求数量
    semaphore = asyncio.Semaphore(concurrent_quantity)
    # 使用TCPConnector限制每个主机的连接数
    connector = aiohttp.TCPConnector(limit_per_host=limit_per_host)
    # 创建aiohttp的ClientSession对象
    async with aiohttp.ClientSession(connector=connector) as session:
        if isinstance(url, list):
            # 如果URL是列表,遍历每个URL
            for api in url:
                # 创建异步任务
                task = asyncio.ensure_future(call_api(session, method, api, header, data))
                tasks.append(task)
        else:
            if isinstance(data, list):
                # 如果data是列表,遍历每个数据
                for item in data:
                    # 创建异步任务
                    task = asyncio.ensure_future(call_api(session, method, url, header, item))
                    tasks.append(task)
            else:
                # 如果URL和data都不是列表,则无需进行异步请求
                raise ValueError("URL与DATA都不为列表,无需进行异步请求!")
        # 并发执行所有任务,并收集结果
        results = await asyncio.gather(*tasks, return_exceptions=True)
        return results

5.5 启动异步请求

最后,我们通过run_request函数来启动异步请求。这个函数会调用pr_main函数,并等待所有异步任务完成。

def run_request(url, header, data, method, limit_per_host=10, concurrent_quantity=10):
    """
    启动异步请求的入口函数。
    :param url: 请求的URL,可以是单个URL或URL列表
    :param header: 请求头
    :param data: 请求参数,可以是单个数据或数据列表
    :param method: HTTP方法,支持"GET"和"POST"
    :param limit_per_host: 每个主机的最大连接数
    :param concurrent_quantity: 并发请求数量
    :return: 所有请求的结果列表
    """
    # 使用asyncio.run运行异步任务
    result = asyncio.run(pr_main(url, header, data, method, limit_per_host, concurrent_quantity))
    return result

6. 完整代码

总结

通过结合asyncioaiohttptenacity,我们实现了一个高效、可靠的异步网络请求框架。这个框架能够处理大量的并发请求,并且在遇到临时性错误时自动重试,从而提高了程序的健壮性和执行效率。在实际应用中,开发者可以根据具体需求调整重试策略、并发数量等参数,以适应不同的应用场景。