数据库连接池准入与公平性:全局 Permit、Deadline 与排空
每个应用实例把本地连接池调到 50,并不代表数据库只会收到 50 条连接。本文用全局 permit、工作负载队列和取消确认控制连接总量,避免一个租户或慢查询拖垮全部请求。
本文要点
- 主题聚焦:TypeScript、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
十个应用 Pod 各设 maxOpen=50,发布扩容后理论上可向 PostgreSQL 建立 500 条连接;一个批处理租户又把所有池槽占满,在线请求只能在应用内无限等待。数据库最终拒绝连接时,重试风暴继续创建 socket,连管理员用于诊断的连接也可能耗尽。本地池大小不是全局容量合同。
PostgreSQL 连接池与 PgBouncer介绍池化模式,数据库连接架构比较代理位置。本篇不重复参数清单,而是回答谁可以排队、何时获得一个数据库执行槽,以及取消或 failover 后何时归还。
先从数据库预算反推 permit
发布 dbAdmissionPolicy.v1,绑定数据库 cluster/role、policy generation、数据库连接硬上限、保留的 admin/replication 容量、PgBouncer pool mode、全局 serverSessionPermits、各 workload class 的 executionPermits、租户权重、最大队列、排队 deadline 和事务时限。两类额度不能混用:session permit 约束真实后端连接并跨 idle 周期持有,request ticket 只约束公平排队与正在执行的事务。
schema: dbAdmissionPolicy.v1
database: orders-primary/write
generation: 27
serverSessionPermits: 180
adminReserve: 12
executionClasses:
interactive: {permits: 130, maxQueue: 800}
batch: {permits: 50, maxQueue: 100}
fairness: weighted-deficit-v2
PostgreSQL max_connections 包含服务端可接受的连接预算,并为超级用户等保留槽位;它不是让应用把数字全部占满的目标。PgBouncer 的 max_client_conn、default_pool_size 等参数区分客户端连接和后端 server connection。Go database/sql 的 SetMaxOpenConns 只限制一个 DB 句柄;这些本地旋钮都不能自动协调多个进程。
request ticket 状态为 QUEUED -> EXECUTION_GRANTED -> CHECKING_OUT -> IN_TRANSACTION -> REQUEST_RELEASED;排队超时为 QUEUE_EXPIRED,取消与 grant 竞争时只能得到 CANCELLED 或 EXECUTION_GRANTED。物理连接另走 SESSION_RESERVED -> CONNECTING -> OPEN_IDLE | OPEN_BUSY -> CLOSING -> SESSION_RELEASED,协议结果不明为 SESSION_UNKNOWN。ticket 绑定 tenant、class、deadline 与 boot ID;session permit 还绑定代理/进程身份、后端 PID、backend epoch 和连接 generation。
Grant 是唯一容量接受点
唯一分配入口 grantDbAdmission 在同一 canonical ledger 上处理两种动作。它先按 class/tenant 公平性把 request ticket 从 QUEUED 改为 EXECUTION_GRANTED;若 checkout 需要新建物理连接,还必须原子消费一个全局 session permit,写 SESSION_RESERVED receipt 后才允许 connect。已有 idle 连接复用其原 session permit,不重复消费。任何进程和 PgBouncer 实例都不能绕过这一本 session 账,重复请求只读原 receipt。
request ticket 在事务 commit/rollback 或可证明未开始后 CAS 释放,连接回到 idle 池时仍持有 session permit。只有物理 session 关闭或服务端确认其 backend PID 消失,才能释放 session permit;重复 release 不增加余额。进程崩溃后无法确认 session 终止就记 SESSION_UNKNOWN 并继续占容量。failover 提升 backend epoch,旧 session 全部 drain;新旧 epoch 的 session permit 总和仍受硬预算约束。跨服务 Deadline负责请求预算,但取消信号本身不证明 SQL 或 session 停止。
公平调度可使用 weighted deficit round robin:每个活跃租户按权重获得额度,未使用额度有限积累,单租户仍受 inflight 上限。interactive 和 batch 使用独立保底池并允许受控借用;借用带 TTL,核心流量排队上升时立即停止新借用。纯 FIFO 会被一个长事务租户头阻塞,纯优先级又会饿死后台维护,所以策略同时设置最大等待和 aging 上限。负载均衡策略决定请求去哪台应用,准入账决定它是否能进入数据库。
队列不是无限缓冲。达到 maxQueue 或剩余 deadline 小于预计 checkout+执行预算时立即拒绝,并返回明确重试分类;自动重试必须服从同一 ticket 身份。多 Agent 过载路由中的负载卸载原则可以复用,但数据库 permit 还需绑定真实 session 和事务终止证明。PostgreSQL 性能调优只能优化单次查询,不能修复无界准入。
用泄漏和饥饿压垮测试环境
故障套件注入 request grant 后崩溃、session reserve 后 connect ACK 丢失、取消与 grant 同时发生、SQL 已执行但客户端超时、idle 连接泄漏、PgBouncer 重启、数据库 failover、租户洪泛、policy 切代和两类 reaper 双回收。分别断言 request ticket 与 session permit 守恒,OPEN + CONNECTING + SESSION_UNKNOWN 不超过后端硬预算,admin reserve 始终可用,idle session 不会因 request release 从账上消失。
门禁要求十万轮并发测试两本账差异均为 0、真实 server session 超出安全线为 0、双 release 和无限饥饿为 0,interactive 排队 p99 不越预算,session 泄漏在两个 lease 周期内告警。上线先 shadow 两类决定,再接管 5% 只读流量。回滚停止发新 generation 的 ticket/session permit,排空并对账在途 session 后切 alias;旧调度器也必须经过全局 session 账,不能恢复为每个 Pod 独立放大连接池。
Related