温热的连接池能够减少反复建立 PostgreSQL 连接,但空闲连接在池里等待期间也可能消失。服务器结束会话、网络故障或中间设备的空闲策略,都可能让客户端仍握着一个已经不能使用的套接字。复用方案必须包含失效识别和替换;对象还在池里,只说明应用保存了它,不能证明下一条查询一定成功。

坏掉的样子

TCP 保活能够识别原本空闲连接上的部分故障,却不会替 SQL 设置执行截止时间,也不证明数据库能够完成某个具体请求,更不会处理结果未知的提交。客户端连接参数只作用于 TCP,使用 Unix 域套接字时相关选项会被忽略;服务器端保活设置控制的是另一端。具体操作系统是否支持某个选项,也会影响最终行为,不能只看参数字符串存在就宣称故障检测已经发生。

借出前健康检查,可以在把连接交给调用方之前识别已经失效的连接。Psycopg 默认不执行这种检查,但提供回调让应用选择启用。多一次探测意味着额外往返,也仍然无法阻止连接在探测成功后立刻出故障。因此,建立连接、等待租用和执行语句,都需要各自的预算与错误处理,不能用一个“保活开启”覆盖全部阶段。

快速验证

准备 Python 3.10 或更新版本,通过 TCP 连接使用默认零值语句超时的 PostgreSQL 18,并安装原演练使用的版本:python3 -m pip install "psycopg[binary]==3.3.5" "psycopg-pool==3.3.1"。把 PG_DSN 设置为可丢弃数据库,使用允许结束自己后台连接的角色。保存为 liveness-demo.py 后运行。程序先记录每个测试池刚创建的连接标识,然后只结束那条已经归还、处于空闲状态的连接:

import os

import psycopg
from psycopg.errors import QueryCanceled
from psycopg_pool import ConnectionPool

dsn = os.environ['PG_DSN']
with psycopg.connect(dsn, autocommit=True) as admin:
    for checked in (False, True):
        with ConnectionPool(
            dsn, min_size=1, max_size=1, timeout=3, open=True,
            check=ConnectionPool.check_connection if checked else None,
            kwargs={
                'connect_timeout': 2,
                'keepalives': 1,
                'keepalives_idle': 30,
                'keepalives_interval': 10,
                'keepalives_count': 3,
            },
        ) as pool:
            pool.wait(timeout=3)
            with pool.connection() as conn:
                assert conn.execute('SELECT inet_server_addr() IS NOT NULL').fetchone()[0]
                original_pid = conn.info.backend_pid
                parameters = conn.info.get_parameters()
                assert [parameters[k] for k in (
                    'keepalives', 'keepalives_idle', 'keepalives_interval', 'keepalives_count'
                )] == ['1', '30', '10', '3']
            # Terminate only the idle connection just created by this fixture.
            assert admin.execute('SELECT pg_terminate_backend(%s,2000)', (original_pid,)).fetchone()[0]

            if not checked:
                try:
                    with pool.connection() as conn:
                        conn.execute('SELECT 1')
                except psycopg.OperationalError as error:
                    print('unchecked_first_use', error.sqlstate)
                else:
                    raise AssertionError('terminated connection unexpectedly worked')

            with pool.connection() as conn:
                replacement_pid = conn.info.backend_pid
                assert replacement_pid != original_pid
                assert conn.execute('SELECT 1').fetchone()[0] == 1
                print('checked', checked, 'replacement', True)

            try:
                with pool.connection() as conn:
                    conn.execute("SET LOCAL statement_timeout='50ms'")
                    conn.execute('SELECT pg_sleep(0.2)')
            except QueryCanceled as error:
                assert error.sqlstate == '57014'
                print('statement_timeout', error.sqlstate)
            else:
                raise AssertionError('long statement did not time out')

            with pool.connection() as conn:
                assert conn.info.backend_pid == replacement_pid
                assert conn.execute('SHOW statement_timeout').fetchone()[0] == '0'
                assert conn.execute('SELECT 1').fetchone()[0] == 1
                print('healthy_after_rollback', True)

两种情况都配置了客户端保活参数,并核对有效值,不打印整个连接串。第一种没有借出前检查。空闲后端被服务器结束后,下次读取遭遇操作错误;本次这种明确的服务器关闭返回 57P01。池随后丢弃坏连接,下一次借用得到不同的后端标识,并成功返回一。

第二种启用内置的连接检查回调。经过同样的受控终止,获取阶段发现旧连接不能使用,再把正常的新连接交给借用者。两种情况最后都报告完成替换,但只有未检查的那一种,把首次使用旧连接的错误直接暴露给业务代码。原实验验证的是显式服务器终止,不是静默网络分区,也没有等待 TCP 保活探测计时器真正触发。本轮审阅未重新执行 TCP 实验。

每种情况还会在五十毫秒的事务局部语句预算下,执行二百毫秒睡眠,预期得到 57014。此时连接仍然活着,TCP 保活也已启用,查询却依旧需要自己的截止时间。异常离开连接上下文后,失败事务被回滚;后续借用继续使用同一个替代后端,看到语句超时恢复为原来的零,并能正常读取一。

这些检查区分了两件事:死连接需要替换,活连接上的语句取消则可以通过正确事务清理恢复。实验还确认局部超时设置没有泄漏给下一位借用者。不能把所有 SQL 错误都当成必须销毁连接的证据,也不能因为健康探测成功,就删除执行阶段的预算。判断应跟随错误发生的阶段,以及对应连接和事务的真实状态。

在明确的操作边界重新连接

池的最大空闲时间和最大寿命,有助于管理连接轮换,但都不能保证套接字一定活到设置的年龄。在 Psycopg 中,max_idle 只回收超过 min_size 的闲置连接,不会把最低预热数量中的每条空闲连接都定时关闭。服务器或中间设备可能更早关闭连接。应协调这些策略,观察丢弃、重连次数和故障期间的获取延迟;健康检查过于频繁,也会给大量短查询增加不可忽略的往返成本。策略需要结合请求长度和故障特征,而不是越积极越可靠。

示例是在应用写入开始之前替换空闲连接。提交期间断连则不同:数据库可能已经提交,只是确认丢失。建立新连接不会自动告诉调用方上一笔究竟成功没有,直接重放写入就可能重复副作用。应保留稳定操作标识,并查询持久化结果,在结果得到核实后再决定是否重发非幂等工作。

获取连接的多次尝试,还应受调用方取消和请求总截止时间控制,避免请求已经结束,后台重连仍然不停消耗资源。保活、健康检查、获取预算和语句预算可以共同工作,但每一项都只回答自己的问题;把它们对应到具体操作边界,才能知道错误之后应该替换连接、回滚事务,还是先核对已经提交的结果。

来源