34.2 连接风暴与排队失控
PostgreSQL 采用一个 client connection 对应一个 backend process 的模型。连接不仅占一个 数字,还需要进程、内存、认证、catalog 初始化、socket、锁表与调度成本。连接池的 价值不是让数据库接受无限请求,而是把大量 client concurrency 变成有上限的 database concurrency。
34.2.1 数据库连接、代理池与应用池三层
三层都在排队
request
-> application worker / local pool waiters
-> PgBouncer client connections / waiters
-> PgBouncer server connections
-> PostgreSQL client backends每层至少有四个量:
admitted
running
waiting
rejected/timed out只看 PostgreSQL numbackends 会漏掉池前的队列;只看应用 pool size 又会漏掉多个
pod/进程/租户汇总后对数据库的总承诺。
一个粗略预算应满足:
$$ \sum_i A_i P_i + M + B \le C_{\text{pool}} $$
其中 $A_i$ 是第 $i$ 类应用实例数,$P_i$ 是每实例可能占用的 server connection, $M$ 是迁移、运维和监控预算,$B$ 是故障切换/伸缩缓冲。对 PostgreSQL:
$$ C_{\text{pool}} + C_{\text{direct}} < \texttt{max_connections} - C_{\text{reserved}} $$
这里不是要求把所有层的上限设成同一个数。应用 client queue 可以大于 PgBouncer server pool,但必须有长度、deadline 和拒绝策略;PgBouncer client connection 也不等于 PostgreSQL backend。
按事务语义分池
不能只按主机分池,还要按工作类型隔离:
| 池 | 特征 | 建议控制 |
|---|---|---|
| OLTP | 短事务、低尾延迟 | 最稳定的预算,快速失败 |
| batch/ETL | 长查询、吞吐优先 | 独立小池,可暂停 |
| admin/migration | 低频但高权限 | 保留直连/管理余量 |
| monitoring | 周期查询 | 有界并发,不能形成自激 |
| read-only | 可接受副本语义时 | 独立只读端点与 staleness 契约 |
若批处理与 OLTP 共用一池,批处理占满 server connections 后,所谓“主库健康”也无法 给在线请求提供 admission。Pigsty 的主写、只读、离线等服务端点可以提供路由分界,但 是否适合某事务仍由应用一致性语义决定。
事故时不要立刻放大 max_connections
扩大上限会让更多工作同时进入执行层,可能把一个有界连接拒绝变成内存、CPU、锁与 I/O 全面争用。只有在以下事实都成立时,调整才是经过评估的容量变更:
current connections are useful, not retry duplicates
per-backend and per-query memory worst case is safe
CPU/I/O still have service headroom
new reserved/admin budget remains available
pool and application limits will not simply refill the new space
rollback and restart/reload semantics are known在线事故的默认路线是收紧 admission,而不是把硬边界向后推。
34.2.2 重试放大、健康检查和短连接
重试会把失败变成新流量
若原始到达率为 $\lambda_0$,每次失败平均触发 $r$ 次下一轮尝试,成功率没有及时恢复, 有效流量近似:
$$ \lambda_{\text{effective}} = \lambda_0(1+r+r^2+\dots) $$
当 $r\ge1$ 且没有 retry budget、deadline 或熔断时,系统进入正反馈:
latency rises
-> client timeout
-> synchronized retry
-> more connections and work
-> latency rises again日志里的“请求量上升”可能不是用户流量,而是同一批请求的重复尝试。必须用稳定的 request/idempotency key 区分 original、retry 与 hedge。
健康检查也会成为负载
设 $N$ 个应用实例,每个实例维护 $P$ 个 worker,每 $h$ 秒建立一次检查连接,则单健康 检查一项就可能产生约 $NP/h$ 次每秒建连。以下设计尤其危险:
- 每个业务请求先新建连接执行
SELECT 1; - 每个 pod 同时启动并预热完整池;
- 多级代理各自以高频新连接探测;
- 故障时 autoscaling 新增实例,同时所有实例立刻重试;
- liveness 把短暂数据库慢判为应用死亡,形成重启风暴。
健康检查要区分:
liveness: process itself是否需要重启
readiness: 是否接收新业务流量
dependency health: 数据库路径是否满足该业务语义数据库慢通常应先让应用 not-ready 或熔断新请求,而不是把所有应用进程重启。
长连接也不是免疫
已有连接在代理切换、数据库重启、证书轮换或网络抖动后会同时重连。池应具备:
randomized connection lifetime
startup/prewarm rate limit
connect timeout shorter than request deadline
bounded reconnect concurrency
exponential backoff with full jitter
global retry budget不要给每一层各自配置十次重试。应用、驱动、service mesh、代理和任务框架叠加后, 最坏尝试次数是乘法。
34.2.3 限流、队列、连接预算与指数退避
把过载变成显式 admission
好的过载控制不是“永不拒绝”,而是在系统仍能完成高价值工作时,尽早、明确地拒绝 超出预算的工作:
admit if:
class budget available
AND request deadline still useful
AND downstream breaker allows probe
else:
reject/queue with bounded cost队列必须同时有:
- 最大长度,防止内存成为下一瓶颈;
- 最大等待时间,过期工作不再进入数据库;
- 公平性或优先级,避免批处理饿死 OLTP;
- 可观测的 admitted/waited/rejected/expired 计数;
- drain 与 deploy 行为,避免发布时丢失或翻倍。
无限队列只是把快速失败变成更晚失败。Little’s Law 给出稳定系统中的关系:
$$ L=\lambda W $$
当平均等待 $W$ 上升时,在途数量 $L$ 也上升;如果请求 deadline 已经小于排队时间, 即使最终执行成功,对用户也没有价值。
指数退避要带随机抖动
一个常见策略:
cap = min(max_backoff, base * 2^attempt)
sleep = random(0, cap) # full jitter
stop when:
request deadline exhausted
retry budget exhausted
operation is not idempotent/reconcilable重试条件也要按错误分类:
| 错误 | 默认处理 |
|---|---|
| 认证/权限/语法 | 不重试,修配置或代码 |
| 连接拒绝/切换窗口 | 有预算、带 jitter 重试 |
| statement timeout | 先判是否仍在数据库执行及是否幂等 |
| deadlock/serialization failure | 整个事务按有限策略重试 |
| unknown COMMIT outcome | 先用业务 token 对账,不裸重放 |
连接事故的止血顺序
1. freeze autoscaling/restart/retry amplification
2. preserve admin/reserved path
3. cap low-priority application admission
4. pause batch and migration pools
5. inspect active/waiting/root blockers
6. cancel exact low-value work if necessary
7. verify queue, success rate and tail latency
8. only after stability, repair capacity/config root cause验收不是“连接数下降”,而是:
new connection rejection stops or is intentional
pool waiting/expired work trends down
useful completion rate recovers
admin path remains available
no new memory/I/O bottleneck appears
temporary limits have an owner and expiry上一节:第一动作:流量型还是保留型 · 返回本章目录 · 下一节:失控查询、锁与事务 · 查看全书目录 · 查看索引中心