Flask / MySQL 启动时报 max_user_connections 超限怎么办?
创建 MySQLConnectionPool(pool_size=10) 时就会建立数据库连接,无需等到路由执行查询。报错表明这个数据库账号的并发连接上限为 10。如果其他进程或工具已经占用了连接,应用初始化连接池时就可能失败。
建议先缩小连接池,查清同一账号的连接来源,再修正请求中的资源清理逻辑。增大 pool_size 会增加连接需求,无法解决账号连接配额不足的问题。
为什么还没查询就超过连接数限制?
这段代码位于模块顶层,会在 Flask 加载应用时执行:
cnxpool = mysql.connector.pooling.MySQLConnectionPool(
pool_name="name",
pool_size=10,
autocommit=True,
user=[user name],
password=[password],
host=[host],
database=[database]
)
初始化时,pool_size=10 会让连接池尝试建立 10 个连接,无需等到有 10 个请求。因此,即使没有打开登录页面,应用也已经开始连接 MySQL。
如果这个账号只有 10 个并发连接名额,一个这样的连接池就可能用满额度。MySQL Workbench、其他应用实例、本地开发进程和后台任务也可能使用同一账号。
连接池是进程内对象。多个 worker 或 dyno 会各自创建连接池,即使使用相同的 pool_name,也不会共享数据库连接。开发环境如果启用了 reloader,应用加载时也可能重复初始化,需要结合启动参数和进程情况确认。
仅凭现有代码,还不能确定是哪个进程占用了剩余连接。
先减少连接需求,再恢复应用
先停止不需要的本地 Flask 进程、重复启动的服务和闲置的数据库客户端,避免释放的连接立即被重新占用。
将创建连接池时的大小调小。例如,先用下面的配置验证启动情况:
cnxpool = mysql.connector.pooling.MySQLConnectionPool(
pool_name="name",
pool_size=2,
autocommit=True,
user=DB_USER,
password=DB_PASSWORD,
host=DB_HOST,
database=DB_NAME,
)
DB_USER 等变量需要替换为应用实际读取的数据库配置。这里的 2 只是排查用示例,不是通用推荐值;如果账号已经达到连接上限,即使只申请一个新连接也会失败。
本地排查时,可以明确关闭 reloader:
python -m flask run --no-reload
这有助于排除与重载有关的重复初始化,但仍需计算连接数预算。TEMPLATES_AUTO_RELOAD 配置的是模板重载,不能用它判断 Flask 的进程 reloader 是否启用。
查清连接由谁占用
使用能够连接数据库的客户端执行:
SHOW PROCESSLIST;
SELECT CURRENT_USER(), @@GLOBAL.max_user_connections;
重点查看 User、Host、Command、Time 等字段,结合本地进程、Heroku 实例和数据库客户端逐一确认来源。当前账号能看到哪些连接,取决于数据库权限。
需要注意:
@@GLOBAL.max_user_connections是全局默认限制,账号还可能设置独立的MAX_USER_CONNECTIONS,不能只看全局值就推断该账号的实际额度。Sleep连接可能是连接池中正常保留的空闲连接,不能仅凭这个状态认定发生泄漏。- Workbench 自身也可能占用连接,因此它能连接成功,不代表剩余名额足以初始化一个大小为 10 的连接池。
确认会话已经废弃后,可以在权限允许的情况下终止对应连接。例如,假设已确认连接 ID 为 12345:
KILL CONNECTION 12345;
不要直接照用示例 ID,也不要终止仍在处理业务的会话。如果应用还在运行并自动补建连接,单独执行 KILL 往往只能短暂释放名额。
重启 Heroku dynos 只会影响相应的应用进程,不会清理本地 Flask、Workbench 或其他服务持有的连接。重启后,应用也会重新创建自己的连接池。
修正连接清理逻辑
原代码的清理方式还有几个独立问题:
cur和conn没有提前初始化,获取连接或创建 cursor 失败后,finally可能引用尚未赋值的变量。- 正常分支关闭一次,
finally又关闭一次,存在重复清理。 - 捕获异常后只调用
print(),可能导致请求分支最终没有返回有效响应,也会增加排查难度。
可以将数据库查询收敛到一个函数中,只在 finally 里清理资源:
def fetch_businesses(business_name):
conn = None
cur = None
try:
conn = cnxpool.get_connection()
cur = conn.cursor(dictionary=True)
cur.execute(
"SELECT * FROM businesses WHERE name = %s",
(business_name,),
)
return cur.fetchall()
finally:
try:
if cur is not None:
cur.close()
finally:
if conn is not None:
conn.close()
在现有登录路由完成输入检查后调用:
rows = fetch_businesses(request.form.get("businessName"))
这样即使查询抛出异常,也会尝试关闭 cursor 并归还连接。异常会继续向上传递,供 Flask 的错误处理逻辑记录或处理。如果需要自行捕获,应记录完整异常并返回明确的错误响应。
对于从连接池取得的连接,conn.close() 通常表示将连接归还连接池,底层数据库连接仍然保留。正确归还能防止池内连接被耗尽,但不意味着 MySQL 端的连接数量会立即下降。
顶层 MySQLConnectionPool(...) 的初始化发生在路由执行之前。如果构造连接池时就已失败,路由中的 try / finally 还没有运行,无法修复启动错误。
按所有实例计算连接预算
对于每个应用进程各建一个连接池的部署方式,可以先这样估算:
应用连接需求 ≈ dyno 数量 × 每个 dyno 的 worker 数量 × pool_size
账号总连接需求 ≈ 应用连接需求 + 本地开发、数据库客户端及其他任务连接
例如,两个进程各创建一个大小为 5 的连接池,就需要 10 个连接,没有给 Workbench 或其他任务留余量。实际部署还要考虑重启或发布时,新旧实例是否同时运行。
恢复启动后,继续确认实际 worker 数量、连接池创建位置和同一账号的其他使用方。如果合理缩小连接池后仍无法满足并发需求,再根据数据库承载能力和托管服务权限评估是否提高账号连接额度。连接池过小、并发请求过多时,也可能无法从池中取得连接,需要与数据库端的账号连接上限分别处理。
备注:内容仅供参考。
