跳至内容

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

上一节:第一动作:流量型还是保留型 · 返回本章目录 · 下一节:失控查询、锁与事务 · 查看全书目录 · 查看索引中心

最后更新于