← [ BACK_TO_NOTES ]

BodySense 数据库第 10 课:Connection Pool、Timeout、Backpressure 与数据库容量

围绕 BodySense Go sql.DB 与 Python AsyncConnectionPool 学习连接池不是性能魔法,并建立连接上限、排队、事务时长、timeout 与数据库容量的系统模型。

#course #growing #backend #database #postgresql #bodysense

[!info] related notes

BodySense 数据库第 10 课:Connection Pool、Timeout、Backpressure 与数据库容量

[!abstract] 本课目标 理解“更多连接”可能让系统更慢;连接池本质是一个有限资源并发控制器。

从零建立概念:Database Connection 是一份有成本的 Server Resource

应用访问 PostgreSQL 前要建立连接。

连接不是一个抽象数字,它意味着:

TCP/TLS
authentication
server backend/session state
memory
socket buffers
transaction state
query execution context

如果每个 HTTP request 都:

connect
query
disconnect

连接建立/认证成本会非常浪费。

于是引入 Connection Pool。

Pool 的真正工作模型

Application Requests
      ↓
Connection Pool
├─ conn 1 [busy]
├─ conn 2 [idle]
├─ conn 3 [busy]
└─ ...
      ↓
PostgreSQL

请求到来:

有 idle connection
→ 借出

没有 idle,但 open < max
→ 新建 connection

open == max
→ 等别人归还 / timeout

Go 官方文档明确指出 sql.DB 就是这种 pool handle。

为什么 PoolSize 不是越大越快

假设 DB 有 8 CPU cores。

应用同时推:

25 active queries

也许可以很好利用 CPU/I/O。

如果变成:

500 active queries

可能发生:

CPU context switch
memory pressure
cache churn
disk contention
lock contention
longer tail latency

所以 Pool 是:

复用连接 + 限制数据库并发。

它某种意义上是一个 Semaphore。

Go sql.DB 的几个关键参数

MaxOpenConns

最多同时打开多少底层连接。

达到上限:

new DB operation
→ waits

MaxIdleConns

最多保留多少当前没在执行工作的连接。

太小:

流量一波动
→ repeatedly reconnect

太大:

长期占着很多 server connections

ConnMaxLifetime

一条连接最多复用多长时间。

可以帮助:

避免永久老连接
适配 load balancer / DB failover
滚动更新连接

但大量连接恰好同一时刻过期也可能造成 reconnect spike,所以真实系统常考虑 jitter。

ConnMaxIdleTime

空闲太久的连接可以主动释放。

BodySense 当前 Go database.go 显式:

MaxOpenConns = 25
MaxIdleConns = 5
ConnMaxLifetime = 5m

它是当前配置事实,不代表这些数字已经经过最终容量验证。

DB.Stats:不要盲调 Pool

Go 可以观察:

OpenConnections
InUse
Idle
WaitCount
WaitDuration
MaxOpenConnections

例如:

InUse ~= MaxOpen
WaitCount 持续增长
WaitDuration 高

说明应用经常在 Pool 前排队。

但下一步不是自动增大 MaxOpen。

还要问:

DB CPU 高吗?
query latency 高吗?
locks 多吗?
连接被长 transaction 占吗?
每请求 query count 是否 N+1?

多服务 / 多副本时要算总连接

假设:

Go API
3 replicas × 25 = 75

Python AI Service
4 replicas × 20 = 80

理论 pool upper bound:

155 connections

再加:

migration
admin
monitoring
background worker
psql

PostgreSQL Server 看到的是所有客户端总和。

所以单个服务的 25 看起来不大,不代表集群总连接安全。

Pool Queueing 与 Backpressure

Backpressure 的核心不是“让系统变慢”。

而是:

当下游只能安全处理 N 个并发时,上游不能无限把更多工作继续灌进去。

Pool 上限本身就是一种 backpressure。

但如果等待无限:

requests accumulate
goroutines/tasks accumulate
memory ↑
user timeout
client retry
more requests

系统仍会崩。

