PAYATHON 2026

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 数量、连接池创建位置和同一账号的其他使用方。如果合理缩小连接池后仍无法满足并发需求,再根据数据库承载能力和托管服务权限评估是否提高账号连接额度。连接池过小、并发请求过多时,也可能无法从池中取得连接,需要与数据库端的账号连接上限分别处理。

备注:内容仅供参考。