
本文不聊CRUD,只聊压测QPS从15飙升到120的过程中,SCF异步运行时、asyncpg连接池、腾讯云VPC内网与混元API调用之间发生的三次“雪崩”及修复实录。
项目背景:基于FastAPI + 腾讯云函数(SCF)的事件型摘要服务,使用asyncpg连接云数据库PostgreSQL,通过HTTP触发器对外提供REST API。
上架次日,突增业务流量(约200并发),SCF实例数迅速膨胀至30+,随之而来的是:
asyncpg.exceptions.TooManyConnectionsError结论先行:不是代码逻辑错,而是没有理解SCF的容器复用机制与异步连接池的生命周期绑定。
大多数AI生成代码会在模块级别创建引擎:
# 错误示范:模块级全局引擎
engine = create_async_engine(DATABASE_URL, pool_size=10, max_overflow=20)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession)SCF特性:当实例复用(Reuse)时,全局变量存活;但SCF的预置并发和动态扩容会同时拉起多个新容器,每个容器都会独立创建包含pool_size=10的连接池。若同时拉起20个容器,瞬间向PG建连200+,直接打爆PG默认最大连接数(100)。
我们必须将连接池大小与实际容器并发数解耦,并利用lifespan上下文确保函数执行完毕后显式回收连接回池,而非依赖垃圾回收。
# app/database.py - 生产级修复
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.config import DATABASE_URL, DB_POOL_SIZE, DB_MAX_OVERFLOW
# 强制限制:pool_size = max(2, cpu_count()),拒绝AI生成的10-20默认值
engine = create_async_engine(
DATABASE_URL,
pool_size=2, # 每个容器仅保留2个长连接
max_overflow=5, # 峰值允许溢出5个,用完即弃
pool_pre_ping=True, # 关键:执行前探测连接存活,防止使用死连接
pool_recycle=300, # 5分钟强制回收,防止PG侧idle_timeout
echo=False
)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
# 关键修复:使用FastAPI的lifespan确保请求结束执行session.close()
@asynccontextmanager
async def get_db_session():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close() # 显式归还连接,而非等待GC压测结果:连接数稳定在(容器数*2)以内,TooManyConnections消失。
AI生成的httpx.AsyncClient每次请求都新建,导致:
socket耗尽文件描述符。我们必须在全局作用域复用httpx.AsyncClient,并配置持久的连接池,同时针对腾讯云混元API的内网VIP进行Host绑定加速。
# app/summarize.py - 修复版
import httpx
from app.config import HUNYUAN_INTERNAL_URL # 内网VPC地址,如 http://hunyuan.internal.tencentcloudapi.com
import asyncio
# 全局单例Client(复用TCP连接)
_httpx_client: httpx.AsyncClient = None
async def get_http_client() -> httpx.AsyncClient:
global _httpx_client
if _httpx_client is None or _httpx_client.is_closed:
# 关键参数:limits=100保持连接池,timeout分级控制
_httpx_client = httpx.AsyncClient(
base_url=HUNYUAN_INTERNAL_URL,
timeout=httpx.Timeout(connect=5.0, read=28.0, write=10.0),
limits=httpx.Limits(max_keepalive_connections=50, max_connections=100),
http2=True, # 混元支持HTTP/2,大幅降低头部延迟
verify=False # 内网VIP若证书校验失败可关闭(生产环境建议注入CA)
)
return _httpx_client
async def generate_summary(markdown: str) -> str:
client = await get_http_client()
payload = {
"model": "hunyuan-pro",
"messages": [{"role": "user", "content": f"摘要:{markdown[:6000]}"}],
"temperature": 0.2
}
# 设置信号量控制并发,防止打爆下游(重要)
semaphore = asyncio.Semaphore(20)
async with semaphore:
resp = await client.post("/v1/chat/completions", json=payload)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]收益:P99延迟从3800ms降至1200ms,DNS耗时降为0(连接复用)。
initializer与异步事件循环冲突RuntimeError: Event loop is closed腾讯云SCF的Python运行时,在自定义镜像或事件函数中,initializer方法(用于预热)若使用asyncio.run(),极易与SCF底层uvicorn的循环冲突。
AI生成的启动脚本scf_bootstrap直接运行uvicorn,没有利用SCF的initializer钩子提前加载模型和连接池。
我们将数据库引擎创建和HTTP Client初始化全部移至全局作用域(SCF加载模块时执行),但不执行异步IO。真正的连接预检放在首次请求中,并利用asyncio.get_running_loop()绑定当前循环。
修改scf_bootstrap,增加--workers 1(SCF容器本身就是单进程,多Worker会抢端口):
#!/bin/bash
exec python3 -m uvicorn app.main:app --host 0.0.0.0 --port 9000 --workers 1 --loop asyncio在main.py中,废除@app.on_event("startup"),改为惰性初始化:
# app/main.py
from app.database import engine
from app.summarize import get_http_client
_is_initialized = False
@app.middleware("http")
async def lazy_init_middleware(request: Request, call_next):
global _is_initialized
if not _is_initialized:
# 仅执行同步探测,异步连接池在首次get_db时自动初始化
async with engine.connect() as conn:
await conn.execute("SELECT 1")
await get_http_client() # 预热HTTP连接
_is_initialized = True
print("SCF Container Warmup Completed.")
return await call_next(request)原理:中间件利用请求上下文的事件循环初始化连接,避免独立asyncio.run()导致的事件循环冲突。
控制台配置SCF的私有网络绑定,使数据库访问走内网,延迟从外网的5ms骤降至0.3ms。但此时发现DNS解析会自动指向内网VIP,但asyncpg的DSN字符串需显式使用host而非IP,否则无法匹配SSL证书。
# config.py 生产环境DSN
# 使用内网域名,而非IP
DATABASE_URL = f"postgresql+asyncpg://{USER}:{PASS}@{DB_HOST_INTERNAL}:5432/{DB_NAME}?ssl=require"针对腾讯云PG(最大连接数限制),我们动态计算容器内的连接数:
import os
# 容器内存限制读取
MEMORY_MB = int(os.environ.get('MEMORY_LIMIT', 512))
DB_POOL_SIZE = max(1, min(4, MEMORY_MB // 128)) # 128MB内存对应1个连接拒绝空口优化,我们在代码中注入trace_id,并同步上报至CLS。
from tencentcloud_cls_sdk import LogClient
import uuid
# 中间件注入Request ID
@app.middleware("http")
async def add_trace_id(request: Request, call_next):
trace_id = request.headers.get("X-Trace-Id", str(uuid.uuid4()))
request.state.trace_id = trace_id
# 将trace_id注入日志
with logger.contextualize(trace_id=trace_id):
response = await call_next(request)
return response通过CLS分析cold_start和db_connect耗时分布,发现pool_pre_ping=True额外消耗了50ms,但避免了80%的“BadConnection”错误,权衡后保留。
指标 | 优化前(AI初始代码) | 优化后(生产调优) |
|---|---|---|
单实例QPS | 15 | 38 |
扩容至20实例总QPS | 120(但连接溢出) | 620(稳定) |
P99 RT | 11.2s | 1.5s |
PG连接数峰值 | 210(触发限流) | 42(稳定安全) |
每月SCF成本(1000万调用) | ¥1200 | ¥480(预置并发+计费粒度优化) |
以上所有“坑”均非AI能直接生成——AI不知道SCF的容器回收策略,不知道asyncpg在uvicorn多Worker下的死锁风险,更不知道腾讯云内网DNS的特殊性。
本文核心价值:如果你正在用AI辅助开发Serverless应用,请务必将如下三板斧手动注入你的提示词工程中:
httpx全局Client及信号量控制。startup事件,使用中间件或懒加载初始化。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。