一、问题背景:同步代码在大促下的崩溃
去年双11大促前,我们团队负责的订单服务突然报警。监控显示/api/orders接口在500并发下,P95延迟从平时的800ms飙升到4.2秒,数据库连接池(max_connections=50)直接被占满,大量请求排队等待连接。
先看当时的代码逻辑(简化版):
# orders_api.py (改造前)
from flask import Flask, jsonify
import psycopg2
from psycopg2.pool import ThreadedConnectionPool
app = Flask(__name__)
pool = ThreadedConnectionPool(5, 50, dsn="postgresql://user:pass@db/orders")
@app.route('/api/orders/')
def get_orders(user_id):
conn = pool.getconn() # 同步阻塞等待连接
try:
with conn.cursor() as cur:
cur.execute("SELECT * FROM orders WHERE user_id = %s", (user_id,))
rows = cur.fetchall()
return jsonify([dict(row) for row in rows])
finally:
pool.putconn(conn)
问题很明显:
1. 线程阻塞:每个请求占用一个线程,线程池默认200,500并发直接排队
2. 数据库连接浪费:同步等待IO时,连接被持有但不工作
3. 无超时控制:数据库慢查询会无限拖垮整个服务
二、环境与版本:选型说明
改造前先明确技术栈:
Python: 3.10.12
框架: Flask 2.3.3(保留Flask做HTTP层,接入asyncio用flask[async])
数据库驱动: asyncpg 0.28.0(比psycopg2异步版更成熟)
异步服务器: hypercorn 0.15.0(支持asyncio的WSGI服务器)
压测工具: wrk 4.2.0
为什么不用FastAPI?因为现有代码是Flask,不想大改路由层。Flask 2.x支持异步视图函数(async def),配合hypercorn可以跑在asyncio事件循环上。
三、方案设计:异步化改造的三个层次
我的改造分三步走:
1. 数据库层:psycopg2 → asyncpg + 连接池
asyncpg是纯asyncio驱动,支持连接池复用,避免每次请求新建连接。
2. 应用层:同步视图 → 异步视图
Flask 2.2+允许async def视图函数,但要注意不能阻塞事件循环。
3. 并发控制:Semaphore限流
防止数据库连接池被瞬时打爆,用asyncio.Semaphore(20)控制同时执行的数据库查询数。
四、核心实现:before/after代码对比
改造前(同步版,已在上文展示)
改造后(异步版)
# orders_api_async.py (改造后)
import asyncio
import asyncpg
from flask import Flask, jsonify
app = Flask(__name__)
# 全局连接池(启动时初始化)
pool = None
async def init_pool():
global pool
# 连接池大小=20,比原来的50小,但因为是异步复用,实际效率更高
pool = await asyncpg.create_pool(
user='user', password='pass', database='orders',
host='db', min_size=5, max_size=20,
command_timeout=5.0 # 关键:5秒超时,防止慢查询拖死服务
)
# Flask异步视图
@app.route('/api/orders/')
async def get_orders_async(user_id):
# 信号量限制并发查询数,保护数据库
async with semaphore:
try:
# asyncpg直接返回dict列表,比psycopg2的cursor更方便
rows = await pool.fetch(
"SELECT * FROM orders WHERE user_id = $1", user_id
)
return jsonify(rows)
except asyncio.TimeoutError:
return jsonify({"error": "db timeout"}), 504
except asyncpg.PostgresError as e:
return jsonify({"error": str(e)}), 500
# 应用入口:用asyncio.run启动连接池
def create_app():
asyncio.run(init_pool()) # 注意:Flask 2.3+在app上下文外需要手动跑
return app
if __name__ == '__main__':
app = create_app()
# 用hypercorn启动,支持asyncio
from hypercorn.asyncio import serve
from hypercorn.config import Config
config = Config()
config.bind = ["0.0.0.0:5000"]
config.workers = 4 # 多进程,每个进程有自己的事件循环
asyncio.run(serve(app, config))
关键改动点:
- asyncpg.create_pool 用 min_size/max_size 控制连接数,比同步池少一半但够用
- command_timeout=5.0 强制数据库操作5秒超时,防止拖垮整个服务
- asyncio.Semaphore(20) 全局限流,避免瞬时并发打满连接池
五、踩坑与优化:三个让我抓狂的问题
坑1:Flask的async def视图阻塞事件循环
现象:改完异步后,性能反而下降,P95到了6秒。
原因:Flask 2.3的异步视图支持是“伪异步”——如果视图里调用了同步库(比如jsonify)。实际上jsonify是同步的,但它在异步函数里会阻塞事件循环。我最初在异步视图里用了jsonify(rows),其实应该用flask.json.dumps + Response。
解决:
from flask import Response, json
return Response(json.dumps(rows, default=str), mimetype='application/json')
坑2:连接池耗尽导致TimeoutError频繁
现象:压测到400并发时,大量asyncio.TimeoutError(连接池获取超时)。
原因:asyncpg.create_pool 默认 max_size=10(我设了20),但Semaphore设的是20,导致等待连接的协程排队,而连接池已经满了。
解决:Semaphore大小设为max_size - 5(即15),留出缓冲。同时把max_inactive_connection_lifetime调短,防止连接被空占。
坑3:hypercorn的worker数和事件循环冲突
现象:多worker时,每个worker独立事件循环,但init_pool只跑了一次。
解决:把连接池初始化移到worker内部,用hypercorn.worker的lifespan钩子:
async def startup():
global pool
if pool is None:
pool = await asyncpg.create_pool(...)
# hypercorn通过ASGI的lifespan协议调用startup
六、效果数据:压测对比
用wrk压测(10线程,持续60秒):
| 指标 | 同步版(改造前) | 异步版(改造后) | 提升 |
|---|---|---|---|
| P50延迟 | 820ms | 210ms | 3.9x |
| P95延迟 | 4.2s | 1.1s | 3.8x |
| P99延迟 | 6.8s | 2.3s | 2.9x |
| 吞吐量 | 380 req/s | 1210 req/s | 3.2x |
| 数据库连接数 | 50(打满) | 15(稳定) | 3.3x |
测试环境:8核16G云主机,PostgreSQL 14单机。
结论:吞吐提升320%不是玄学,是IO复用的数学结果——同步线程在等待IO时CPU是空闲的,而异步协程在等待时能处理其他请求。
七、总结与建议
这次改造的核心收获:
1. 异步不是银弹:只适合IO密集型场景。如果你的接口是CPU密集(比如图像处理),异步反而增加调度开销。
2. 连接池和限流必须配套:Semaphore大小要略小于连接池max_size,留出10%-20%缓冲。
3. 超时是必须品:command_timeout和HTTP请求超时都要设置,否则一个慢查询能拖垮整个服务。
4. Flask异步视图有坑:别在异步函数里用任何同步IO库,包括jsonify。
如果你也是Flask老项目想优化,建议先压测确认瓶颈在IO等待,再考虑异步改造。如果瓶颈在数据库本身(比如慢查询),先优化SQL和索引,别急着换框架。
最后提醒:生产环境建议用uvicorn或hypercorn替代flask run,它们能正确处理ASGI的异步协议。我的完整代码(含docker-compose)已放在GitHub:github.com/example/async-orders-api,有需要的可以参考。