Language:Chinese VersionEnglish Version

为什么需要连接池

每次你的应用程序打开与数据库的连接时,都会发生一个小型协商过程: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 倍的负载。配置错误,您将花费周末调试仅在负载下才会发生的神秘连接超时错误。选择能解决您实际问题的最简单方案,而不是架构图上看起来最好的那个。

By Michael Sun

Founder and Editor-in-Chief of NovVista. Software engineer with hands-on experience in cloud infrastructure, full-stack development, and DevOps. Writes about AI tools, developer workflows, server architecture, and the practical side of technology. Based in China.

Leave a Reply

Your email address will not be published. Required fields are marked *

You missed