FastAPI结合SQLAlchemy与PostgreSQL的连接泄漏问题排查求助
我现在碰到了FastAPI项目里的数据库连接泄漏问题——连接没法正常关闭,一直处于idle状态。尽管我已经尽可能谨慎地管理连接,空闲连接数还是持续上涨,最后甚至会触达数据库的最大连接限制,导致服务异常。
项目基本信息
- 框架:FastAPI v0.104.0
- Web服务器:Uvicorn v0.23.0,配置24个worker进程
- 数据库工具:SQLAlchemy v2.0.21
- 数据库:Docker部署的PostgreSQL 16
PostgreSQL的Docker Compose配置
postgres: image: postgres:16 container_name: postgres_db ports: volumes: - postgres_data:/var/lib/postgresql/data environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 300s timeout: 5s retries: 3
SQLAlchemy连接池配置
DATABASE_URL = os.environ.get("DATABASE_URL") engine = create_engine( DATABASE_URL, pool_size=24, # 连接池常驻连接数 max_overflow=12, # 连接池允许临时扩容的额外连接数 pool_timeout=30, # 获取连接的超时等待时间 pool_recycle=1800, # 连接自动回收周期(30分钟) pool_pre_ping=True, # 使用前自动验证连接有效性 )
数据库会话管理器实现
class DatabaseManager: _instance = None def __new__(cls): if not cls._instance: cls._instance = super(DatabaseManager, cls).__new__(cls) cls._instance.engine = engine cls._instance.session_maker = scoped_session( sessionmaker(autocommit=False, autoflush=False, bind=cls._instance.engine) ) return cls._instance def get_session(self): session = self._instance.session_maker() try: yield session finally: session.close() self._instance.session_maker.remove()
登录路由代码
@user_router.post("/login") def login_user( request: Request, login_data: LoginRequest, usermanager=Depends(UserManager), db_session: Session = Depends(DatabaseManager().get_session), ): return usermanager.login_user(request, login_data, db_session)
登录核心逻辑
def login_user(self, request, login_data, db_session: Session): try: user = self.authenticate_user(request, login_data, db_session) if not user: raise HTTPException(status_code=401, detail="Incorrect username or password") if user.is_active: tokens = self.create_tokens_for_user(user) return tokens else: raise HTTPException(status_code=401, detail="User is inactive") except HTTPException as e: self.log_exception("login", e.status_code, e.detail, traceback.format_exc()) raise e except Exception as e: try: self.log_exception("login retry", 500, str(e), traceback.format_exc()) db_session = next(DatabaseManager().get_session()) user = self.authenticate_user(request, login_data, db_session) if user and user.is_active: return self.create_tokens_for_user(user) finally: db_session.close() raise HTTPException(status_code=500, detail="An error occurred while logging in")
问题具体表现
我用以下SQL查询监控PostgreSQL的连接状态:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
某次查询的输出如下:
state | count -------+------- | 5 active | 1 idle | 73
空闲连接数一直在持续上涨!我原本预期最多只有24个空闲连接(对应连接池配置的pool_size),但实际数量不断攀升,直到触达数据库的最大连接限制。
在连接数达到上限前,我还偶尔会遇到这个错误:
sqlalchemy.exc.InvalidRequestError: This session is provisioning a new connection; concurrent operations are not permitted
当连接数触达上限后,就会触发这个致命错误:
psycopg2.OperationalError: connection to server at "postgres", port 5432 failed: FATAL: sorry, too many clients already
额外观察结果
即使空闲连接数已经达到上限,登录函数里的重试逻辑居然还能正常工作。我已经确保所有未使用FastAPI依赖注入的会话都通过finally块手动关闭,而且用的是scoped_session(每个线程维护一个会话),但问题依然存在。
我试过各种能想到的办法,甚至找AI工具帮忙,但都没能解决这个问题。
补充测试数据
我运行了一个应用的克隆实例,完全不处理任何请求,14小时后的连接统计结果符合预期:
p_db=# SELECT state, count(*) FROM pg_stat_activity GROUP BY state; state | count --------+------- | 5 active | 1 idle | 24 (3 rows)
这和scoped_session的特性一致——每个worker线程对应一个连接,正好是24个。
但在处理登录请求的主应用里,连接统计结果却完全失控:
p_db=# SELECT state, count(*) FROM pg_stat_activity GROUP BY state; state | count --------+------- | 5 active | 1 idle | 89
我完全搞不懂为什么主应用的空闲连接数会无限制增长!而且即使出现“too many clients already”的错误,登录的重试尝试居然还能正常执行,这太诡异了。
更新排查进展
我甚至完全移除了连接池的配置,但问题还是一模一样,现在不知道该怎么进一步排查了。
我查询了空闲连接的详细信息,结果行数和空闲连接数一致,这里只复制了部分记录:
p_db=# SELECT pid, state, query, backend_start, query_start, client_addr FROM pg_stat_activity WHERE state = 'idle'; pid | state | query | backend_start | query_start | client_addr -----+-------+----------+-------------------------------+-------------------------------+------------- 35 | idle | ROLLBACK | 2025-01-17 10:08:44.755878+00 | 2025-01-17 10:31:27.250991+00 | 172.30.30.6 36 | idle | ROLLBACK | 2025-01-17 10:08:44.77771+00 | 2025-01-17 10:37:32.826234+00 | 172.30.30.6 37 | idle | ROLLBACK | 2025-01-17 10:08:45.007471+00 | 2025-01-17 10:38:24.326404+00 | 172.30.30.6 38 | idle | ROLLBACK | 2025-01-17 10:08:45.064301+00 | 2025-01-17 10:22:07.79541+00 | 172.30.30.6 39 | idle | ROLLBACK | 2025-01-17 10:08:45.139814+00 | 2025-01-17 10:22:04.841665+00 | 172.30.30.6
其中172.30.30.6是我的FastAPI容器的IP地址。
备注:内容来源于stack exchange,提问作者Hassan

