FastAPI接口压测时JWT令牌验证失败问题求助
FastAPI负载测试JWT接口异步报错排查
问题背景
在FastAPI开发接口时,使用Postman进行负载测试发现:带JWT验证的接口出现报错;移除JWT依赖后,压测所有请求均返回成功。所有代码都是同步函数,但报错信息显示存在异步相关问题。
接口代码
@router.get("/task/{id}") # 获取指定任务 def getTask(id:int,db: Session =Depends(get_db),currentUser: dict = Depends(getCurrentUser)): print(currentUser) task = db.query(Task).filter(Task.id == id).first() if task.owner_id == currentUser.id: return(task) else: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'requested task id: {id} not available')
JWT处理代码
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login') SECRET_KEY = "kjjuhdsfvjkhdkfvkdsfjvhlidhfvidhhsf" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 300 def createToken(data:dict): to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes= ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp":expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt def verifyToken(token:str,credentialException): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get('userid') print(f'the logged in user is {username}') if username is None: raise credentialException return username except ExpiredSignatureError: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) except JWTError as e: print(e) def getCurrentUser(token: str = Depends(oauth2_scheme),db : Session =Depends(get_db)): credentialException = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='not authorized',headers={"WWW-Authenticate":"Bearer"}) token = verifyToken(token,credentialException) currentUser = db.query(User).filter(User.email == token).first() return(currentUser)
报错信息
ERROR: Exception in ASGI application Traceback (most recent call last): File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\streams\memory.py", line 98, in receive return self.receive_nowait() File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\streams\memory.py", line 93, in receive_nowait raise WouldBlock anyio.WouldBlock During handling of the above exception, another exception occurred: Traceback (most recent call last): File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\base.py", line 78, in call_next message = await recv_stream.receive() File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\streams\memory.py", line 118, in receive raise EndOfStream anyio.EndOfStream During handling of the above exception, another exception occurred: Traceback (most recent call last): File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 428, in run_asgi result = await app( # type: ignore[func-returns-value] File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 78, in __call__ return await self.app(scope, receive, send) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\applications.py", line 282, in __call__ await super().__call__(scope, receive, send) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\applications.py", line 122, in __call__ await self.middleware_stack(scope, receive, send) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\errors.py", line 184, in __call__ raise exc File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\errors.py", line 162, in __call__ await self.app(scope, receive, _send) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\base.py", line 108, in __call__ response = await self.dispatch_func(request, call_next) File "H:\projects\fastApi-user-login\git code\userLogin\main.py", line 65, in add_process_time_header response = await call_next(request) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\base.py", line 84, in call_next raise app_exc File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\base.py", line 70, in coro await self.app(scope, receive_or_disconnect, send_no_error) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\cors.py", line 83, in __call__ await self.app(scope, receive, send) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\exceptions.py", line 79, in __call__ raise exc File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\exceptions.py", line 68, in __call__ await self.app(scope, receive, sender) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 20, in __call__ raise e File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 17, in __call__ await self.app(scope, receive, send) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\routing.py", line 718, in __call__ await route.handle(scope, receive, send) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\routing.py", line 276, in handle await self.app(scope, receive, send) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\routing.py", line 66, in app response = await func(request) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\routing.py", line 241, in app raw_response = await run_endpoint_function( File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\routing.py", line 169, in run_endpoint_function return await run_in_threadpool(dependant.call, **values) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\concurrency.py", line 41, in run_in_threadpool return await anyio.to_thread.run_sync(func, *args) File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\to_thread.py", line 33, in run_sync return await get_asynclib().run_sync_in_worker_thread( File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\_backends\_asyncio.py", line 877, in run_sync_in_worker_thread return await future File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\_backends\_asyncio.py", line 807, in run result = context.run(func, *args)
排查分析
核心原因
- 数据库会话线程安全问题:FastAPI会把同步函数放到线程池执行,若
get_db依赖未正确实现每个请求独立会话,并发时多个线程共享同一个DB Session,会导致会话状态混乱,尤其JWT验证中也有DB查询,负载高时冲突更明显。 - JWT验证异常处理不完整:
verifyToken函数捕获JWTError后仅打印错误未抛出异常,导致getCurrentUser可能拿到None,后续代码访问currentUser.id会抛出异常,结合线程池资源耗尽触发EndOfStream。 - 线程池资源耗尽:带JWT验证的请求需要额外的DB查询,耗时更长,负载测试时快速占满线程池,AnyIO无法获取空闲线程处理新请求,从而抛出
WouldBlock和EndOfStream。
解决方案
1. 修复DB Session依赖实现
确保get_db为每个请求创建独立会话,使用yield自动管理会话生命周期:
from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" # 替换为你的数据库地址 engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} # SQLite需加此参数,其他数据库可移除 ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() def get_db(): db = SessionLocal() try: yield db finally: db.close()
2. 完善JWT验证的异常处理
在verifyToken中捕获JWTError后抛出凭证异常,避免返回None:
def verifyToken(token:str,credentialException): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get('userid') print(f'the logged in user is {username}') if username is None: raise credentialException return username except ExpiredSignatureError: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token已过期") except JWTError as e: print(e) raise credentialException # 必须抛出异常,终止请求并返回401
3. 优化性能与并发配置
- 缓存用户信息:将JWT验证后的用户信息缓存到Redis,避免每次请求都查询数据库,减少数据库压力:
# 示例:用Redis缓存用户信息 import redis import json redis_client = redis.Redis(host='localhost', port=6379, db=0) def getCurrentUser(token: str = Depends(oauth2_scheme),db : Session =Depends(get_db)): credentialException = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='未授权',headers={"WWW-Authenticate":"Bearer"}) user_id = verifyToken(token,credentialException) # 先查缓存 cached_user = redis_client.get(f"user:{user_id}") if cached_user: return json.loads(cached_user) # 缓存不存在则查DB currentUser = db.query(User).filter(User.email == user_id).first() if not currentUser: raise credentialException # 写入缓存,过期时间设为Token有效期 redis_client.setex(f"user:{user_id}", ACCESS_TOKEN_EXPIRE_MINUTES*60, json.dumps({"id": currentUser.id, "email": currentUser.email})) return currentUser - 调整Uvicorn并发参数:启动时增加工作进程和线程数,提升并发处理能力:
uvicorn main:app --workers 4 --threads 8
4. 检查中间件实现
确保自定义中间件(如报错中的add_process_time_header)是正确的异步函数,避免阻塞事件循环:
# 正确的异步中间件示例 import time from fastapi import Request @app.middleware("http") async def add_process_time_header(request: Request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) return response
内容的提问来源于stack exchange,提问作者gowri shankar
相关产品推荐
相关产品推荐

