先说个有点反直觉的结论:GaussDB轻量化版本(25.1.30,内核版本505.2.1)如果出现CPU、内存使用率都不高,但数据库连接数被打满、应用连不上库的情况,大概率不是资源瓶颈,而是连接管理层出了问题。这里说的"连接管理层",包括应用连接池配置、数据库会话槽位、空闲回收策略,以及轻量化版本默认参数这几个维度。
这篇文章是我一次真实排障过程的完整记录。从现象确认、连接状态分类、轻量化版本参数暗坑,到最终的根治方案和复盘建议,我都按实际操作的顺序写出来。不管你现在正守着GaussDB轻量化环境,还是准备把标准版迁移到资源受限场景,这套排查链路基本都能直接复用。
1. 故障现场还原:CPU内存都很闲,连接池却先绷不住了
1.1 从一句“数据库连不上”开始
业务方报障时给的信息不多,就一句话:“应用报错,获取数据库连接失败,连续重试也不行。”我当时第一反应是查常规性能指标:CPU、内存、磁盘IO、慢SQL、锁等待,过了一遍全都没有异常。服务器CPU使用率长期在20%上下,内存也没到告警线,怎么看都不像需要救火的状态。
但我用gsql连进数据库执行了一条查询之后,问题立刻清楚了:
SELECT count(*) FROM pg_stat_activity;结果直接顶到了上限。再看参数:
SHOW max_connections;两个数字一对比,连接数已经贴边了,新连接根本排不上位置。所谓的“连不上数据库”,本质上就是数据库的会话槽位被占满了。
这里有个很容易忽略的细节:连接数打满时,不同的驱动和框架报错形式完全不一样。有的直接报“too many clients already”,有的是“remaining connection slots are reserved for non-replication superuser connections”,还有的是应用侧连接池抛出“connection pool exhausted”。报错长得五花八门,但根源都指向同一个问题:连接槽位用完了。
1.2 资源空闲和连接打满为什么能同时发生
CPU和内存代表的是计算资源和内存资源,连接数却是一个相对独立的管理维度。用一个收费站类比:CPU和内存相当于高速公路本身,连接数则像是收费站的通道数量。道路再空旷,如果每个通道都被不走车的车占着,后面的车照样进不了收费站。
空闲连接几乎不消耗CPU,它占用的主要是一个会话槽位、一份内存,以及可能的线程资源。当应用侧的连接池创建了连接但迟迟不归还,或者数据库侧的空闲回收策略没有真正生效,这些连接就会一直蹲在槽位上。服务器看起来非常“闲”,实际连接资源已经耗尽。
这个场景在轻量化部署环境里更容易出现。轻量化版本本身就是面向资源受限场景发布的,默认的max_connections通常比标准版保守。如果应用侧连接池还是按标准版的习惯去配,连接数打满只是时间问题。
2. 第一刀:先分清是数据库会话数超限,还是操作系统的承载到顶
2.1 数据库侧的max_connections和当前连接数怎么看
排障的第一步永远是确认数字。连接数打满最大的尴尬是:应用已经连不进去了,但管理员往往还能进去。原因是GaussDB在max_connections之外会预留一小部分槽位给超级用户和运维连接。所以出现“普通应用连不上、DBA却能登进去”的情况,不要觉得奇怪,这是保护机制在起作用。
管理员登录后执行:
SHOW max_connections; SELECT count(*) AS total_conn FROM pg_stat_activity;然后按会话状态做个分组,这一步非常关键:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY 2 DESC;如果idle占了大头,说明连接建了没回收。如果active占大头,说明确实在处理业务,那要进一步看慢SQL和并发情况。如果idle in transaction占大头,问题就更麻烦了,这些连接不仅占着槽位,还拖着事务快照,时间长了连数据库的清理工作都会受影响。
我当时看到的现象是:idle连接占比超过70%,而且很多连接建立了很长时间,application_name指向的是应用连接池。看到这个结果,我心里基本有数了——不是业务量大,是连接管理配合出了问题。
2.2 操作系统层:文件描述符和连接队列也可能成为隐形瓶颈
虽然我们这次确认瓶颈在数据库侧,但排查时必须把操作系统层过一遍,否则容易漏判。数据库连接在操作系统层对应一个socket句柄,也受文件描述符数量限制。
常用的检查命令:
# 查看当前进程可打开的文件句柄上限 ulimit -n # 查看系统当前的TCP连接状态统计 ss -s # 查看指定端口的连接数量 netstat -anp | grep <port> | wc -l重点看两部分:一是ulimit -n的值,如果这个值很小,连接还没走到数据库就被系统拒了;二是有没有大量TIME_WAIT或CLOSE_WAIT堆积。TIME_WAIT多通常是短连接高并发导致的,CLOSE_WAIT多则往往说明应用没有正常关闭连接。
轻量化部署的服务器配置本身可能不高,sysctl参数和ulimit往往也没人专门调过,排障的时候顺手确认一下,能排除掉不少干扰项。
2.3 确认上限后,先算这台轻量化节点的内存账
连接数打满之后,很多人的第一反应是“调大max_connections”。我的建议是:先算内存账再做决定。GaussDB每个连接都会分配一定的私有内存,即使空闲连接也不会被完全释放。轻量化版本本身就把内存预算卡得比较紧,直接把max_connections调上去,可能临时止了血,转头内存使用率就开始飙升。
我当时先统计了已有连接占用的内存和系统整体内存余量,简单估算方式是这样:
估算连接占用内存 ≈ 当前连接数 × 单连接平均内存开销单连接平均内存开销可以从数据库监控里捞,不同规格、不同参数配置下差异很大。如果内存余量充足,可以适当调高max_connections;如果余量不足,宁可先清理异常连接,也不要无脑扩容连接数。这条原则在轻量化环境里尤其重要,资源本来就紧,更不能拆东墙补西墙。
3. 第二刀:用pg_stat_activity给连接“称重”,找到谁占着茅坑不拉屎
3.1 把连接按状态归堆,问题基本就现出原形了
GaussDB的pg_stat_activity视图和PostgreSQL生态很接近,很多常用查询可以直接复用。连接数打满后,最先要做的是把连接状态分类:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY 2 DESC;各状态代表的意义要心里有数:
- active:正在执行SQL,这类连接是“真忙”。如果active很多,要结合慢SQL日志看是不是有烂SQL在拖。
- idle:连接空闲,没有正在执行的SQL。连接池里有大量空闲连接是正常的,但空闲连接占比过高且持续不回收,说明连接池或者数据库回收策略有问题。
- idle in transaction:处于事务中,但没有执行下一步。这类连接最危险,它占着槽位、占着事务快照,还会拖累数据库的清理机制。如果这个状态的连接持续堆积,往往和代码里开了事务没提交/没回滚有关。
- fastpath function call等其他状态:不同版本显示不同,同样要单独看。
我们这次就是通过这个查询,一眼看出idle连接是绝对主力。当时服务器有三个应用实例,每个实例连接池都建了一堆空闲连接,而且这些连接几乎不释放。
3.2 从backend_type和等待事件里找深层原因
状态归堆之后,还可以再深入一层,看看连接的来源和等待情况:
SELECT backend_type, state, wait_event_type, wait_event, count(*) FROM pg_stat_activity GROUP BY backend_type, state, wait_event_type, wait_event ORDER BY count(*) DESC;backend_type能区分client backend和后台进程。这里有个容易误判的点:部分监控工具统计的是pg_stat_activity总行数,但这个视图里其实混着autovacuum worker、walsender等后台任务。如果后台进程也占了不少行数,就会造成“连接数占用率很高”的假象。排查的时候最好过滤掉后台进程,只看真正的客户端连接:
SELECT state, count(*) FROM pg_stat_activity WHERE backend_type = 'client backend' GROUP BY state ORDER BY 2 DESC;如果开了流复制或者逻辑复制,replication连接也要单独算进连接预算里,这部分连接平时不起眼,但在连接数逼近上限时会成为压垮骆驼的最后一根稻草。
3.3 连接泄漏的典型现场:QPS不高但连接数天天涨
连接泄漏是“使用率不高但连接打满”最常见的幕后黑手,症状非常有特点:连接数缓慢爬坡,今天100,明天120,后天150,到了某个峰值点直接打满。因为业务量不大,CPU和内存一直很稳定,应用侧也没有明显报错,直到连接数撞线才发现。
定位连接泄漏的核心思路是看连接的年龄和归属:
SELECT pid, state, application_name, client_addr, backend_start, now() - backend_start AS conn_age FROM pg_stat_activity WHERE backend_type = 'client backend' ORDER BY conn_age DESC;如果一个连接的backend_start非常早,而且已经idle了很久,那基本可以判断是某个应用连接没有正确关闭。再配合application_name和client_addr,能直接定位到是哪个实例、哪个模块干的事。
如果连接池有监控,比如HikariCP的metrics、Druid的StatFilter,也可以看连接池里的活跃连接和空闲连接走势。不过很多系统的连接池监控并没有接入,这时候数据库侧的诊断就显得格外重要。
3.4 别忘了认证慢和中间层连接排队
还有一种情况:max_connections明明还有空位,但应用就是连不上。这个时候要考虑认证链路拖慢。一个连接从建立到可用,要经历TCP建链、SSL握手、认证鉴权、分配会话几个步骤,任何一个环节慢,都会造成应用侧的连接请求排队堆积。
排查方式:看数据库日志里有没有大量认证超时或失败记录;看是否配置了SSL,证书校验耗时是否正常;如果前面挂了中间件,还要看中间件自身的连接池是否在排队。
我们这次场景里认证环节没有异常,所以后续精力主要花在会话空闲和回收策略上。但这条线没有排除之前,我不会轻易下结论,避免排查方向跑偏。
4. 轻量化版本暗坑:默认参数、线程池逻辑和内核505.2.1的特殊行为
4.1 默认参数先卡住你,这是轻量化部署的第一课
轻量化版本的定位是“在有限资源里能跑起来”,所以很多参数默认值会比标准版保守。以max_connections为例,标准版可能给到几百甚至上千,轻量化版默认往往只有一两百甚至更少,具体要看实际部署规格和应用配置。
建议先核对以下几个参数:
| 参数 | 作用 | 排查重点 |
|---|---|---|
| max_connections | 数据库最大连接数 | 是否与应用连接池总量匹配 |
| idle_in_transaction_session_timeout | 事务内空闲超时 | 是否存在大量idle in transaction会话 |
| session_timeout | 空闲会话超时 | 空闲连接是否会被自动回收 |
| enable_thread_pool | 线程池开关 | 是否把并发压力转移到线程维度 |
| max_prepared_transactions | 两阶段事务上限 | 是否有残留prepared事务占据槽位 |
不同小版本的参数名可能有差异,以实际执行SHOW的结果为准。轻量化部署下,这些参数往往不会有人专门调过,遇到问题时要先确认它们是不是默认值。
4.2 线程池和连接池不是一回事,别调错对象
“连接打满”很容易让人想到调大max_connections,但在GaussDB里还涉及线程池的逻辑。连接池、会话槽位、线程池是三层不同的概念:
- 应用连接池:应用侧维护的一堆数据库连接,主要目的是复用连接、降低建连开销。
- 数据库会话槽位:数据库侧能同时承载的会话数量上限,由max_connections控制。
- 线程池:数据库内部处理SQL的线程资源,由线程池相关参数控制。
CPU不高但连接数打满时,问题通常出在“应用连接池配置过大”或者“会话回收慢”,不是线程池不够。反过来,如果CPU打满、并发SQL很多,才应该优先看线程池参数。
轻量化版本的线程池模式和标准版也可能有差异,每个会话对应的线程开销需要格外注意。连接数调得过大,线程池规格会跟着变大,内存和CPU都会被拖累。所以我的原则是:先控制连接总量,再考虑并发处理能力,两头不能都开大。
4.3 内核505.2.1上我注意到的两个容易误判的点
先说结论:我不打算下“这个版本有bug”的结论,但从实际环境来看,有两个容易把排查方向带偏的点值得留意。
第一是连接数统计口径。有些监控看的pg_stat_activity总行数,混入了后台进程和内部任务。排查时要用backend_type='client backend'过滤,只看真正的客户端连接,否则会出现“连接数占用90%但业务连接只有一半”的假象。
第二是空闲回收参数的组合生效问题。只设置了session_timeout,但如果应用连接池一直在做心跳或保活查询,数据库会认为这些连接不是空闲的,回收逻辑就一直不触发。排查时要连接池心跳间隔和数据库回收参数一起看,两者的时间差要给足。
这两个点在我们这次排障中都有体现,也是我建议所有轻量化环境使用者提前注意的地方。
5. 根治方案:应用连接池、数据库回收策略、监控告警一起调
5.1 应用侧连接池水位到底怎么定
先把结论给出:数据库连接数预算必须集中规划,不能每个应用各配各的,否则一定会出问题。
推荐公式如下:
每个实例连接池上限 ≤ (max_connections × 预留比例 - 后台/运维连接数) / 应用实例数举例:假设max_connections = 200,预留20%给运维和后台任务,可用业务连接约160。应用有4个实例,每个实例最大连接数就不要超过40。如果之前每个实例配了100,连接数打满就是必然结果。
如果再结合连接池的具体参数,一般关注这几个:
- maximumPoolSize:按上面的公式计算,不要取整取大。
- minimumIdle:建议比最大值小一些,避免频繁创建和销毁连接。
- idleTimeout:要比数据库的空闲回收时间短一点,让连接池先主动回收,不要等数据库来杀连接。
- maxLifetime:要小于数据库和中间件的连接超时时间,防止连接被“杀”后再回写。
- connectionTimeout:不能设太长,否则数据库不可用时,应用线程会全部卡在等待连接上。
这些参数在不同连接池里名称略有差异,但思路是一样的。连接池不是越大越好,它和数据库的容量是一套联动系统。
5.2 数据库侧的空闲回收怎么配才不误伤
数据库侧的回收策略,重点看两个方向:事务内空闲超时和普通空闲会话超时。
如果确认应用连接池不会主动释放空闲连接,建议数据库侧开启兜底回收。比如把事务内空闲超时设成5分钟,普通空闲会话超时设成10分钟。再配合连接池的idleTimeout一起使用,问题会缓解很多。
但这里有一个坑:连接池如果配了心跳机制,每30秒ping一次数据库,那么session_timeout可能永远等不到触发条件。数据库认为连接是“活着”的,所以不回收。解决办法是让连接池的idleTimeout或validationTimeout小于数据库回收时间,让连接池先回收空闲连接,数据库回收机制只作为最后兜底。
我当时调整后的配置思路大致是这样:
| 层级 | 配置项 | 建议方向 |
|---|---|---|
| 连接池 | idleTimeout | 小于数据库空闲回收时间 |
| 连接池 | maxLifetime | 小于中间层/数据库连接超时 |
| 数据库 | idle_in_transaction_session_timeout | 按业务容忍度设置,建议5分钟 |
| 数据库 | session_timeout | 兜底回收,建议10分钟 |
这里有两点要提醒:配置改动要评估对现有业务的影响;不要追求绝对的“零空闲连接”,连接池本身需要维持一定的活跃连接来应对突发流量。
5.3 监控告警和快速止血
连接数问题必须做监控,而且要按状态分开监控,不能只看一个总数。建议关注这几个指标:
- 总连接数 / max_connections 百分比,超过80%预警,90%告警。
- idle会话数占比,超过50%需要关注。
- idle in transaction数量,持续大于0就要告警。
- 新建连接速率,异常突增时提示可能存在连接泄漏。
最朴素的监控脚本可以直接用gsql定期跑:
SELECT count(*) AS total_conn, count(*) FILTER (WHERE state = 'idle') AS idle_conn, count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_tx, max_connections FROM pg_stat_activity, (SELECT setting AS max_connections FROM pg_settings WHERE name = 'max_connections') AS mc WHERE backend_type = 'client backend';生产环境建议接入统一监控平台,但这个SQL截出来的几个指标已经足够做日常巡检。
真到连接数打满需要止血时,动作优先级这样排:
- 用
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE ...清理确认空转的无用会话。 - 内存余量允许时,临时调大max_connections撑过高峰期。
- 快速检查应用发布记录,回滚疑似引入连接泄漏的版本。
- 根因确认后再改正式配置,不能只停留在止血。
提示:执行pg_terminate_backend前务必确认会话身份和状态,不要误杀正在跑业务的长事务连接。
6. 排障复盘:这次问题为什么能躲过所有常规检查
6.1 三条线索叠加,才造成“看着很闲却连不上”
我们这次最后定位下来,是三个因素叠加的结果:
- 应用连接池的maximumPoolSize配得过大,没有按轻量化版本的max_connections做预算。
- 应用代码里有少量连接泄漏,特定操作触发后,连接数会出现锯齿状上涨。
- 数据库侧的空闲回收参数虽然配置了,但没有真正生效,原因是连接池的心跳机制一直维持着连接活性。
单独看任何一条都不致命,三条叠在一起,连接数就在业务量不高的时候慢慢爬到了顶。这也能解释为什么常规检查全都没发现问题:CPU不高、内存不高、慢SQL没有,只有连接数这个维度在悄悄逼近极限。
6.2 轻量化版本上线前,连接管理要做一次体检
给准备使用GaussDB轻量化版本(25.1.30 / 内核505.2.1这类)的团队提几个实操建议:
- 上线前的压测里,专门做一次“长连接保持”测试,看连接池空闲连接是否会被正确回收。
- 把所有应用的连接池参数汇总到一张表里,由DBA统一审核,避免每个项目各自为政。
- 轻量化版本资源有限,连接数宁可配紧一点,也不要让内存预算和连接数互相打架。
- 内核版本升级后,重新检查一遍默认参数,不要沿用旧环境的配置。
最后再说一个我个人体会最深的地方:这次问题最困难的不是修复,而是“敢相信连接数真的会打满”。CPU和内存都正常的时候,人很容易反复去查SQL、查锁、查磁盘,完全没意识到连接管理是一个独立的资源维度。经历过一次之后,我现在看任何数据库告警,都会先花五分钟看连接数的水位和状态分布,再决定要不要深入慢SQL和锁的排查。这个习惯帮我省了很多排查时间,也分享给同样被“使用率不高但连不上”折腾过的朋友。