Skip to content
GitHub

Database too many client already

偶然一次拉起后端项目, 执行报错 connection failed: connection to server at "xx.xx.xx.xx", port 5432 failed: FATAL: sorry, too many clients already, 由报错可知数据库已到达连接上限,无法再申请连接
使用 Postgres 数据库, Postgres 默认最大连接数为 100

有两种方法解决

  • 增加数据库连接上限
  • 减少不必要的项目连接数据库

我回想了之前一些连接数据库的项目和脚本有三类

  • Grafana 看板有多个图表显示数据库数据
  • FastAPI 后端项目, 使用封装 psycopg_pool 的 Database 类, 默认最大 10 个连接
  • 定时任务脚本, 使用封装 psycopg_pool 的 Database 类, 默认最大 10 个连接
-- 网上找到查询连接的 sql, 
SELECT * FROM pg_stat_activity;

根据 sql 执行结果, 发现有 82 个连接是同一个 IP, 其余 IP 只有零星 1 ~ 2 条, 数据库和多个任务都部署在该 IP 机台
有两个连接带有 sql 语句确认是 Grafana 上设置的 sql, 查看 Grafana 数据源设置, Connection limit 设置默认值 100, 修改为 10, 再次查看连接数没有减少
分析 Database 类, 将 max_size 从 10 减小到 5, 重启部分后端或直接关闭, 连接数仍然没有减少

class Database:
    """ database module """
    is_init = False
    
    @classmethod
    def init(cls) -> None:
        """ 数据库初始化连接 """
        if cls.is_init:
            return
        
        for conn in DATABASE:
            if getattr(cls, conn.dbname, None) is not None:
                continue

            setattr(cls, conn.dbname, ConnectionPool(
                min_size=1,
                max_size=5,
                conninfo=" ".join(f"{k}={v}" for k, v in asdict(conn).items())
            ))

        cls.is_init = True
        atexit.register(cls.close)

    @contextmanager
    def __new__(cls, dbname: DBName="postgres"):
        cls.init()
        pool: ConnectionPool = getattr(cls, dbname, None)
        conn = None
        try:
            with pool.connection() as conn:
                with conn.cursor(row_factory=dict_row) as cursor:
                    yield cursor
        except Exception as err:
            logger.error(f"database execute error: {err}")

    @classmethod
    def close(cls):
        for conn in DATABASE:
            pool = getattr(cls, conn.dbname, None)
            pool and pool.close()

思考: Database 类使用 with 语句, 会自动关闭连接, 且通过 atexit 注册了关闭连接, 关闭项目连接也会关闭, 是否还有其它连接?
在机台查找所有 python 项目逐个排查

# 发现大量定时任务脚本进程(crontab 定时任务脚本在固定时间执行后会自动退出)
ps -ef |grep python
root     3642348 3642346  0 Apr09 ?        00:00:00 /bin/sh -c /root/miniforge3/envs/fastapi/bin/python /root/desktop/schedule/dump.py # dump
root     3741118 3741116  0 Jun29 ?        00:00:00 /bin/sh -c /root/miniforge3/envs/fastapi/bin/python /root/desktop/schedule/dump.py # dump
root     3741806 3741803  0 Jun29 ?        00:00:00 /bin/sh -c /root/miniforge3/envs/fastapi/bin/python /root/desktop/schedule/version_history.py # version
root     3847231 3847228  0 Jul13 ?        00:00:00 /bin/sh -c /root/miniforge3/envs/fastapi/bin/python /root/desktop/schedule/version_history.py # version
root     3906963 3906959  0 Jun10 ?        00:00:00 /bin/sh -c /root/miniforge3/envs/fastapi/bin/python /root/desktop/schedule/version_history.py # version
root     4042444 4042441  0 May21 ?        00:00:00 /bin/sh -c /root/miniforge3/envs/fastapi/bin/python /root/desktop/schedule/version_history.py # version

强制关闭定时任务进程后, 冗余连接果然减少, 可以确定就是定时任务未正常退出导致连接未正常断开
重复测试发现, 当定时脚本使用了多线程执行, 当某个线程执行 raise 错误就会卡死该线程一直不退出, 脚本卡死, 连接无法释放