所以还需要:

bounded request deadline
pool wait bound
load shedding / admission control

Timeout 不只有一种

HTTP / Context Deadline

这个用户请求总共最多允许多久。

Connection Acquisition Timeout

最多等多久才能从 pool 拿到 connection。

Statement Timeout

SQL 本身最多执行多久。

Lock Timeout

最多等锁多久。

Idle in Transaction Timeout

Transaction 开着却长期不执行 SQL,应该被限制。

这些 timeout 保护的是不同 failure mode。

为什么 Transaction 中不要做慢外部调用

例如:

BEGIN
SELECT ... FOR UPDATE
↓
call LLM 20 seconds
↓
UPDATE
COMMIT

这 20 秒:

connection 被占
row lock 可能被占
snapshot 继续活着
其他 transaction 等待

正确设计通常:

transaction 内只做必须原子的 DB work
尽量把慢 HTTP/LLM/File I/O 放外面

需要跨系统一致性时再用 Outbox/Job/Saga 等协议,而不是把数据库 transaction 无限拉长。

一个简单容量估算直觉

假设平均每个 DB operation 持 connection 20ms。

单连接理论上每秒大约能完成:

1000 / 20 = 50 operations

25 条连接理论上约:

25 × 50 = 1250 operations/s

这只是极粗的排队直觉,不是 benchmark 结果。

真实还受:

query complexity
CPU/I/O
lock
network
transaction length
workload distribution

影响。

重点是建立:

吞吐、并发、平均占用时间和排队之间是有关联的。

1. Pool 解决什么

建立数据库连接有成本,且 PostgreSQL backend connection 本身也消耗资源。Pool 复用连接并限制并发。

BodySense Go 当前:

MaxOpenConns = 25
MaxIdleConns = 5
ConnMaxLifetime = 5m

这些值不是越大越好。

2. 三层并发

HTTP request concurrency
→ application DB pool
→ PostgreSQL max_connections / CPU / I/O

如果上游同时 1000 请求,而 pool 只有 25:

25 executing
975 waiting

这不是错误,关键是等待是否有上限、请求是否能及时 timeout / shed load。

3. Long Transaction 的放大效应

一个连接被 transaction 占住期间不能被其他请求复用。外部 HTTP/LLM 调用若发生在数据库事务中,会把 connection hold time 拉长并扩大 lock time。

4. Timeout

需要区分:

request/context timeout
connection acquisition wait
statement timeout
lock timeout
idle in transaction timeout

每一类保护不同失败模式。

Prediction

[!question]

  1. 把 MaxOpenConns 从 25 调到 500,吞吐一定上升吗?
  2. 请求大部分时间在等 pool,PostgreSQL CPU 却不高,问题可能在哪?
  3. 在 transaction 中调用 20 秒 LLM 有什么数据库后果?
  4. Pool 满时无限等待为什么危险?
  5. Go API 和 Python service 各有 25 个连接,数据库实际最大并发只需要考虑 25 吗?

[!success]- Answer / 解析 1. 不一定。 PostgreSQL connection、CPU、I/O 和 lock contention 都有限,过多并发可能增加 context switch 和争用,反而降低吞吐。

2. 应用层 pool size/连接泄漏/长事务可能是瓶颈;数据库没收到足够并发,所以 CPU 低。

3. 连接长期占用,已获取锁更久,snapshot 变老,还可能阻碍 vacuum;外部慢操作应尽量放在 transaction 外。

4. 请求堆积会放大内存、goroutine/task、上游 timeout,形成排队雪崩。

5. 不是。 多个服务/副本的 pool 要汇总考虑,真实总连接是所有实例之和。

Lab

测一个真实 endpoint:记录 concurrent requests、DB pool wait、open/in-use/idle connections、SQL latency。把 pool 人为缩小,观察延迟从执行时间转移到排队时间。

L4 Gate

给定 QPS、平均 DB time、实例数和 pool 配置,能解释系统在哪一层排队,以及增加连接是否可能改善还是恶化。

创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本