一个连接执行了 BEGIN,修改或读取了一些数据,然后因为应用异常、人工操作中断或连接管理疏漏,再也没有提交。它看起来只是“挂着”,实际上可能持续持有锁、阻碍清理旧版本,最终让一次普通的连接遗忘演变成锁等待、表膨胀和请求堆积。
PostgreSQL 的 idle_in_transaction_session_timeout 正是用来限制这类会话:当会话已经进入事务、但在指定时间内没有继续执行语句时,服务器会终止该连接。
“idle”不等于没有成本
PostgreSQL 会话处于 idle in transaction,通常意味着事务已经开始,但客户端暂时没有发送下一条命令。此时数据库后端没有消耗大量 CPU,却可能保留两类重要状态:
- 尚未释放的行锁或表锁,使其他事务进入等待。
- 较旧的事务快照,令 VACUUM 无法回收仍可能对该事务可见的旧行版本。
故障往往是级联发生的。一个会话先阻塞 VACUUM 或写请求,被阻塞的请求继续占用连接池,连接池耗尽后,更多 API 请求排队或超时。只看 CPU 和连接总数,很容易错过真正的起点。
需要区分几个相近状态:
idle:连接没有执行语句,也不在事务中。idle in transaction:事务尚未结束,客户端没有发送新语句。active:语句仍在运行,即使运行很久,也不属于事务内空闲。
因此,这个参数不是通用的慢查询超时。运行时间过长的 SQL 应结合 statement_timeout 管理;等待锁过久则可以评估 lock_timeout。
把保护设在合适的作用域
idle_in_transaction_session_timeout 是 PostgreSQL 的运行参数,可以按系统、数据库、角色或会话设置。具体数值不能脱离业务决定:在线 API 的事务通常应很短,数据迁移工具或人工排障会话可能需要更宽松的窗口。
可以先查看当前值:
SHOW idle_in_transaction_session_timeout;
临时验证时,可以只修改当前连接:
SET idle_in_transaction_session_timeout = '30s';
SHOW idle_in_transaction_session_timeout;
面向应用落地时,通常按角色配置更容易控制影响范围。下面示例假设应用使用角色 app_user;请替换为实际角色名:
ALTER ROLE app_user
SET idle_in_transaction_session_timeout = '60s';
也可以进一步限定到某个数据库:
ALTER ROLE app_user IN DATABASE app_db
SET idle_in_transaction_session_timeout = '60s';
角色或数据库级配置通常对新建连接生效,因此修改后应让连接池逐步重建连接,并通过应用会话执行 SHOW 验证。设置为 0 表示禁用该超时,不适合作为生产环境的默认保护策略。
可以这样实践:复现并观察超时
下面是一个最小 Python 示例。它会打开事务、执行一次查询,然后让客户端停止发送命令。把连接串改成测试数据库,并安装驱动后运行:
python -m venv .venv
. .venv/bin/activate
pip install "psycopg[binary]"
import time
import psycopg
DSN = "postgresql://app_user:password@localhost:5432/app_db"
with psycopg.connect(DSN) as conn:
with conn.cursor() as cur:
cur.execute("SET idle_in_transaction_session_timeout = '5s'")
cur.execute("BEGIN")
cur.execute("SELECT current_timestamp")
print("Transaction opened; waiting for the server timeout...")
time.sleep(8)
try:
cur.execute("SELECT 1")
print(cur.fetchone())
except psycopg.Error as exc:
print(f"Connection was terminated: {exc}")
这里必须使用客户端的 time.sleep()。如果执行 SELECT pg_sleep(8),服务器仍在主动运行 SQL,会话状态是 active,不能模拟 idle in transaction。
在另一个连接中,可以用以下查询寻找正在积累风险的会话:
SELECT
pid,
usename,
application_name,
client_addr,
xact_start,
now() - xact_start AS transaction_age,
state_change,
now() - state_change AS idle_for,
wait_event_type,
wait_event,
left(query, 120) AS last_query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;
监控时不要只统计数量。更有价值的告警信号是最老事务年龄、事务内空闲时长、角色和 application_name。这些字段能帮助团队定位具体服务、任务或运维客户端。
超时只能止损,不能修复事务边界
启用超时后,应用会看到连接被服务器关闭。连接池需要识别失效连接并创建新连接,业务代码也必须正确处理异常。不要在不知道事务结果的情况下自动重放非幂等写操作,否则可能制造重复订单或重复扣款。
落地前可以按以下顺序推进:
- 查询
pg_stat_activity,记录现有空闲事务的持续时间和来源。 - 优先对在线应用角色设置一个相对宽松的值,再根据观测逐步收紧。
- 检查连接池的连接健康检查、失效连接淘汰和异常重试行为。
- 为事务使用明确的
commit、rollback和上下文管理,确保异常路径同样结束事务。 - 给迁移、批处理和人工管理角色单独配置,避免在线 API 与运维任务共享同一套限制。
- 同时监控锁等待、最老事务年龄、VACUUM 进度和连接池占用率。
idle_in_transaction_session_timeout 的价值不在于替应用管理事务,而在于给失控事务设置明确上限。合理的超时、清晰的事务边界和可定位的会话元数据配合起来,才能阻止一个被遗忘的连接拖住整套数据库。