在 Python 网络应用里,异步编程往往是性能优化中收益最直接的一环。原文作者提到过一个日均请求量超过百万的 API 网关项目:从同步 Flask 架构迁移到异步 aiohttp 之后,服务器资源消耗下降了 70%,平均响应时间从 200ms 缩短到 50ms。这个对比并非玄学,根源在于同步模型把时间都花在了等待 I/O 上,而异步模型把这些等待时间让给了其他请求。下面按事件循环、HTTP 客户端与服务端、异步数据库、WebSocket 的顺序,把原文的代码实现和参数含义重新梳理一遍。
一、同步与异步的差距是怎么量出来的
原文用一个最小可运行脚本做了对比:对同一批 5 个延迟 1 秒的接口,同步版用 requests 顺序请求,异步版用 aiohttp 的 ClientSession 并发请求,最后打印两者的耗时和提升倍数。这段脚本的价值在于它把“阻塞”和“非阻塞”变成了可观测的数字。
- import time
- import asyncio
- import aiohttp
- import requests
- def sync_http_requests():
- urls = ['https://httpbin.org/delay/1'] * 5
- start = time.time()
- for url in urls:
- response = requests.get(url)
- print(f"同步请求完成: {response.status_code}")
- sync_time = time.time() - start
- print(f"同步总耗时: {sync_time:.2f}秒")
- return sync_time
- async def async_http_requests():
- urls = ['https://httpbin.org/delay/1'] * 5
- start = time.time()
- async with aiohttp.ClientSession() as session:
- tasks = [session.get(url) for url in urls]
- responses = await asyncio.gather(*tasks)
- for i, response in enumerate(responses):
- print(f"异步请求 {i} 完成: {response.status}")
- await response.release()
- async_time = time.time() - start
- print(f"异步总耗时: {async_time:.2f}秒")
- return async_time
- async def performance_comparison():
- sync_time = sync_http_requests()
- async_time = await async_http_requests()
- print(f"同步耗时: {sync_time:.2f}秒")
- print(f"异步耗时: {async_time:.2f}秒")
- print(f"性能提升: {sync_time/async_time:.1f}倍")
- if __name__ == "__main__":
- asyncio.run(performance_comparison())
复制代码
两个细节值得注意。其一,aiohttp 的 session.get() 返回的是可等待的请求上下文,放进列表交给 asyncio.gather 才会真正并发发起;其二,拿到响应后要调用 await response.release(),把连接及时归还连接池,否则连接数会被慢慢占满。
二、asyncio 事件循环:调度器的三个关键动作
事件循环是异步程序的发动机,它负责决定哪个协程先跑、哪个 I/O 完成后唤醒谁。原文用 get_running_loop() 拿到当前循环,并演示了 call_soon(尽快执行同步回调)、call_later(延迟执行回调)以及 create_task + gather 的任务管理方式。理解这三点,基本就能看懂异步框架的行为。
- import asyncio
- class EventLoopInsight:
- @staticmethod
- async def demonstrate_event_loop():
- loop = asyncio.get_running_loop()
- print(f"事件循环: {loop}")
- print(f"循环时间: {loop.time()}")
- print(f"是否运行: {loop.is_running()}")
- def synchronous_callback():
- print("同步回调执行")
- loop.call_soon(synchronous_callback)
- def delayed_callback():
- print("延迟回调执行")
- loop.call_later(2, delayed_callback)
- await asyncio.sleep(1)
- print("主协程继续执行")
- @staticmethod
- async def task_management_demo():
- async def worker(name, duration):
- print(f"任务 {name} 开始执行")
- await asyncio.sleep(duration)
- print(f"任务 {name} 完成")
- return f"{name}-结果"
- tasks = [
- asyncio.create_task(worker("任务A", 2)),
- asyncio.create_task(worker("任务B", 1)),
- asyncio.create_task(worker("任务C", 3))
- ]
- results = await asyncio.gather(*tasks)
- print(f"所有任务完成: {results}")
复制代码
这里有个容易踩的点:call_later 注册的延迟回调不会阻塞主协程,主协程仍然按 await asyncio.sleep(1) 的节奏继续往下走;create_task 只是把协程包装成任务排进循环,真正等待结果的是 gather。
三、aiohttp 客户端:连接池与超时是两个必须显式配置的参数
默认配置在低并发下够用,到了高并发就必须调连接池和超时。原文给出的写法是 TCPConnector 控制连接数量,ClientTimeout 控制各类超时,两层配置分工明确:limit 是全局最大连接数,limit_per_host 限制单主机连接数,keepalive_timeout 决定空闲连接保留多久;timeout 一侧 total 是总超时,connect 是建连超时,sock_read 是读取超时。
- import aiohttp
- connector = aiohttp.TCPConnector(
- limit=100,
- limit_per_host=30,
- keepalive_timeout=30
- )
- timeout = aiohttp.ClientTimeout(
- total=60,
- connect=10,
- sock_read=30
- )
- async def client_demo():
- async with aiohttp.ClientSession(
- connector=connector,
- timeout=timeout,
- headers={'User-Agent': 'MyAsyncApp/1.0'}
- ) as session:
- urls = [
- 'https://httpbin.org/json',
- 'https://httpbin.org/uuid',
- 'https://httpbin.org/headers'
- ]
- tasks = [session.get(url) for url in urls]
- responses = await asyncio.gather(*tasks)
- for i, response in enumerate(responses):
- data = await response.json()
- print(f"响应 {i}: {len(str(data))} 字节")
复制代码
如果只设 total 不设 sock_read,遇到对端迟迟不返回数据的情况,请求会一直挂到总超时才失败,排查时表现为“连接是通的,但请求永远不结束”。原文把三个超时拆开配置,正是为了避免这种互相牵连。
四、aiohttp 服务端:路由 + WebSocket 处理器
服务端部分由 web.Application 组织,通过 app.router.add_get 把路径绑定到协程处理器;普通接口用 web.Response 或 web.json_response 返回,WebSocket 接口用 web.WebSocketResponse 完成握手后异步迭代消息。
- import time
- import aiohttp
- from aiohttp import web
- async def handle_root(request):
- return web.Response(text="Hello, Async World!")
- async def handle_api(request):
- data = {'status': 'ok', 'timestamp': time.time()}
- return web.json_response(data)
- async def handle_websocket(request):
- ws = web.WebSocketResponse()
- await ws.prepare(request)
- async for msg in ws:
- if msg.type == aiohttp.WSMsgType.TEXT:
- await ws.send_str(f"ECHO: {msg.data}")
- elif msg.type == aiohttp.WSMsgType.ERROR:
- print(f"WebSocket错误: {ws.exception()}")
- return ws
- app = web.Application()
- app.router.add_get('/', handle_root)
- app.router.add_get('/api', handle_api)
- app.router.add_get('/ws', handle_websocket)
- # web.run_app(app, host='127.0.0.1', port=8080)
复制代码
WSMsgType.ERROR 分支里取 ws.exception() 打印,是排查 WebSocket 断连原因最省事的办法:客户端异常断开、协议错误都会从这里冒出来,而不是静默断开。
五、异步数据库:连接池、事务和批量写入
databases 库的 Database 对象同时承担连接池和查询入口两个角色,min_size / max_size 就是池的上下水位。原文的 AsyncDatabaseManager 封装了 connect、建表、复杂查询与基准测试,其中最能体现异步收益的是“顺序插入”和“事务内批量插入”的对比。
- from databases import Database
- class AsyncDatabaseManager:
- def __init__(self, database_url, min_connections=2, max_connections=20):
- self.database_url = database_url
- self.min_connections = min_connections
- self.max_connections = max_connections
- self.db = None
- async def connect(self):
- self.db = Database(
- self.database_url,
- min_size=self.min_connections,
- max_size=self.max_connections
- )
- await self.db.connect()
- await self._create_tables()
- async def _create_tables(self):
- create_table_query = """
- CREATE TABLE IF NOT EXISTS users (
- id SERIAL PRIMARY KEY,
- username VARCHAR(50) UNIQUE NOT NULL,
- email VARCHAR(100) UNIQUE NOT NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- );
- CREATE TABLE IF NOT EXISTS messages (
- id SERIAL PRIMARY KEY,
- user_id INTEGER REFERENCES users(id),
- content TEXT NOT NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- );
- """
- await self.db.execute(create_table_query)
- async def perform_benchmark(self, iterations=1000):
- async def sequential_insert():
- start_time = time.time()
- for i in range(iterations):
- query = "INSERT INTO users (username, email) VALUES (:username, :email)"
- values = {"username": f"user_{i}", "email": f"user_{i}@example.com"}
- await self.db.execute(query, values)
- return time.time() - start_time
- async def batch_insert():
- start_time = time.time()
- values_list = [
- {"username": f"batch_user_{i}", "email": f"batch_{i}@example.com"}
- for i in range(iterations)
- ]
- async with self.db.transaction():
- query = "INSERT INTO users (username, email) VALUES (:username, :email)"
- for values in values_list:
- await self.db.execute(query, values)
- return time.time() - start_time
- sequential_time = await sequential_insert()
- batch_time = await batch_insert()
- print(f"顺序插入耗时: {sequential_time:.2f}秒")
- print(f"批量插入耗时: {batch_time:.2f}秒")
- print(f"性能提升: {sequential_time/batch_time:.1f}倍")
- await self.db.execute("DELETE FROM users WHERE username LIKE 'user_%' OR username LIKE 'batch_user_%'")
- async def disconnect(self):
- if self.db:
- await self.db.disconnect()
复制代码
事务块 async with self.db.transaction() 的作用不只是提速:把一批写入包在同一个事务里,可以减少提交次数,同时保证要么全成功、要么全回滚。原文的 complex_query_example 用事务先插用户、再插 5 条消息,最后用 JOIN 查询验证,就是典型的“写一致 + 读关联”组合用法。
六、SQLAlchemy 异步引擎:读写分离与连接池监控
当项目已经使用 ORM 时,可以换成 create_async_engine 走异步驱动。原文的 MultiDatabaseManager 同时维护主库和从库两个引擎,用 read_only 参数决定从哪个引擎取会话,并用 @asynccontextmanager 统一处理 commit 和 rollback。
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
- from contextlib import asynccontextmanager
- class MultiDatabaseManager:
- def __init__(self, primary_db_url, replica_db_url=None):
- self.primary_engine = create_async_engine(
- primary_db_url,
- echo=False,
- pool_size=10,
- max_overflow=20,
- pool_pre_ping=True
- )
- self.replica_engine = None
- if replica_db_url:
- self.replica_engine = create_async_engine(
- replica_db_url,
- echo=False,
- pool_size=5,
- max_overflow=10,
- pool_pre_ping=True
- )
- @asynccontextmanager
- async def get_session(self, read_only=False):
- engine = self.replica_engine if read_only and self.replica_engine else self.primary_engine
- async with AsyncSession(engine) as session:
- try:
- yield session
- await session.commit()
- except Exception:
- await session.rollback()
- raise
- async def connection_pool_metrics(self):
- primary_pool = self.primary_engine.pool
- print(f"连接池大小: {primary_pool.size()}")
- print(f"已检入连接: {primary_pool.checkedin()}")
- print(f"已检出连接: {primary_pool.checkedout()}")
- print(f"溢出连接: {primary_pool.overflow()}")
复制代码
这里的参数含义需要明确:pool_size 是常驻连接数,max_overflow 是高峰时允许临时超出的连接数,pool_pre_ping=True 会在取连接时先探活,避免拿到已失效的旧连接报错。connection_pool_metrics 打印的 checkedout 与 overflow 是判断“是不是连接池不够用”的直接依据——如果 checkedout 长期贴着上限、overflow 持续增长,就该考虑调大 pool_size 或者缩短会话持有时间。
七、WebSocket 连接管理
实时推送场景下,服务端需要维护“谁在线、往谁发”的映射关系。原文用 WebSocketManager 类持有 connections 字典(键为标识,值为 WebSocketResponse 集合),这也是异步框架里管理长连接的常见结构:连接建立时登记,断开时移除,广播时遍历集合逐个 send_str。处理消息循环时用 async for msg in ws 接收,并按 WSMsgType.TEXT / ERROR 分支处理,与上面服务端示例的写法一致。
八、优化与排查清单
按原文涉及的技术点归纳,落地时可以按这几条对照:
1. 客户端连接池要设上限。limit 与 limit_per_host 不设,高并发下容易把连接数打满或触发对端限流。
2. 三类超时分开配。total、connect、sock_read 各管一段,排查“卡住不返回”时先看 sock_read。
3. 响应记得 release。并发请求拿到结果后释放连接,让连接回到池里复用。
4. 批量写入包事务。事务既能减少提交开销,也能在异常时统一回滚。
5. 连接池指标要能看见。size / checkedin / checkedout / overflow 四个数字足以定位池是否耗尽。
6. 长连接异常要打印 exception。WebSocket 的 ERROR 分支不取 ws.exception(),线上断连基本无从下手。
7. 生产环境关掉 SQL echo。原文在 create_async_engine 里显式写了 echo=False 并注明生产环境应如此,避免日志把 SQL 全量刷出来拖慢性能。
整体来看,原文的代码骨架覆盖了异步网络应用的主要环节:事件循环负责调度,aiohttp 负责收发,异步数据库驱动负责持久化,WebSocket 负责实时推送。把这些组件的参数配好、把连接与事务管好,同步架构迁移到异步架构时所说的“十倍级提升”才有现实基础;反过来,如果连接池不设上限、超时不分开、连接不释放,即使换成了 asyncio,也只是把阻塞换了个地方发生。 |