连接管理
CloudBase PostgreSQL 提供两种连接入口:直连数据库实例,或经平台的连接池服务接入。两者的连接数占用和会话语义不同,选错入口容易导致连接数耗尽或功能不符合预期。
选择入口前,先判断业务属于哪类场景:客户端数量可控、依赖会话状态时用直连;客户端数量多、连接短且请求密集时经连接池接入。
直连与连接池
两种入口的定义:
- 直连实例:客户端通过 PostgreSQL 协议直接连接到数据库实例,请求不经过中间层。
- 经连接池接入:客户端先连接到平台提供的连接池服务,由连接池再连接到数据库实例。连接池在服务端复用与实例之间的连接,因此多个客户端可以共享少量真实连接。
区别在于接入目标,而不在应用代码的写法。控制台会提供对应的连接地址和端口,直连地址与连接池地址不同,具体以控制台展示为准。
| 对比项 | 直连实例 | 经连接池接入 |
|---|---|---|
| 连接目标 | 数据库实例 | 平台连接池服务 |
| 实例上的真实连接数 | 每个客户端连接对应一条 | 由连接池与实例之间的连接数决定 |
| 会话连续性 | 连接持有期间保持同一会话 | 跨请求不保证落在同一会话 |
| 受实例连接数上限约束 | 直接受约束 | 客户端数量与实例连接数解耦 |
| 典型场景 | 迁移、运维、长事务、依赖会话特性 | Web API、云函数、高并发短请求 |
代码层面通常只需替换连接地址:
import pg from "pg";
// 直连实例
const direct = new pg.Pool({
host: process.env.PG_DIRECT_HOST,
port: Number(process.env.PG_DIRECT_PORT),
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
});
// 经连接池接入
const pooled = new pg.Pool({
host: process.env.PG_POOL_HOST,
port: Number(process.env.PG_POOL_PORT),
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
});
直连的使用场景
依赖会话状态的操作。 预处理语句(prepared statements)、会话级变量、临时表、游标和 advisory lock 都依附于当前会话。直连期间连接持续持有,这些状态可以跨语句生效。
长事务与批量处理。 数据迁移、历史数据回填、批量导入导出需要显式控制事务边界,执行时间较长,直连便于掌握进度和回滚。
运维与诊断。 使用 psql、控制台的数据管理工具排查问题时,需要看到实例上的真实连接和执行计划,直连才能准确反映实例侧状态。
客户端数量可控的常驻服务。 服务实例数固定、每个实例只开少量连接时,直连占用的配额可以预估。
需要留意:直连会直接消耗实例的连接数配额,配额上限由实例规格决定。客户端数量不可控、或服务会弹性伸缩时,应改用连接池接入。
连接池的使用场景
连接池在服务端复用连接,客户端连接数与实例真实连接数解耦。
- 客户端数量多或不可控。 Web API、多人协作工具、对外网关等场景,客户端连接数随流量增长,直连容易触及实例连接数上限。
- 云函数等短生命周期运行环境。 实例被反复创建和销毁,每次建连都会占用配额,经连接池可以平滑这部分波动。
- 请求密集、单次请求耗时短。 以查询为主、快速返回的请求,连接复用收益明显。
- 需要为突发流量预留余量。 连接池作为缓冲层,避免流量峰值直接压到实例。
经连接池接入时需要留意:
- 不保证同一会话连续。会话级状态可能不会延续到下一次请求,具体行为以控制台提供的连接说明为准。
- 事务内的多条语句必须使用同一个客户端连接,否则事务边界会失效。
客户端连接池与服务端连接池
上一节的连接池是服务端连接池,属于接入入口。应用内部还可以维护客户端连接池,两者解决不同层面的问题:
| 层次 | 作用 | 影响范围 |
|---|---|---|
| 服务端连接池 | 复用与实例之间的连接,降低实例真实连接数 | 实例的连接数配额 |
| 客户端连接池 | 复用客户端到目标地址的连接,减少建连开销 | 单个应用进程的连接数 |
两层可以叠加使用:经连接池接入的同时,在应用内维护 pg.Pool。也可以只使用其中一层。
连接复用
以下针对应用内的客户端连接池。在 Node.js 中,应将连接池定义在模块顶层,让同一个运行时实例复用连接。
import pg from "pg";
export const pool = new pg.Pool({
host: process.env.PGHOST,
port: Number(process.env.PGPORT || 5432),
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
max: 5,
});
不要在每次请求内部创建新的连接池。
连接池大小
这里指客户端连接池。它并不是越大越好,需要按部署实例数估算客户端连接总数:
客户端连接总数 = 单实例连接池上限 × 最大实例数
如果云函数最多同时扩容到 20 个实例,每个实例连接池 max=5,理论上可能建立 100 条客户端连接。
直连时这些连接会直接占用实例的连接数配额,需要与实例规格核对。经连接池接入时,客户端连接由服务端连接池承接,不会等量转化为实例连接,但客户端池设置过大仍会造成请求排队。
释放连接
从客户端连接池手动取出的 client 必须释放。
const client = await pool.connect();
try {
const result = await client.query("select now()");
return result.rows;
} finally {
client.release();
}
普通查询可以直接使用 pool.query(),由连接池自动借还连接。
超时设置
建议为业务查询设置合理超时,避免慢查询长期占用连接。
set statement_timeout = '5s';
也可以在应用层设置请求超时和重试策略。重试只适合幂等操作,写操作重试前需要确认是否可能重复写入。
监控连接
可以通过 pg_stat_activity 查看当前连接。直连时看到的是客户端连接,经连接池接入时看到的是连接池与实例之间的连接,两者数量含义不同。
select state, count(*)
from pg_stat_activity
group by state
order by count(*) desc;
定位长时间运行的查询:
select pid, state, now() - query_start as duration, query
from pg_stat_activity
where state <> 'idle'
order by duration desc
limit 20;
常见问题
- 连接数持续增长:检查是否每次请求创建新连接池,或手动连接未释放。
- 大量 idle 连接:调小客户端连接池上限和 idle 超时时间。
- 查询排队:检查慢查询、锁等待和连接池上限。
- 间歇性连接失败:检查网络、实例连接数上限和服务实例扩容情况。
- 客户端数量不多但实例连接被打满:确认是否有大量短连接绕过连接池直连实例。
- 预处理语句报错或会话状态丢失:当前接入方式可能不支持跨请求保持会话,考虑改用直连。