为什么需要连接池
每次你的应用程序打开与数据库的连接时,都会发生一个小型协商过程:TCP握手、TLS协商(如果你使用SSL,你应该使用)、身份验证和会话初始化。对于PostgreSQL,这个过程大约需要50-100毫秒。对于MySQL,速度稍快但仍不可忽视。当你的应用程序每秒处理500个请求,且每个请求都需要数据库查询时,每秒就需要进行500次连接建立和断开——仅连接管理这一项,每秒就会产生大约25-50秒的累积开销。
连接池通过维护一组预先建立的连接来解决这一问题,你的应用程序可以借用和归还这些连接。连接建立成本只需支付一次,后续查询可以重用现有连接,开销几乎为零。这听起来很简单,从概念上讲确实如此。复杂性来自于决定在哪里进行池化,如何进行池化,以及是否需要外部连接池。
应用级池化:你已有的默认选项
大多数现代框架都包含内置的连接池。如果你使用SQLAlchemy、Django ORM、Rails ActiveRecord或任何成熟的数据库库,你已经拥有了应用级池化。以下是典型的SQLAlchemy池配置示例:
from sqlalchemy import create_engine
engine = create_engine(
"postgresql://user:pass@localhost:5432/mydb",
pool_size=10, # 持久连接数量
max_overflow=20, # 负载下的额外连接数
pool_timeout=30, # 等待连接的秒数
pool_recycle=1800, # 30分钟后回收连接
pool_pre_ping=True, # 使用前验证连接
)
# 在底层,当你执行:
with engine.connect() as conn:
result = conn.execute(text("SELECT * FROM users WHERE id = :id"), {"id": 42})
# 连接从池中借用,使用后归还。
# 没有TCP握手,没有身份验证——只是在现有套接字上执行查询。
应用级池化对于具有稳定工作负载的单进程应用程序效果良好。但在几种常见情况下它会失效:
- 多个应用实例:如果你在Kubernetes中运行50个pod,每个有10个连接池,那就是500个数据库连接。PostgreSQL在连接数超过200-300时会开始吃力,性能会急剧下降。
- 无服务器函数:Lambda函数、Cloud Functions和类似平台会不断创建和销毁进程。每次调用都可能建立新连接,使数据库不堪重负。
- 部署后的连接风暴:当所有pod在部署期间同时重启时,它们都会尝试同时建立连接。
这就是外部连接池发挥作用的地方。
PgBouncer:PostgreSQL的工作马
PgBouncer 是部署最广泛的 PostgreSQL 连接池。它位于你的应用程序和数据库之间,维护一个较小的实际数据库连接池,为更多的客户端连接提供服务。它轻量级、稳定,并且在大规模生产环境中经过了实战检验。
池模式
PgBouncer 支持三种池模式,选择正确的模式至关重要:
| 模式 | 工作原理 | 适用场景 | 注意事项 |
|---|---|---|---|
| 会话模式 | 客户端在整个会话期间拥有一个服务器连接 | 使用会话级功能的应用程序(LISTEN/NOTIFY、预处理语句) | 池化效果最小——本质上是一个代理 |
| 事务模式 | 客户端仅在事务期间获得一个服务器连接 | 大多数 Web 应用程序、API | 不能跨事务使用会话级功能 |
| 语句模式 | 客户端为每个单独的语句获得一个连接 | 简单的 SELECT 工作负载,仅用于连接限制的 pgbouncer | 不支持多语句事务 |
事务模式是 90% 的 Web 应用程序的正确选择。这是一个生产就绪的 PgBouncer 配置:
# pgbouncer.ini
[databases]
mydb = host=10.0.1.50 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# 池大小
pool_mode = transaction
default_pool_size = 25 # 每个用户/数据库对的连接数
min_pool_size = 5 # 保持至少 5 个连接处于活跃状态
reserve_pool_size = 5 # 紧急溢出连接
reserve_pool_timeout = 3 # 使用备用池前的等待时间(秒)
# 连接限制
max_client_conn = 1000 # PgBouncer 的最大客户端连接数
max_db_connections = 50 # 最大实际数据库连接数
# 超时设置
server_idle_timeout = 300 # 5 分钟后关闭空闲的服务器连接
client_idle_timeout = 0 # 不关闭空闲的客户端连接
server_connect_timeout = 15 # 连接到 PostgreSQL 的超时时间
query_timeout = 120 # 终止运行超过 2 分钟的查询
# 日志
log_connections = 0
log_disconnections = 0
log_pooler_errors = 1
stats_period = 60
预处理语句问题
在事务模式下使用 PgBouncer 的最大陷阱是预处理语句。PostgreSQL 预处理语句是会话范围的,意味着它们存在于特定的服务器连接上。在事务模式下,您的应用可能会为每个事务获得不同的服务器连接,因此在一个事务中创建的预处理语句在下一个事务中是不可见的。
# 这在 PgBouncer 事务模式下将会失效:
conn = pool.getconn()
cur = conn.cursor()
cur.execute("PREPARE get_user AS SELECT * FROM users WHERE id = $1")
# ... 稍后,可能在不同的服务器连接上 ...
cur.execute("EXECUTE get_user(42)") # 错误:预处理语句不存在
# 解决方案1:在驱动中禁用预处理语句
# 对于 psycopg2:
engine = create_engine(
"postgresql://user:pass@pgbouncer:6432/mydb",
connect_args={"options": "-c statement_timeout=30000"},
pool_pre_ping=True,
)
# 解决方案2:使用支持预处理语句的 PgBouncer 1.21+
# 添加到 pgbouncer.ini:
# max_prepared_statements = 100
PgBouncer 1.21(2024年发布)增加了服务器端预处理语句跟踪,这很大程度上解决了这个问题。如果您正在运行旧版本,请升级。
ProxySQL:MySQL 的等效方案(以及更多)
ProxySQL 对 MySQL 承担的角色与 PgBouncer 对 PostgreSQL 承担的角色相同,但它的功能要丰富得多。除了连接池,ProxySQL 还处理查询路由、读写分离、查询缓存和查询重写。这种额外的复杂性既是它的优势也是它的弱点。
# 通过 SQL 接口配置 ProxySQL
# 连接到 ProxySQL 管理接口:
mysql -u admin -padmin -h 127.0.0.1 -P 6032
-- 添加后端 MySQL 服务器
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight)
VALUES
(10, '10.0.1.50', 3306, 1000), -- 主库(写)
(20, '10.0.1.51', 3306, 1000), -- 副库1(读)
(20, '10.0.1.52', 3306, 500); -- 副库2(读,权重较低)
-- 配置连接池
UPDATE mysql_servers SET max_connections = 100;
-- 设置读写分离规则
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup)
VALUES
(1, 1, '^SELECT .* FOR UPDATE', 10), -- SELECT FOR UPDATE -> 主库
(2, 1, '^SELECT', 20), -- 普通 SELECT -> 副库
(3, 1, '.*', 10); -- 其他所有 -> 主库
-- 加载配置
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;
ProxySQL 的优势场景
ProxySQL在这些场景中特别有价值:
- 读写分离:自动将读取查询路由到副本,无需更改应用程序代码。
- 连接复用:与 PgBouncer 类似,它将多个客户端连接映射到更少的服务器连接。
- 查询缓存:缓存频繁执行的读取查询,基于 TTL 的失效机制。
- 故障转移:自动检测主库故障并提升副本。
何时确实需要外部连接池
并非每个应用程序都需要 PgBouncer 或 ProxySQL。以下是决策框架:
在以下情况下,你可能需要外部连接池:
- 运行超过 10 个连接到同一数据库的应用程序实例
- PostgreSQL 的总连接数超过 100 或 MySQL 超过 500
- 使用连接到关系数据库的无服务器函数
- 在部署或扩展事件期间遇到连接风暴
- 需要读写分离而不想更改应用程序代码(MySQL/ProxySQL)
在以下情况下,你可能不需要外部连接池:
- 应用程序实例少于 5 个
- 数据库连接数远在限制范围内
- 框架内置的连接池能够无问题地处理你的负载
- 你使用的是内置连接池的托管数据库(例如 Neon、PlanetScale)
值得考虑的托管替代方案
托管数据库领域已经显著发展。现在多个提供商提供内置的连接池,无需单独的连接池:
- Neon:通过其代理层提供内置连接池。支持池化连接和直接连接。在无服务器环境中表现良好。
- Supabase:在其托管的 PostgreSQL 产品中包含 PgBouncer。Supavisor 是他们基于 Elixir 的新连接池。
- PlanetScale:与 MySQL 兼容,具有内置连接处理。其基于 Vitess 的架构内部处理连接管理。
- AWS RDS Proxy:用于 RDS 和 Aurora 的托管连接池。支持 PostgreSQL 和 MySQL。按 vCPU 小时计费,成本会累积。
部署模式
连接池的部署位置很重要。有三种常见模式:
模式 1:边车
在每个 pod 中将 PgBouncer 作为 sidecar 容器运行。这为每个应用程序实例提供自己的连接池,减少延迟但增加到数据库的总连接数。
# 使用 PgBouncer sidecar 的 Kubernetes 部署
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
env:
- name: DATABASE_URL
value: "postgresql://user:pass@localhost:6432/mydb"
- name: pgbouncer
image: edoburu/pgbouncer:1.22
ports:
- containerPort: 6432
env:
- name: DATABASE_URL
value: "postgresql://user:pass@postgres-primary:5432/mydb"
- name: POOL_MODE
value: "transaction"
- name: DEFAULT_POOL_SIZE
value: "5"
- name: MAX_CLIENT_CONN
value: "50"
模式 2:集中式连接池
运行专用的 PgBouncer 实例(或集群),所有应用程序实例都连接到它。这使您可以精确控制到数据库的总连接数。
模式 3:按服务的连接池
每个服务都有自己的 PgBouncer 部署。这平衡了隔离性和效率,是我推荐给大多数微服务架构的模式。
监控您的连接池
配置错误的连接池比没有连接池更糟糕。请监控以下指标:
# PgBouncer 统计查询(连接到 PgBouncer 管理接口)
SHOW POOLS;
# 关注:cl_active, cl_waiting, sv_active, sv_idle
# 如果 cl_waiting 经常 > 0,增加连接池大小
# 如果 sv_idle 总是很高,减少连接池大小
SHOW STATS;
# 关注:avg_query_time, total_wait_time
# total_wait_time > 0 表示客户端正在等待连接
连接池是基础设施的管道——不起眼但至关重要。配置正确,您的数据库可以在不费力气的情况下处理 10 倍的负载。配置错误,您将花费周末调试仅在负载下才会发生的神秘连接超时错误。选择能解决您实际问题的最简单方案,而不是架构图上看起来最好的那个。
