PgBouncer使用笔记

文章目录

  • [说明: 本文基于 AI 问答生成](#说明: 本文基于 AI 问答生成)
  • 教程
  • [PgBouncer 生产环境最佳实践培训教程](#PgBouncer 生产环境最佳实践培训教程)
    • 第一章:核心原理
      • [1.1 PgBouncer 是什么](#1.1 PgBouncer 是什么)
      • [1.2 为什么需要连接池](#1.2 为什么需要连接池)
      • [1.3 池模型:按 (database, user) 分池](#1.3 池模型:按 (database, user) 分池)
    • [第二章:三种 Pooling 模式](#第二章:三种 Pooling 模式)
      • [2.1 三种模式对比](#2.1 三种模式对比)
      • [2.2 Session Pooling(会话级)](#2.2 Session Pooling(会话级))
      • [2.3 Transaction Pooling(事务级,生产最常用)](#2.3 Transaction Pooling(事务级,生产最常用))
      • [2.4 Statement Pooling(语句级)](#2.4 Statement Pooling(语句级))
      • [2.5 模式选择决策树](#2.5 模式选择决策树)
    • 第三章:配置详解
      • [3.1 完整生产配置模板](#3.1 完整生产配置模板)
      • [3.2 认证文件 `userlist.txt`](#3.2 认证文件 userlist.txt)
      • [3.3 PostgreSQL 侧配套配置](#3.3 PostgreSQL 侧配套配置)
      • [3.4 系统级配置](#3.4 系统级配置)
    • [第四章:Transaction 模式的限制(重点)](#第四章:Transaction 模式的限制(重点))
      • [4.1 不可用的功能](#4.1 不可用的功能)
      • [4.2 仍可用的功能](#4.2 仍可用的功能)
      • [4.3 预处理语句的特殊机制](#4.3 预处理语句的特殊机制)
      • [4.4 会话级状态的处理建议](#4.4 会话级状态的处理建议)
      • [4.5 `server_reset_query` 在 transaction 模式下不执行](#4.5 server_reset_query 在 transaction 模式下不执行)
    • 第五章:参数详解与配置经验
      • [5.1 池大小参数](#5.1 池大小参数)
      • [5.2 前端连接参数](#5.2 前端连接参数)
      • [5.3 超时参数](#5.3 超时参数)
      • [5.4 参数取值优先级](#5.4 参数取值优先级)
    • 第六章:运维与监控
      • [6.1 启动与停止](#6.1 启动与停止)
      • [6.2 管理控制台](#6.2 管理控制台)
      • [6.3 核心监控命令](#6.3 核心监控命令)
      • [6.4 `SHOW POOLS` 关键指标解读](#6.4 SHOW POOLS 关键指标解读)
      • [6.5 常见运维场景](#6.5 常见运维场景)
        • [场景 1:数据库重启](#场景 1:数据库重启)
        • [场景 2:修改配置后生效](#场景 2:修改配置后生效)
        • [场景 3:PG 故障转移](#场景 3:PG 故障转移)
        • [场景 4:零停机滚动重启](#场景 4:零停机滚动重启)
      • [6.6 信号速查](#6.6 信号速查)
    • 第七章:生产最佳实践
      • [7.1 容量规划清单](#7.1 容量规划清单)
      • [7.2 安全最佳实践](#7.2 安全最佳实践)
      • [7.3 性能调优清单](#7.3 性能调优清单)
      • [7.4 监控告警](#7.4 监控告警)
      • [7.5 常见踩坑清单](#7.5 常见踩坑清单)
    • 第八章:故障排查
      • [8.1 客户端连接被拒](#8.1 客户端连接被拒)
      • [8.2 查询延迟高](#8.2 查询延迟高)
      • [8.3 连接泄漏](#8.3 连接泄漏)
    • 第九章:核心配置速查表
    • 第十章:一句话总结
  • [问题 1](#问题 1)
  • [回复 1](#回复 1)
  • [问题 2](#问题 2)
  • [回答 2](#回答 2)
  • [问题 3](#问题 3)
  • [回答 3](#回答 3)
    • [一、核心认知:`max_client_conn` 管的是"便宜连接"](#一、核心认知:max_client_conn 管的是"便宜连接")
    • [二、推算过程(5 个视角交叉验证)](#二、推算过程(5 个视角交叉验证))
      • [视角 1:从"原直连峰值"反推](#视角 1:从"原直连峰值"反推)
      • [视角 2:从"transaction pooling 复用倍数"推算](#视角 2:从"transaction pooling 复用倍数"推算)
      • [视角 3:从"应用部署规模"推算](#视角 3:从"应用部署规模"推算)
      • [视角 4:从"突发容灾"推算](#视角 4:从"突发容灾"推算)
      • [视角 5:从"资源开销"反向验证](#视角 5:从"资源开销"反向验证)
    • 三、五个视角汇总
    • 四、为什么不是更小或更大
      • [为什么不设 200(保守)](#为什么不设 200(保守))
      • [为什么不设 10000(激进)](#为什么不设 10000(激进))
    • 五、动态调优方法
    • [六、一个容易被忽略的前提:系统 fd 限制](#六、一个容易被忽略的前提:系统 fd 限制)
    • 七、完整设计逻辑链
    • 八、一句话总结
    • 一、五大可能原因(按发生概率排序)
    • 二、逐个排查
      • [原因 1:PgBouncer 版本 < 1.21(功能根本不存在)](#原因 1:PgBouncer 版本 < 1.21(功能根本不存在))
      • [原因 2:`max_prepared_statements = 0`(功能被禁用)](#原因 2:max_prepared_statements = 0(功能被禁用))
      • [原因 3:应用用了 SQL 级 `PREPARE`(而非协议级)](#原因 3:应用用了 SQL 级 PREPARE(而非协议级))
      • [原因 4:`max_prepared_statements` 值确实配小了](#原因 4:max_prepared_statements 值确实配小了)
      • [原因 5:客户端驱动兼容性问题](#原因 5:客户端驱动兼容性问题)
    • 三、完整排查流程图
    • 四、快速止血方案(不管原因,先止血)
      • [方案 A:禁用服务端 prepared statement(最快)](#方案 A:禁用服务端 prepared statement(最快))
      • [方案 B:临时切 session 模式(牺牲性能)](#方案 B:临时切 session 模式(牺牲性能))
      • [方案 C:升级 PgBouncer + 启用跟踪(根治)](#方案 C:升级 PgBouncer + 启用跟踪(根治))
    • 五、排查命令汇总(一键执行版)
    • 六、一句话总结
  • 问题
  • 答复
    • [一、TCP Keepalive 参数含义](#一、TCP Keepalive 参数含义)
      • [1.1 这组参数管什么](#1.1 这组参数管什么)
      • [1.2 四个参数的含义](#1.2 四个参数的含义)
      • [1.3 各参数详解](#1.3 各参数详解)
      • [1.4 总死亡检测时间](#1.4 总死亡检测时间)
      • [1.5 为什么需要这组参数](#1.5 为什么需要这组参数)
      • [1.6 注意事项](#1.6 注意事项)
    • [二、admin_users 和 stats_users 是否需要在 PG 中创建](#二、admin_users 和 stats_users 是否需要在 PG 中创建)
      • [2.1 结论先行](#2.1 结论先行)
      • [2.2 认证流程的区别](#2.2 认证流程的区别)
      • [2.3 配置方法](#2.3 配置方法)
        • [步骤 1:在 `userlist.txt` 中添加用户](#步骤 1:在 userlist.txt 中添加用户)
        • [步骤 2:在 `pgbouncer.ini` 中配置](#步骤 2:在 pgbouncer.ini 中配置)
      • [2.4 权限区别](#2.4 权限区别)
      • [2.5 安全建议](#2.5 安全建议)
      • [2.6 总结对比表](#2.6 总结对比表)
    • 三、max_prepared_statements:这是最容易混淆的概念
      • [3.1 你的困惑是合理的](#3.1 你的困惑是合理的)
      • [3.2 两种预处理语句的本质区别](#3.2 两种预处理语句的本质区别)
      • [3.3 你的混淆点](#3.3 你的混淆点)
      • [3.4 `max_prepared_statements` 的作用](#3.4 max_prepared_statements 的作用)
      • [3.5 `max_prepared_statements = 200` 的含义](#3.5 max_prepared_statements = 200 的含义)
      • [3.6 哪种驱动会用到协议级预处理语句](#3.6 哪种驱动会用到协议级预处理语句)
      • [3.7 验证你的应用是否需要](#3.7 验证你的应用是否需要)
      • [3.8 完整对比总结](#3.8 完整对比总结)
    • 四、一句话总结
  • 问题
  • 回答
    • 一、关键认知:两种用户,两套认证模型
    • 二、业务用户的密码怎么来(对比理解)
    • [三、admin/stats 用户的密码怎么来(你问的核心)](#三、admin/stats 用户的密码怎么来(你问的核心))
      • [方法 1:明文密码(最简单,推荐起步用)](#方法 1:明文密码(最简单,推荐起步用))
      • [方法 2:MD5 哈希(如果用 MD5 认证)](#方法 2:MD5 哈希(如果用 MD5 认证))
      • [方法 3:SCRAM-SHA-256 哈希(推荐生产用)](#方法 3:SCRAM-SHA-256 哈希(推荐生产用))
        • [方式 3a:临时在 PG 中创建用户,拷贝哈希,再删除(推荐)](#方式 3a:临时在 PG 中创建用户,拷贝哈希,再删除(推荐))
        • [方式 3b:用 Python 生成](#方式 3b:用 Python 生成)
        • [方式 3c:用 PgBouncer 自带的命令(1.23+)](#方式 3c:用 PgBouncer 自带的命令(1.23+))
    • 四、完整操作流程(从零开始)
      • [场景:为 PgBouncer 创建 admin 和 stats 用户](#场景:为 PgBouncer 创建 admin 和 stats 用户)
    • [五、用 `auth_user` / `auth_query` 能省掉 userlist.txt 吗](#五、用 auth_user / auth_query 能省掉 userlist.txt 吗)
    • 六、两种用户的完整配置对比
    • 七、一句话总结
  • [问题: 为什么有了应用层连接池,还需要 PgBouncer?](#问题: 为什么有了应用层连接池,还需要 PgBouncer?)
    • 先搞懂一个类比
    • [场景一:只有应用层连接池(没有 PgBouncer)](#场景一:只有应用层连接池(没有 PgBouncer))
      • [问题 1:钥匙浪费](#问题 1:钥匙浪费)
      • [问题 2:钥匙数量随服务员数量线性增长](#问题 2:钥匙数量随服务员数量线性增长)
      • [问题 3:客人走后钥匙不马上还](#问题 3:客人走后钥匙不马上还)
    • [场景二:加了 PgBouncer](#场景二:加了 PgBouncer)
      • [好处 1:钥匙可以共享](#好处 1:钥匙可以共享)
      • [好处 2:用完马上还](#好处 2:用完马上还)
      • [好处 3:加服务员不增加钥匙](#好处 3:加服务员不增加钥匙)
    • 对应到技术世界
    • 一张图看懂核心区别
    • 最直白的总结

说明: 本文基于 AI 问答生成

教程

用户要求一份 PgBouncer 生产环境最佳实践培训教程,涵盖原理、配置、注意点。这是个综合性大任务,我需要先查官方文档确保内容准确,然后系统组织。

我先查关键文档,确保技术细节准确。

我先查证几个关键点的官方文档,确保培训内容准确无误。

我已经获取了三份官方文档的完整内容。现在我可以基于准确信息编写一份完整的 PgBouncer 生产环境最佳实践培训教程。

内容应该包括:

  1. 原理介绍
  2. 配置介绍
  3. 注意点(事务模式限制等)
  4. 运维监控
  5. 最佳实践

我会基于刚才对话中讨论的实际场景(40 库、单用户、常态 1-2 连接、峰值 5),把内容组织成一份完整的培训教程。

基于官方文档和之前的讨论,下面是一份完整的 PgBouncer 生产环境培训教程。


PgBouncer 生产环境最佳实践培训教程

版本基准:PgBouncer 1.25.x | 文档来源:pgbouncer.org 官方文档


第一章:核心原理

1.1 PgBouncer 是什么

PgBouncer 是一个 PostgreSQL 的轻量级连接池中间件,用 C 语言编写,单进程事件驱动。

核心价值

复制代码
                    前端(便宜)              后端(昂贵)
                ┌──────────────┐      ┌──────────────────┐
  N 个客户端 ──►│  PgBouncer   │─────►│  PostgreSQL      │
  (max_client   │  连接池复用   │ M 个  │  max_connections │
   _conn)       └──────────────┘连接   └──────────────────┘
                N >> M (复用)
指标 客户端到 PgBouncer PgBouncer 到 PostgreSQL
内存开销 2 KB/连接 5~10 MB/连接
建连成本 极低 高(PG fork 进程)
数量级 可达数千 通常数十到数百

1.2 为什么需要连接池

PostgreSQL 的连接模型是每个连接一个进程(backend process),这与 MySQL 的线程模型不同:

复制代码
客户端连接 → PG 主进程 fork → backend 进程 (5~10MB)

连接数过多的代价

  • 内存浪费:1000 个连接 × 10MB = 10GB 仅用于连接
  • 上下文切换开销:CPU 在大量进程间切换
  • max_connections 升高会降低整体性能

PgBouncer 的解决方案 :让 1000 个客户端复用 100 个后端连接。

1.3 池模型:按 (database, user) 分池

PgBouncer 不是 一个全局大池,而是按 (database, user) 二元组切分成多个独立小池:

复制代码
PgBouncer 进程
├── pool(db_A, user_X)  ← 独立池, 互不共享
├── pool(db_A, user_Y)
├── pool(db_B, user_X)
└── pool(db_B, user_Y)

关键特性

  • 每个池有独立的 pool_size 上限
  • 池是按需创建的:没有客户端连接时,池不存在
  • 连接是逐个按需建立的,不是预分配
  • 空闲连接通过 server_idle_timeout 逐步回收

第二章:三种 Pooling 模式

2.1 三种模式对比

模式 连接分配时机 连接归还时机 复用率 兼容性
session 客户端连接时 客户端断开时 1:1(最低) 100% 兼容
transaction 事务开始时 事务结束时 5~50 倍 破坏会话级功能
statement 语句执行时 语句执行后 最高 强制 autocommit

2.2 Session Pooling(会话级)

复制代码
客户端连接 ──► 分配后端连接 ──► 整个会话期间独占 ──► 客户端断开 ──► 归还池
  • 特点:最"礼貌",完全兼容 PG 所有功能
  • 缺点:复用率低,1:1 映射,几乎没有连接复用价值
  • 适用:需要会话级特性(临时表、SET 变量、LISTEN/NOTIFY 等)的应用

2.3 Transaction Pooling(事务级,生产最常用)

复制代码
BEGIN ──► 分配后端连接 ──► COMMIT ──► 归还池
                                      下个事务可能用不同连接
  • 特点:仅事务期间占用后端连接,事务结束立即归还
  • 优点:高复用率,少量后端连接支撑大量客户端
  • 缺点:破坏会话级功能(详见第四章)
  • 适用:绝大多数 OLTP Web 应用

2.4 Statement Pooling(语句级)

复制代码
每条 SQL ──► 分配连接 ──► 执行完立即归还
  • 特点:最激进,强制 autocommit,禁止多语句事务
  • 适用:PL/Proxy 等特殊场景,生产环境很少用

2.5 模式选择决策树

复制代码
应用是否使用会话级功能?
├── 是 (临时表/SET/LISTEN/会话锁)
│   └── session pooling
└── 否
    └── 是否需要多语句事务?
        ├── 是
        │   └── transaction pooling  ★ 最常用
        └── 否 (纯 autocommit)
            └── statement pooling

第三章:配置详解

3.1 完整生产配置模板

以"40 个 database、1 个 user、常态 1-2 连接/库、峰值 5/库"的场景为例:

ini 复制代码
;; ============================================================
;; pgbouncer.ini - 生产环境配置模板
;; ============================================================

[databases]
;; 方案 A: 所有库在同一台 PG (用通配符)
* = host=127.0.0.1 port=5432

;; 方案 B: 库分布在不同 PG (逐条列出, 不配 pool_size)
; db_app_main    = host=10.0.0.1 port=5432 dbname=db_app_main
; db_app_order   = host=10.0.0.1 port=5432 dbname=db_app_order
; db_tenant_01   = host=10.0.0.2 port=5432 dbname=db_tenant_01

;; 方案 C: 某库需要单独调优 (覆盖通配符)
; db_hot = host=127.0.0.1 port=5432 pool_size=5


[pgbouncer]
;; ---- 基础监听 ----
listen_addr = 0.0.0.0
listen_port = 6432
unix_socket_dir = /var/run/postgresql

;; ---- 进程身份 ----
user = pgbouncer
;; 启动后切换到此用户 (安全)

;; ---- 池化模式 ----
pool_mode = transaction          ;; ★ 生产首选

;; ---- 池大小 ----
default_pool_size = 3            ;; 常态每池上限
min_pool_size = 0                ;; 不预热 (0=禁用)
reserve_pool_size = 1            ;; 突发额外加 1
reserve_pool_timeout = 3         ;; 3秒拿不到才动用 reserve

;; ---- 前端连接 ----
max_client_conn = 1000           ;; 客户端总连接上限

;; ---- 跨池上限 ----
max_db_connections = 5           ;; ★ 单库后端连接硬上限
;; max_user_connections = 100     ;; 单用户后端连接上限 (可选)


;; ============================================================
;; 认证配置
;; ============================================================
auth_type = scram-sha-256        ;; 推荐使用 SCRAM
auth_file = /etc/pgbouncer/userlist.txt

;; 进阶: 从 PG 动态查询用户密码 (免维护 userlist.txt)
;; auth_user = pgbouncer_auth
;; auth_query = SELECT rolname, rolpassword FROM pg_authid WHERE rolname = $1 AND rolcanlogin
;; auth_dbname = postgres


;; ============================================================
;; 连接健康检查与生命周期
;; ============================================================
server_reset_query = DISCARD ALL    ;; session 模式清理 (transaction 模式不执行)
server_reset_query_always = 0       ;; 不强制在 transaction 模式执行
server_check_delay = 30             ;; 30秒内复用不检查
server_check_query = select 1       ;; 健康检查语句
server_lifetime = 3600              ;; 后端连接最多活 1 小时
server_idle_timeout = 600           ;; 空闲 10 分钟关闭
server_connect_timeout = 15         ;; 连 PG 超时
server_login_retry = 15             ;; 登录失败重试间隔
client_login_timeout = 60           ;; 客户端登录超时


;; ============================================================
;; 危险超时 (谨慎使用, 按需开启)
;; ============================================================
query_timeout = 0                   ;; 查询执行超时 (0=禁用)
query_wait_timeout = 120            ;; 客户端等连接超时, 防止无限排队
client_idle_timeout = 0             ;; 客户端空闲超时 (0=禁用)
idle_transaction_timeout = 60       ;; ★ 事务空闲 60秒强制断开
transaction_timeout = 0             ;; 整个事务超时 (0=禁用)
cancel_wait_timeout = 10            ;; 取消请求超时


;; ============================================================
;; 日志
;; ============================================================
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/postgresql/pgbouncer.pid

log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
log_stats = 1
stats_period = 60                  ;; 每 60 秒输出统计

;; 可选: syslog
;; syslog = 1
;; syslog_ident = pgbouncer
;; syslog_facility = daemon


;; ============================================================
;; 管理控制台
;; ============================================================
admin_users = dbadmin
stats_users = dbmonitor


;; ============================================================
;; 预处理语句 (transaction 模式)
;; ============================================================
max_prepared_statements = 200       ;; 跟踪的预处理语句数量


;; ============================================================
;; 底层网络 (通常不需要改)
;; ============================================================
pkt_buf = 4096
listen_backlog = 128
tcp_keepalive = 1
;; tcp_keepidle = 60
;; tcp_keepintvl = 10
;; tcp_keepcnt = 6

3.2 认证文件 userlist.txt

text 复制代码
;; 格式: "username" "password"
;; 密码支持三种格式: 明文 / MD5 / SCRAM-SHA-256

"postgres" "SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey"
"app_user" "SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey"
"dbadmin"  "SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey"

从 PG 导出 SCRAM 密码

sql 复制代码
-- 在 PG 中查询
SELECT rolname, rolpassword FROM pg_authid WHERE rolcanlogin;

3.3 PostgreSQL 侧配套配置

ini 复制代码
# postgresql.conf
max_connections = 150                      ;; 配合 PgBouncer, 大幅降低
superuser_reserved_connections = 5         ;; 留给 DBA 救火

# 认证方式 (pg_hba.conf)
# TYPE  DATABASE  USER      ADDRESS      METHOD
host    all       all       127.0.0.1/32  scram-sha-256

# 为 PgBouncer 创建专用用户 (最小权限)
# CREATE USER pgbouncer PASSWORD 'xxx';
# GRANT CONNECT ON DATABASE db_xxx TO pgbouncer;

3.4 系统级配置

bash 复制代码
# 文件描述符 (必须)
# max_client_conn=1000 至少需要 ulimit -n >= 3000
ulimit -n 65535

# /etc/security/limits.conf
# *  soft  nofile  65535
# *  hard  nofile  65535

# systemd service 配置
# /etc/systemd/system/pgbouncer.service
# [Service]
# LimitNOFILE=65535

第四章:Transaction 模式的限制(重点)

4.1 不可用的功能

Transaction pooling 模式下,以下功能完全不可用

功能 原因
SET / RESET 会话级参数不跨事务保留
LISTEN 通知通道绑定到特定后端连接
WITH HOLD CURSOR 可保持游标跨事务, 连接已归还
SQL 级 PREPARE/DEALLOCATE 预处理语句绑定到连接
临时表(PRESERVE/DELETE ROWS 行数据跨事务不保证
LOAD 语句 加载的库绑定到连接
会话级咨询锁 pg_advisory_lock() 绑定到连接

4.2 仍可用的功能

功能 说明
启动参数 client_encodingDateStyleTimezone
NOTIFY 单次通知可用
普通游标 WITHOUT HOLD 在事务内可用
协议级预处理语句 需配 max_prepared_statments > 0
ON COMMIT DROP 临时表 事务结束时自动删除
DEALLOCATE ALL / DISCARD ALL 清理 PgBouncer 跟踪的语句

4.3 预处理语句的特殊机制

PgBouncer 在 transaction 模式下对协议级预处理语句有特殊支持:

复制代码
客户端: PREPARE stmt1 AS SELECT * FROM users WHERE id=$1
         ↓
PgBouncer 内部跟踪:
  - 分配内部名称: PGBOUNCER_1
  - 记录查询字符串
         ↓
执行时:
  - 若当前后端连接未准备此语句 → 透明地先 PREPARE 再 EXECUTE
  - 相同查询字符串跨客户端复用

配置

ini 复制代码
max_prepared_statements = 200    ;; 设为应用常用预处理语句数量

已知问题

  • DDL 变更后可能报错 cached plan must not change result type
  • 解决:执行 RECONNECT 强制重建连接

4.4 会话级状态的处理建议

如果应用必须使用会话级功能,有三种方案:

复制代码
方案 1: 改用 session pooling (牺牲复用率)
方案 2: 应用改造, 将会话状态存到应用层或 Redis
方案 3: 混合模式 (部分库 session, 部分库 transaction)

混合模式配置

ini 复制代码
[databases]
db_app_main = host=... pool_mode=transaction    ;; OLTP 库
db_report   = host=... pool_mode=session        ;; 报表库需要临时表

4.5 server_reset_query 在 transaction 模式下不执行

ini 复制代码
server_reset_query = DISCARD ALL    ;; 仅 session 模式生效
server_reset_query_always = 0       ;; transaction 模式不执行

原因 :transaction 模式下每个事务可能在不同连接上执行,执行 DISCARD ALL 没有意义(甚至有害)。

强制执行的风险 :设 server_reset_query_always=1 会导致每次事务结束后都执行 DISCARD ALL会破坏应用状态,不推荐。


第五章:参数详解与配置经验

5.1 池大小参数

参数 作用 配置经验
default_pool_size 每池默认上限 常态连接数 × 1.5~2
min_pool_size 每池最小连接数 0(默认);设 1 可抗突发但有前提
reserve_pool_size 突发额外连接 1~3,不宜过大
reserve_pool_timeout 何时动用 reserve 3~5 秒
max_db_connections 单库硬上限 锁死峰值
max_user_connections 单用户硬上限 多应用共用时使用

核心公式

复制代码
Σ pool_size ≤ PG max_connections × 0.8
max_db_connections = 历史峰值
default_pool_size = 常态连接数 + 少量余量

5.2 前端连接参数

参数 作用 配置经验
max_client_conn 客户端总连接上限 后端活跃 × 复用倍数(10~15)

推算方法

复制代码
后端活跃连接 ≈ 60 (40库 × 1.5)
transaction 复用倍数 ≈ 15
前端可承载 = 60 × 15 = 900
突发余量 × 1.5 ≈ 1000

5.3 超时参数

参数 默认值 推荐值 说明
server_idle_timeout 600s 600s 空闲连接回收
server_lifetime 3600s 3600s 连接最大生命周期
query_wait_timeout 120s 60s 客户端排队超时
idle_transaction_timeout 0(禁用) 60s 事务空闲超时(推荐开启)
client_idle_timeout 0(禁用) 0 客户端空闲超时(按需)

三个超时容易混淆

复制代码
客户端连接进来, 不做事      → client_idle_timeout
客户端 BEGIN 后不做事       → idle_transaction_timeout
客户端 BEGIN 后一直执行慢查询 → transaction_timeout

5.4 参数取值优先级

每个池的 pool_size 按以下优先级取值(取第一个命中的):

复制代码
1. [databases] 段的 pool_size=      (最高优先级)
2. [users] 段的 pool_size=
3. default_pool_size                  (兜底)

第六章:运维与监控

6.1 启动与停止

bash 复制代码
# 启动
pgbouncer -d /etc/pgbouncer/pgbouncer.ini

# 前台启动 (调试)
pgbouncer -v /etc/pgbouncer/pgbouncer.ini

# 重新加载配置
kill -HUP $(cat /var/run/postgresql/pgbouncer.pid)
# 或连接控制台执行 RELOAD

# 安全停止 (等待客户端断开)
kill -TERM $(cat /var/run/postgresql/pgbouncer.pid)

# 立即停止
kill -QUIT $(cat /var/run/postgresql/pgbouncer.pid)

6.2 管理控制台

bash 复制代码
# 连接管理控制台
psql -p 6432 -U dbadmin pgbouncer

6.3 核心监控命令

sql 复制代码
-- ★ 查看各池状态 (最常用)
SHOW POOLS;
-- 关注: cl_active, cl_waiting, sv_active, sv_idle, maxwait

-- 查看统计信息
SHOW STATS;
-- 关注: avg_query_time, avg_wait_time, total_query_count

-- 汇总统计
SHOW TOTALS;

-- 查看后端连接详情
SHOW SERVERS;

-- 查看客户端连接详情
SHOW CLIENTS;

-- 查看数据库配置
SHOW DATABASES;

-- 查看当前配置
SHOW CONFIG;

-- 查看内部状态
SHOW LISTS;
SHOW MEM;
SHOW STATE;

6.4 SHOW POOLS 关键指标解读

text 复制代码
 database  | user  | cl_active | cl_waiting | sv_active | sv_idle | sv_used | maxwait
-----------+-------+-----------+------------+-----------+---------+---------+--------
 db_main   | app   | 15        | 0          | 3         | 0       | 0       | 0       ← 正常
 db_order  | app   | 20        | 5          | 3         | 0       | 0       | 2.3     ← ⚠️ 排队了
 db_arch   | app   | 0         | 0          | 0         | 0       | 0       | 0       ← 冷库无连接
指标 健康值 异常信号
cl_waiting 0 > 0 说明池不够用
maxwait 0 持续增长说明严重不足
sv_idle ≤ pool_size 长期等于 pool_size 说明池过大
sv_active < pool_size 长期等于 pool_size 说明池过小

6.5 常见运维场景

场景 1:数据库重启
sql 复制代码
-- 1. 暂停 (等待当前事务完成, 新连接排队)
PAUSE db_main;

-- 2. 在 PG 侧执行重启
-- systemctl restart postgresql

-- 3. 恢复
RESUME db_main;
场景 2:修改配置后生效
sql 复制代码
RELOAD;
-- 如修改了 pool_size 等需重建连接的参数
WAIT_CLOSE db_main;
场景 3:PG 故障转移
sql 复制代码
-- 渐进式 (只读副本切换)
RECONNECT db_main;
WAIT_CLOSE db_main;

-- 紧急切换
KILL db_main;
-- 修改配置指向新 PG
RELOAD;
RESUME db_main;
场景 4:零停机滚动重启
bash 复制代码
# 使用 so_reuseport 多进程方案
# 1. 启动两个 PgBouncer 进程监听同一端口
pgbouncer -d /etc/pgbouncer/pgbouncer.ini   # 进程 A
pgbouncer -d /etc/pgbouncer/pgbouncer.ini   # 进程 B (so_reuseport=1)

# 2. 滚动重启进程 A
psql -p 6432 pgbouncer -c "SHUTDOWN WAIT_FOR_CLIENTS;"
# 等待客户端重连到进程 B 后, 进程 A 自动退出

# 3. 重启进程 A
pgbouncer -d /etc/pgbouncer/pgbouncer.ini

# 4. 对进程 B 重复上述步骤

6.6 信号速查

信号 等价命令 说明
SIGHUP RELOAD 重新加载配置
SIGTERM SHUTDOWN WAIT_FOR_CLIENTS 等待客户端断开后关闭
SIGINT SHUTDOWN WAIT_FOR_SERVERS 等待后端连接释放后关闭
SIGQUIT SHUTDOWN 立即关闭
SIGUSR1 PAUSE 暂停
SIGUSR2 RESUME 恢复

第七章:生产最佳实践

7.1 容量规划清单

复制代码
1. 统计现状
   □ 直连 PG 时各库的常态连接数
   □ 直连 PG 时各库的峰值连接数
   □ 应用部署台数 × 每台连接池大小

2. 选择 pool_mode
   □ 应用是否使用临时表/SET/LISTEN/会话锁?
       是 → session (或混合模式)
       否 → transaction ★

3. 配置池大小
   □ default_pool_size = 常态连接数 × 1.5~2
   □ max_db_connections = 历史峰值
   □ reserve_pool_size = 1~3
   □ Σ pool_size ≤ PG max_connections × 0.8

4. 配置前端
   □ max_client_conn = 后端活跃 × 复用倍数(10~15) × 突发余量(1.5)
   □ 确认 ulimit -n ≥ max_client_conn × 2 + 1000

5. 配置 PG
   □ max_connections = Σ pool_size / 0.8 (向上取整)
   □ superuser_reserved_connections = 5~10
   □ shared_buffers / work_mem 按内存调整

7.2 安全最佳实践

ini 复制代码
# 1. 使用 SCRAM 认证
auth_type = scram-sha-256

# 2. PgBouncer 以非 root 用户运行
user = pgbouncer

# 3. 最小权限: 为 PgBouncer 创建专用 PG 用户
# CREATE USER pgbouncer_app PASSWORD 'xxx';
# GRANT CONNECT ON DATABASE db_xxx TO pgbouncer_app;

# 4. 管理控制台权限分离
admin_users = dbadmin      ;; 只有 DBA 有管理权限
stats_users = monitor      ;; 监控用户只读

# 5. 限制监听地址 (生产环境)
listen_addr = 10.0.0.5     ;; 只监听内网, 不用 0.0.0.0

# 6. 启用 TLS (可选, 跨网络时推荐)
client_tls_sslmode = require
client_tls_key_file = /etc/ssl/private/pgbouncer.key
client_tls_cert_file = /etc/ssl/certs/pgbouncer.crt

7.3 性能调优清单

ini 复制代码
# 1. 开启 idle_transaction_timeout (防止漏 COMMIT 卡连接)
idle_transaction_timeout = 60

# 2. 合理设置 query_wait_timeout (防止客户端无限排队)
query_wait_timeout = 60

# 3. 开启预处理语句跟踪 (transaction 模式)
max_prepared_statatements = 200

# 4. 应用连接池大小 ≤ PgBouncer pool_size
# 应用 HikariCP maxPoolSize = 20~50 (放心开大)

# 5. 监控 avg_wait_time (应 < 1ms)
# SHOW STATS; 若 avg_wait_time 高, 说明池不够

# 6. 定期重建连接 (防止长连接内存泄漏)
server_lifetime = 3600

7.4 监控告警

sql 复制代码
-- 告警 1: 客户端排队 (cl_waiting > 0 持续)
SHOW POOLS;

-- 告警 2: 客户端连接接近上限
SHOW TOTALS;  -- cur_client / max_client_conn > 80%

-- 告警 3: 后端连接接近上限
SELECT count(*) FROM pg_stat_activity;
-- count / max_connections > 80%

-- 告警 4: idle in transaction 过多
SELECT count(*) FROM pg_stat_activity WHERE state = 'idle in transaction';

-- 定期采集脚本
psql -p 6432 pgbouncer -c "SHOW POOLS;" >> /var/log/pgbouncer/pools.log
psql -p 6432 pgbouncer -c "SHOW STATS;" >> /var/log/pgbouncer/stats.log

7.5 常见踩坑清单

症状 解决
max_client_conn = max_connections 复用价值丧失 前者远大于后者
Σ pool_size > max_connections 部分池拿不到连接 前者 ≤ 后者 × 0.8
应用连接池 > PgBouncer pool_size 延迟尖峰 应用侧 ≤ PgBouncer
transaction 模式用临时表 报错或数据丢失 改 session 或改造应用
忘记设 max_db_connections 单库吃光 PG 连接 锁死单库上限
ulimit -n 太小 大量连接时报错 调到 65535
server_reset_query_always=1 破坏应用状态 保持 0
DDL 后预处理语句报错 cached plan must not change result type 执行 RECONNECT
忘记留给 superuser 的连接 DBA 连不上 superuser_reserved_connections=5

第八章:故障排查

8.1 客户端连接被拒

bash 复制代码
# 1. 检查 PgBouncer 是否运行
ps aux | grep pgbouncer

# 2. 检查端口
netstat -tlnp | grep 6432

# 3. 检查 max_client_conn
psql -p 6432 pgbouncer -c "SHOW TOTALS;"
# cur_client 是否接近 max_client_conn?

# 4. 检查认证
tail -f /var/log/pgbouncer/pgbouncer.log | grep -i "auth"

8.2 查询延迟高

sql 复制代码
-- 1. 检查是否有排队
SHOW POOLS;
-- cl_waiting > 0 或 maxwait > 0 → 池不够

-- 2. 检查 avg_wait_time
SHOW STATS;
-- avg_wait_time > 1ms → 后端响应慢

-- 3. 检查 PG 侧
psql -p 5432 -c "
  SELECT state, count(*), max(now() - state_change) as max_duration
  FROM pg_stat_activity
  WHERE backend_type = 'client backend'
  GROUP BY state;
"
-- idle in transaction 多 → 应用漏 COMMIT

8.3 连接泄漏

sql 复制代码
-- 1. 检查后端连接数趋势
psql -p 5432 -c "SELECT count(*) FROM pg_stat_activity;"

-- 2. 若持续增长, 检查 server_lifetime 和 server_idle_timeout
SHOW CONFIG;
-- server_idle_timeout 是否设为 0 (禁用了回收)?

-- 3. 强制重建连接
RECONNECT;

第九章:核心配置速查表

参数 默认值 生产推荐 说明
pool_mode session transaction 池化模式
max_client_conn 100 1000~5000 前端连接上限
default_pool_size 20 2~5 常态每池上限
max_db_connections 0(无限) 峰值 单库硬上限
reserve_pool_size 0 1~3 突发缓冲
reserve_pool_timeout 5 3 动用 reserve 的等待
server_idle_timeout 600 600 空闲回收
server_lifetime 3600 3600 连接最大生命周期
query_wait_timeout 120 60 客户端排队超时
idle_transaction_timeout 0 60 事务空闲超时
max_prepared_statements 200 200 预处理语句跟踪
auth_type md5 scram-sha-256 认证方式
server_reset_query DISCARD ALL DISCARD ALL session 模式清理

第十章:一句话总结

PgBouncer 的核心价值是"压缩后端、释放前端 ":用少量昂贵的 PG 后端连接,支撑大量便宜的客户端连接。生产环境首选 transaction pooling 模式,配合 default_pool_size(管常态)、max_db_connections(锁峰值)、idle_transaction_timeout(防漏 COMMIT)、max_prepared_statements(支持预处理语句),并确保 Σ pool_size ≤ PG max_connections × 0.8用 SHOW POOLS 的 maxwaitcl_waiting 做调优依据,而非拍脑袋

问题 1

db_tenant_01 = host=127.0.0.1 port=5432 dbname=db_tenant_01 pool_size=2

这里 pool_size = 2 是预先分配的吗 还是客户端访问才会申请两个连接 还是一个个增长 最多到 2 个?

回复 1

这是个非常深入的问题,涉及 PgBouncer 的连接建立时机机制。我需要查源码或官方文档确认准确行为,避免臆测。

关键问题:

  1. pool_size=2 是预先分配 2 个连接吗?
  2. 还是客户端访问时一次性建立 2 个?
  3. 还是一个一个按需增长,上限 2 个?

我查一下官方文档和相关源码。

好问题,涉及 PgBouncer 的连接建立时机机制。我先查官方文档和源码确认行为,避免臆测。

查到了明确的答案。结论很清晰:

  1. pool_size=2 不是预分配
  2. 不是客户端访问时一次性申请 2 个
  3. 一个个按需增长,上限 2 个

关键证据:

  • min_pool_size=0(默认),完全不维护最小连接数
  • 连接是逐个按需创建
  • 即使设 min_pool_size > 0,也只有在至少有一个客户端连接到该池时才生效
  • 池在完全空闲(无客户端连接)时,所有服务器连接最终都会被关闭

所以答案是:一个个增长,最多到 2 个

我应该清晰直接地回答,并给出实际演进过程。用户的疑问很具体,我需要:

  1. 直接给出答案
  2. 解释增长机制
  3. 给出实际时间线示例
  4. 介绍 min_pool_size 如果想预热的情况
  5. 介绍 server_idle_timeout 的回收机制

简洁明了地回答。

答案明确:一个个按需增长,最多到 2 个,且空闲会回收。PgBouncer 不会预分配、也不会批量建立。


一、直接结论

你的猜测 是否正确
预先分配 2 个连接
客户端访问时一次性申请 2 个
一个个按需增长,上限 2 个

pool_size上限,不是预分配数量。


二、连接增长的实际过程

db_tenant_01pool_size=2 为例:

复制代码
时间线                后端连接数    说明
─────────────────────────────────────────────────────────
PgBouncer 启动        0           池不存在, 不预分配
首个客户端连接进来     0→1         按需新建 1 个后端连接
客户端查询完成        1           连接保持(空闲), 不关闭
第二个客户端同时访问   1→2         再新建 1 个, 达到上限 2
第三个客户端访问       2(排队)     达上限, 客户端等待
某客户端释放          2           仍保持 2(复用空闲的)
所有客户端断开 + 空闲 10 分钟      2→1→0      server_idle_timeout 逐步关闭

关键特征

  • 每次客户端请求时最多新建 1 个连接(不是一次性建 2 个)
  • 达到 pool_size 上限后排队等待,不超额建立
  • 空闲超时后逐步回收,最终可能归零

三、池的"生"与"死"

复制代码
                     ┌─ 客户端有请求 ──► 按需新建连接 (上限 pool_size)
                     │
池生命周期            ├─ 客户端空闲 ────► 连接保持, 等待复用
                     │
                     └─ 空闲超时 ──────► server_idle_timeout 关闭连接
                                        最终可能归零(池实际消失)

重要 :池本身是按需创建 的。没有客户端连过 db_tenant_01 时,这个池根本不存在SHOW POOLS 看不到它。


四、如果想"预热":用 min_pool_size

默认 min_pool_size=0,不维护最小连接数。如果希望冷启动时就有连接可用(减少首个请求的建连延迟),可以这样配:

ini 复制代码
[pgbouncer]
default_pool_size = 2
min_pool_size = 1            ; ★ 维持至少 1 个连接

但有个重要前提(来自官方文档):

min_pool_size 仅在以下情况生效:

  1. 该 database 配置了 forced user(在 [databases] 段指定了 user=),
  2. 至少有一个客户端已连接到该池

也就是说,即使配了 min_pool_size=1,在没有任何客户端连过 时,池仍然是空的。只有第一个客户端连进来后,PgBouncer 才会逐步补充到 min_pool_size 个连接并维持。

复制代码
无 min_pool_size:     0 → 1 → 2 → (空闲) → 1 → 0
有 min_pool_size=1:   0 → 1 → 2 → (空闲) → 1 → 1 (维持不下于 1)

五、连接回收机制

连接不会一直占着,有两道闸门逐步回收:

参数 默认值 作用
server_idle_timeout 600 秒 后端连接空闲超过此值就关闭
server_lifetime 3600 秒 后端连接存活超过此值就关闭(无论是否空闲)

所以你的 pool_size=2 在实际运行中:

复制代码
业务高峰: 2 个连接 (上限)
业务低谷: 1 个连接 (另一个被 server_idle_timeout 关闭)
完全空闲: 0 个连接 (全部被回收, 池消失)

六、验证方法

bash 复制代码
# 1. 查看池的实时连接数
psql -p 6432 -d pgbouncer -c "SHOW POOLS;"
# 关注列: cl_active, cl_waiting, sv_active, sv_idle, sv_used, sv_tested

# 2. 持续观察增长过程
watch -n 2 "psql -p 6432 -d pgbouncer -c 'SHOW POOLS;'"

# 3. 查看后端连接详情(每个连接的 IP/状态/连接时长)
psql -p 6432 -d pgbouncer -c "SHOW SERVERS;"

SHOW POOLS 输出各列含义:

含义
cl_active 正在执行查询的客户端数
cl_waiting 排队等待后端连接的客户端数
sv_active 正在被客户端使用的后端连接数
sv_idle 空闲可复用的后端连接数
sv_used 已分配但还没开始查询的连接数

七、一句话总结

pool_size=2上限而非预分配 :客户端请求时一个个按需新建,最多到 2 个;空闲后通过 server_idle_timeout(默认 10 分钟)逐步回收,最终可能归零。需要预热可配 min_pool_size=1,但仅在已有客户端连接后才生效。PgBouncer 的设计哲学是"惰性建立、积极回收"

问题 2

如果我不确定 具体哪些 database 最多 5 个 我是不是不用单独对每个 db 做设置

而只设置整体的?

回答 2

用户问的是:如果不确定哪些 database 峰值 5 个连接,是否可以不对每个 db 单独设置 pool_size,而只用全局 default_pool_size?

这是个很实际的简化配置问题。我应该:

  1. 明确回答:可以,且推荐这么做
  2. 给出简化配置
  3. 解释如何通过 max_db_connections 作为安全网
  4. 解释事后如何通过观察 SHOW POOLS 来发现需要单独调的库

要点:

  • 全局 default_pool_size 设一个适合常态的值(比如 2-3)
  • 用 max_db_connections 作为单库硬上限(5),无论哪个库突发都不会超过
  • 不需要在 databases 段逐个写 pool_size
  • 用通配符 * 或只写 host 映射,甚至可以用 wildcard database

PgBouncer 支持通配符数据库配置:* = host=... 这样任何连进来的 db 都自动映射。

给一个干净的简化配置。

完全可以,且这正是推荐做法 。配合 max_db_connections 作为安全网,你完全不需要预先判断哪些库是"热库"。


一、为什么这样做更好

做法 问题
提前猜哪些库热,逐个配 pool_size=5 猜错就白配,运维负担重
全局 default_pool_size + max_db_connections 兜底 自适应,靠数据说话

核心思路:

  • default_pool_size 设适合常态的值(你的常态是 1-2,设 2 或 3)
  • max_db_connections 锁死峰值上限(你的峰值 5)
  • 哪个库突发都吃不到超过 5 个连接
  • 事后用 SHOW POOLS 观察哪些库真的需要单独调

二、推荐简化配置

pgbouncer.ini

ini 复制代码
[databases]
; 用通配符, 所有 db 自动映射到同一台 PG, 不逐个配 pool_size
* = host=127.0.0.1 port=5432

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432

; --- 池核心 ---
pool_mode = transaction
default_pool_size = 3           ; ★ 常态 1-2, 给点余量设 3
reserve_pool_size = 1          ; 突发加 1 (上限 = 3+1 = 4)
reserve_pool_timeout = 3

; --- 前端 ---
max_client_conn = 1000

; --- 单库硬上限: 锁死不超过 5 ---
max_db_connections = 5          ; ★ 关键安全网, 任何库都不会超过 5

; --- 超时防护 ---
server_idle_timeout = 600
query_wait_timeout = 60
idle_transaction_timeout = 60

; --- 连接生命周期 ---
server_lifetime = 3600
server_connect_timeout = 15

; --- 日志 ---
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60

PostgreSQL 侧

ini 复制代码
# postgresql.conf
max_connections = 150
superuser_reserved_connections = 5

三、关键点:max_db_connections 是真正的安全网

这是你方案中最关键的一行

ini 复制代码
max_db_connections = 5

它的作用

场景 default_pool_size=3 的效果 max_db_connections=5 的效果
某库常态 1-2 连接 池按需建 1-2 个, 不浪费 不触发, 无影响
某库突发到 3 个并发 池建到 3 个 不触发, 还在限内
某库突发到 4 个 reserve_pool_size=1 启动, 到 4 不触发, 还在限内
某库突发到 6+ 个 已达 reserve 上限 4, 客户端排队 硬卡死在 5, 多余客户端排队等待

所以你完全不需要预先知道哪些库会突发 ------max_db_connections=5 会自动处理所有库。


四、通配符 * 的用法

ini 复制代码
[databases]
* = host=127.0.0.1 port=5432

这行的含义是:任何客户端请求的 database 名,都自动映射到 127.0.0.1:5432 上的同名 database

  • 客户端连 db_tenant_01 → PgBouncer 自动连到 PG 的 db_tenant_01
  • 客户端连 db_archive_05 → PgBouncer 自动连到 PG 的 db_archive_05
  • 不需要在 [databases] 段列出 40 个库名

注意 :通配符只能用于"所有库都连同一个 PG 实例"的场景。如果你的 40 个库分布在多台 PG 上,就不能用 *,但仍然可以不配 pool_size(走 default):

ini 复制代码
[databases]
; 分散在多台 PG, 但都不配 pool_size
db_app_main    = host=10.0.0.1 port=5432 dbname=db_app_main
db_app_order   = host=10.0.0.1 port=5432 dbname=db_app_order
db_tenant_01   = host=10.0.0.2 port=5432 dbname=db_tenant_01
; ... 只配连接信息, 不配 pool_size, 全部走 default_pool_size

五、事后如何发现需要单独调的库

跑一段时间后(比如一周),用这些命令观察哪些库接近上限:

bash 复制代码
# 1. 看各池的连接使用情况
psql -p 6432 -d pgbouncer -c "SHOW POOLS;" | column -t

# 输出示例:
#  database    | user    | cl_active | cl_waiting | sv_active | sv_idle | max_wait
#  db_app_main | postgres| 12        | 3          | 3         | 1       | 2.5      ← 排队了!
#  db_tenant_01| postgres| 1         | 0          | 1         | 0       | 0
#  db_archive_01|postgres| 0         | 0          | 0         | 0       | 0

调优信号

观察到 含义 处理
cl_waiting > 0max_wait 该库池不够用 单独给这个库配 pool_size=5
sv_idle 长期等于 pool_size 该库池过大 全局 default_pool_size 可调小
max_wait = 0 无等待 配置合理, 无需动

只有观察到某库频繁排队时,才在 [databases] 段单独给它配

ini 复制代码
[databases]
* = host=127.0.0.1 port=5432
db_app_main = host=127.0.0.1 port=5432 pool_size=5    ; 只给这个库单独配

通配符和具体条目可以共存,具体条目优先于通配符


六、最终对比

维度 之前(逐个配) 现在(全局+兜底)
配置复杂度 高, 40 行 低, 1 行 *=
需要预先知道哪些库热
单库峰值保护 pool_size max_db_connections
事后调优 已固化, 难改 观察后再单独调, 灵活
Σ pool_size 上限 89 40×3=120 (稍高, 但有 max_db_connections 兜底)

PG max_connections 需要相应调整:

复制代码
Σ pool_size 上限 = 40 × 3 + 40 × 1(reserve) = 160
但有 max_db_connections=5 限制:
  实际同时满载概率低, 40×5=200 是理论极限
  实际峰值约 60~80
max_connections = 150 仍够用

七、一句话总结

不要提前猜,用全局 default_pool_size=3 + max_db_connections=5 兜底 。前者管常态(1-2 连接),后者管峰值(锁死 5)。配合通配符 * = host=... 一行搞定 40 个库映射。跑一周后用 SHOW POOLS 看哪个库排队了,再单独给它配 pool_size=5------用数据驱动调优,而非拍脑袋

问题 3

当前案例中max_client_conn = 1000 是如何思考设计的?

回答 3

The user is asking about how I arrived at the value of 1000 for max_client_conn in their specific case. This is a great question - they want to understand the reasoning/design logic behind that specific number. Let me walk through the thinking carefully.

Key context:

  • 40 databases, 1 user, transaction pooling mode
  • Before PgBouncer: each db had max 5 connections, most of the time 1-2
  • Total backend connections: ~60 normal, ~100 peak
  • PG max_connections = 150

The reasoning for max_client_conn = 1000 involves multiple factors. Let me think through this systematically.

这是个很值得讲透的问题。max_client_conn=1000 不是拍脑袋定的,而是基于你的实际场景反推的。


一、核心认知:max_client_conn 管的是"便宜连接"

先明确它的本质:

复制代码
客户端 ──► PgBouncer (max_client_conn 管这层, 便宜, 2KB/个)
              │
              ▼
           PostgreSQL (max_connections 管这层, 贵, 5-10MB/个)
  • max_client_conn 限制的是客户端到 PgBouncer 的连接数
  • 每个客户端连接只占 2 KB 内存(官方数据),1000 个才 2 MB,几乎不费资源
  • 它的真正作用不是"防止 PgBouncer 被压垮",而是防止客户端数量失控后拖垮 PgBouncer 自身的文件描述符/内存

所以 max_client_conn 可以也应该开得比后端连接数大很多


二、推算过程(5 个视角交叉验证)

视角 1:从"原直连峰值"反推

复制代码
原直连模式:
  40 库 × 峰值 5 = 理论 200 连接
  但实际峰值总和 ≈ 80~100 (错峰)
  
这就是你 PG 原来 max_connections=200 的依据。

引入 PgBouncer 后,后端连接数不变甚至下降,但前端可以放大。

视角 2:从"transaction pooling 复用倍数"推算

transaction 模式的复用率通常在 5~20 倍(取决于事务占比和持有时长):

复制代码
复用倍数 = 客户端连接数 / 后端活跃连接数

你的场景:
  后端常态活跃 ≈ 30~60 (40库 × 平均1.5)
  
若复用 10 倍:
  前端可承载 = 60 × 10 = 600
  
若复用 15 倍:
  前端可承载 = 60 × 15 = 900
  
取上限并留余量 → 1000

视角 3:从"应用部署规模"推算

假设你的应用部署情况:

复制代码
应用服务器台数 × 每台应用连接池大小 = 客户端连接数

典型场景:
  5 台应用 × 每台 HikariCP maxPoolSize=50 = 250
  10 台应用 × 每台 HikariCP maxPoolSize=50 = 500
  20 台应用 × 每台 HikariCP maxPoolSize=50 = 1000

→ 1000 能覆盖 20 台应用各开 50 连接池的场景

视角 4:从"突发容灾"推算

复制代码
常态:    ~250 客户端连接
高峰:    ~500
突发(重试/重连风暴): 常态 × 2 = 1000

→ 1000 留了 2 倍的突发余量

视角 5:从"资源开销"反向验证

复制代码
max_client_conn = 1000
每个客户端连接内存 ≈ 2 KB
总内存开销 = 1000 × 2 KB = 2 MB  ← 几乎可忽略

文件描述符 = 1000 个
默认 ulimit -n 通常 256 或 1024
→ 需要确认系统 fd 限制足够 (建议调到 65535)

三、五个视角汇总

推算视角 计算逻辑 得出值
原直连峰值放大 200 × 5 倍 1000
transaction 复用率 60 × 15 倍 900
应用部署规模 20 台 × 50 1000
突发容灾 500 × 2 1000
资源开销验证 2MB 内存, 可忽略 无瓶颈

五个视角都指向 1000 附近,所以 1000 是个合理的设计值。


四、为什么不是更小或更大

为什么不设 200(保守)

复制代码
max_client_conn = 200
↓
5 台应用 × 40 连接池就占满
突发重连时客户端被拒, 报 "no more connections allowed"
→ 失去 PgBouncer "放大前端" 的核心价值

为什么不设 10000(激进)

复制代码
max_client_conn = 10000
↓
1. 后端 Σ pool_size 才 120, 前端 10000 几乎都用不上
2. 文件描述符压力增大 (需调 ulimit)
3. 若应用代码有连接泄漏, 10000 会被慢慢吃满
4. 失去了"早期预警"作用

设计原则max_client_conn 应该略大于你预期的峰值,留 2-3 倍余量即可,不是越大越好。


五、动态调优方法

max_client_conn 不是配完就完了,应该根据实际使用调整:

bash 复制代码
# 1. 观察常态客户端连接数
psql -p 6432 -d pgbouncer -c "SHOW TOTALS;"
# 关注 total_xact_count, total_query_count, cur_client

# 2. 持续监控峰值
watch -n 60 "psql -p 6432 -d pgbouncer -c 'SHOW TOTALS;' | grep -i client"

# 3. 看是否接近上限
psql -p 6432 -d pgbouncer -c "SHOW TOTALS;"
# 若 cur_client 长期 > 800 (80%), 考虑调大到 1500
# 若 cur_client 长期 < 100 (10%), 可调小到 500

调优信号表

cur_client 常态占比 调整建议
< 10% 调小 max_client_conn
10%~70% 合理,不动
70%~90% 关注,准备调大
> 90% 立即调大,否则突发会拒绝服务

六、一个容易被忽略的前提:系统 fd 限制

max_client_conn=1000 意味着 PgBouncer 进程需要至少 1000 个文件描述符,加上后端连接、日志等,建议系统级 ulimit 调到更高:

bash 复制代码
# 检查当前限制
ulimit -n
# 通常默认 256 或 1024

# 临时调大
ulimit -n 65535

# 永久生效 (写入 /etc/security/limits.conf 或 systemd 配置)
# /etc/security/limits.conf:
# *  soft  nofile  65535
# *  hard  nofile  65535

# 若用 systemd 管理 pgbouncer:
# /etc/systemd/system/pgbouncer.service 中加:
# LimitNOFILE=65535

经验公式

复制代码
ulimit -n ≥ max_client_conn × 2 + 1000 (余量)
        = 1000 × 2 + 1000 = 3000
        → 设 65535 留充足余量

七、完整设计逻辑链

复制代码
你的现状
  ├─ 40 库, 常态 1-2 连接/库, 峰值 5/库
  ├─ 后端连接总和常态 ~60, 峰值 ~100
  └─ PG max_connections=200

         ↓ 引入 PgBouncer (transaction pooling)

后端层设计
  ├─ default_pool_size = 3 (常态够用)
  ├─ max_db_connections = 5 (锁死单库峰值)
  └─ PG max_connections = 150 (后端压缩)

前端层设计  ← 这就是你问的
  ├─ 复用倍数: transaction 模式 10-15 倍
  ├─ 后端活跃 ≈ 60 → 前端可承载 600~900
  ├─ 应用规模: 20 台 × 50 连接池 = 1000
  ├─ 突发余量: 2 倍
  └─ max_client_conn = 1000

资源验证
  ├─ 内存: 1000 × 2KB = 2MB (可忽略)
  ├─ fd: 需 ulimit -n ≥ 3000, 设 65535
  └─ PG 压力: 不受影响 (后端仍受 pool_size 约束)

八、一句话总结

max_client_conn=1000 是从后端活跃连接 60 × transaction 复用 15 倍 ≈ 900 ,再结合应用部署规模 20 台 × 50 = 1000 ,加上2 倍突发余量 交叉验证得出的。本质是:后端 100 连接的服务能力,通过 transaction pooling 在前端放大到 1000------这正是 PgBouncer 的价值所在。关键前提是确认系统 ulimit -n 足够(建议 65535)。

不一定。max_prepared_statements 配小了只是可能原因之一,还有多个其他可能。我按"从最可能到最不可能"的顺序给你排查清单。


一、五大可能原因(按发生概率排序)

复制代码
报错: prepared statement does not exist
         │
         ▼
┌──────────────────────────────────────────────────┐
│ 原因 1: PgBouncer 版本 < 1.21 (功能根本不存在)    │  最常见
│ 原因 2: max_prepared_statements = 0 (功能被禁用)  │  很常见
│ 原因 3: 应用用了 SQL 级 PREPARE (而非协议级)      │  常见
│ 原因 4: 值确实配小了 (唯一查询数超过上限)          │  偶发
│ 原因 5: 客户端驱动兼容性问题                      │  特定场景
└──────────────────────────────────────────────────┘

二、逐个排查

原因 1:PgBouncer 版本 < 1.21(功能根本不存在)

这是最常见的根因max_prepared_statements 功能从 1.21.0 才引入:

Since version 1.21.0 PgBouncer can track prepared statements in transaction pooling mode.

如果你的版本 < 1.21,这个参数根本不生效,PgBouncer 不会跟踪预处理语句,必然报错。

检查方法

bash 复制代码
# 方法 1: 命令行
pgbouncer -V

# 方法 2: 控制台
psql -p 6432 pgbouncer -c "SHOW VERSION;"

# 方法 3: 包管理器
pgbouncer --version
# 或
apt list --installed 2>/dev/null | grep pgbouncer
yum list installed pgbouncer

判断

版本 是否支持协议级 prepared statement 处理方式
< 1.21 ❌ 完全不支持 升级到 1.21+
≥ 1.21 ✅ 支持(需配置) 检查 max_prepared_statements

升级方法

bash 复制代码
# Ubuntu/Debian (官方源)
apt update && apt install --only-upgrade pgbouncer

# 或用 PostgreSQL 官方 APT 源
# 添加 PGDG 源后:
apt install pgbouncer

# macOS
brew upgrade pgbouncer

# 从源码编译
git clone https://github.com/pgbouncer/pgbouncer.git
cd pgbouncer
git checkout pgbouncer_1_23_0  # 选最新稳定版
./autogen.sh && ./configure && make && make install

原因 2:max_prepared_statements = 0(功能被禁用)

即使版本 ≥ 1.21,如果显式设为 0,功能也被禁用。

检查方法

bash 复制代码
psql -p 6432 pgbouncer -c "SHOW CONFIG;" | grep max_prepared

输出示例

复制代码
            key           | value | default | changeable
--------------------------+-------+---------+-----------
 max_prepared_statements  | 0     | 200     | yes

关键认知

  • 默认值是 200(不是 0)
  • 但如果有人在配置文件中显式写了 max_prepared_statements = 0,就禁用了
  • 如果配置文件中根本没写这一行,走默认值 200(没问题)

修复

ini 复制代码
# pgbouncer.ini
max_prepared_statements = 200    ;; 确保非零, 或删掉这行走默认 200

改完后重载:

bash 复制代码
psql -p 6432 pgbouncer -c "RELOAD;"

原因 3:应用用了 SQL 级 PREPARE(而非协议级)

这是最容易踩的坑max_prepared_statements 只跟踪协议级 预处理语句,不跟踪 SQL 级的。

两种预处理语句的区别

复制代码
┌─────────────────────────────────────────────────┐
│ SQL 级 (PgBouncer 不跟踪, 会报错)              │
│                                                 │
│  应用代码里直接发 SQL:                          │
│    PREPARE stmt1 AS SELECT * FROM users ...    │
│    EXECUTE stmt1(1);                            │
│    DEALLOCATE stmt1;                            │
│                                                 │
│  → PgBouncer 直接转发, 不跟踪                   │
│  → 连接切换后 stmt1 不在新连接上 → 报错         │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ 协议级 (PgBouncer 跟踪, 不报错)                 │
│                                                 │
│  驱动底层用扩展查询协议:                        │
│    Parse 消息 + Bind 消息 + Execute 消息        │
│    (应用代码里看不到 PREPARE 字样)              │
│                                                 │
│  → PgBouncer 跟踪 + 重写 + 透明准备            │
│  → 切换连接后自动在新连接上准备 → 不报错        │
└─────────────────────────────────────────────────┘

检查你的应用用哪种

检查方式 操作
看应用代码 PREPAREEXECUTEDEALLOCATE 关键字
看驱动配置 JDBC 看是否设了 prepareThreshold;psycopg2 看是否设了 prepare=True
抓 PgBouncer 日志 看是否有 PGBOUNCER_ 前缀的内部语句名
bash 复制代码
# 查看日志中是否有 PGBOUNCER_ 前缀 (有的话说明协议级正在被跟踪)
tail -f /var/log/pgbouncer/pgbouncer.log | grep -i "PGBOUNCER_"

# 如果搜不到, 但报错持续, 很可能是 SQL 级 PREPARE

常见驱动用的协议级还是 SQL 级

驱动 默认行为 配置方式
JDBC (Java) 协议级(prepareThreshold=5,执行 5 次后自动切换) 连接串加 prepareThreshold=0 可禁用
psycopg2 (Python) 默认简单查询(不用 prepared) prepare=True 启用协议级
psycopg3 (Python) 协议级 默认支持
node-postgres 协议级 默认支持
Go pgx 协议级 默认支持
PHP PDO 协议级 需 PHP 8.4+ 且 libpq 17
psql 命令行 简单查询 不用 prepared

如果是 SQL 级 PREPARE,解决方案

复制代码
方案 A: 应用改造, 改用协议级 prepared statement (推荐)
方案 B: 应用改造, 把 PREPARE/EXECUTE 改成普通 SQL
方案 C: 切换到 session pooling 模式 (牺牲复用率)

原因 4:max_prepared_statements 值确实配小了

如果版本没问题、功能已启用、应用用的是协议级,但仍报错,可能是值太小

判断逻辑

复制代码
应用有 N 个不同的预处理语句 (唯一查询字符串)
  ↓
max_prepared_statements = M
  ↓
若 N > M → 超过上限的语句被拒绝 → 报错

检查方法

bash 复制代码
# 1. 看当前配置值
psql -p 6432 pgbouncer -c "SHOW CONFIG;" | grep max_prepared

# 2. 估算应用有多少个唯一预处理语句
# 方法 A: 问开发, 统计代码中的 SQL 查询数量
# 方法 B: 在 PG 侧查询 (需 PG 13+)
psql -p 5432 -c "
  SELECT query, calls
  FROM pg_stat_statements
  ORDER BY calls DESC
  LIMIT 500;
"
# 不同的 query 字符串数 ≈ 预处理语句数

# 3. 看 PgBouncer 日志中是否有超限报错
grep -i "max_prepared\|too many\|exceed" /var/log/pgbouncer/pgbouncer.log

调整建议

复制代码
max_prepared_statements = 唯一查询数 × 1.5 (留余量)

示例:
  应用有 150 个不同 SQL → 设 250
  应用有 300 个不同 SQL → 设 500

原因 5:客户端驱动兼容性问题

某些驱动版本与 PgBouncer 的协议级 prepared statement 支持不完全兼容。

已知问题

驱动 问题 解决方案
PHP/PDO 需 PHP 8.4+ 且 libpq 17 升级或设 PDO::ATTR_EMULATE_PREPARES => true
JDBC prepareThreshold 行为导致间歇报错 连接串加 prepareThreshold=0 临时禁用
psycopg2 默认不用协议级 prepare=False(默认)或改用 psycopg3
PgBouncer 1.22.0 已知 bug:设 max_prepared_statements > 0 时客户端频繁断开 升级到 1.22.1+

JDBC 临时禁用方案(快速止血):

复制代码
jdbc:postgresql://host:6432/db?prepareThreshold=0

PHP/PDO 临时方案

php 复制代码
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

三、完整排查流程图

复制代码
报错: prepared statement does not exist
         │
         ▼
┌─────────────────────────┐
│ 步骤 1: 查 PgBouncer 版本 │
│ pgbouncer -V             │
└─────────────┬───────────┘
              │
     版本 < 1.21? ──是──► 升级到 1.21+
              │
              否
              ▼
┌─────────────────────────┐
│ 步骤 2: 查 max_prepared  │
│ _statements 配置值       │
│ SHOW CONFIG              │
└─────────────┬───────────┘
              │
     值 = 0? ──是──► 改为 200, RELOAD
              │
              否
              ▼
┌─────────────────────────┐
│ 步骤 3: 查应用用哪种      │
│ SQL 级还是协议级?        │
└─────────────┬───────────┘
              │
    SQL 级 PREPARE? ──是──► 改用协议级或普通 SQL
              │
              否 (协议级)
              ▼
┌─────────────────────────┐
│ 步骤 4: 查值是否够大      │
│ 统计唯一查询数            │
└─────────────┬───────────┘
              │
    唯一查询数 > 配置值? ──是──► 调大 max_prepared_statements
              │
              否
              ▼
┌─────────────────────────┐
│ 步骤 5: 查驱动兼容性      │
│ PHP/JDBC/psycopg2 版本  │
└─────────────┬───────────┘
              │
    不兼容? ──是──► 升级或禁用服务端 prepared
              │
              否
              ▼
┌─────────────────────────┐
│ 步骤 6: 查看 PgBouncer   │
│ 详细日志定位             │
│ verbose = 2             │
└─────────────────────────┘

四、快速止血方案(不管原因,先止血)

如果生产正在报错,先用这个方案立即止血

方案 A:禁用服务端 prepared statement(最快)

让应用改用客户端模拟预处理,不依赖 PG 服务端 prepared:

驱动 禁用方法
JDBC 连接串加 prepareThreshold=0
PHP PDO PDO::ATTR_EMULATE_PREPARES => true
psycopg2 不设 prepare=True(默认就是禁用)
psycopg3 prepare=False
Go pgx default_query_exec_mode=simple_protocolprefer_simple_protocol=true
node-postgres 不支持禁用,需用其他方案

方案 B:临时切 session 模式(牺牲性能)

ini 复制代码
# pgbouncer.ini
pool_mode = session
bash 复制代码
psql -p 6432 pgbouncer -c "RELOAD;"

方案 C:升级 PgBouncer + 启用跟踪(根治)

bash 复制代码
# 1. 升级
apt install --only-upgrade pgbouncer

# 2. 确认版本 ≥ 1.21
pgbouncer -V

# 3. 配置
# max_prepared_statements = 200  (默认就是 200, 不用改)

# 4. 重启
systemctl restart pgbouncer

五、排查命令汇总(一键执行版)

bash 复制代码
#!/bin/bash
# pgbouncer_prepared_stmt_diagnose.sh
# PgBouncer prepared statement 排查脚本

echo "=== 1. PgBouncer 版本 ==="
pgbouncer -V 2>/dev/null || echo "pgbouncer 命令未找到"

echo ""
echo "=== 2. max_prepared_statements 配置 ==="
psql -p 6432 pgbouncer -c "SHOW CONFIG;" 2>/dev/null | grep -i max_prepared || echo "无法连接控制台"

echo ""
echo "=== 3. PgBouncer 运行状态 ==="
psql -p 6432 pgbouncer -c "SHOW VERSION;" 2>/dev/null
psql -p 6432 pgbouncer -c "SHOW STATE;" 2>/dev/null

echo ""
echo "=== 4. 日志中的 prepared 相关错误 ==="
grep -i "prepared\|PGBOUNCER_\|does not exist" /var/log/pgbouncer/pgbouncer.log 2>/dev/null | tail -20 || echo "无日志或日志路径不对"

echo ""
echo "=== 5. PG 侧的预处理语句数量 (需 pg_stat_statements) ==="
psql -p 5432 -c "
  SELECT count(DISTINCT query) as unique_queries
  FROM pg_stat_statements;
" 2>/dev/null || echo "pg_stat_statements 未安装或无法连接"

六、一句话总结

"prepared statement does not exist" 不一定max_prepared_statements 配小了。按概率排查:①版本 < 1.21(功能不存在)→ ②配置为 0(功能禁用)→ ③应用用 SQL 级 PREPARE(不被跟踪)→ ④值确实小了 → ⑤驱动兼容性 。最常见的前两个原因,一个升级、一个改配置即可解决。快速止血可让应用禁用服务端 prepared(JDBC 加 prepareThreshold=0)。

问题

tcp_keepalive = 1

tcp_keepidle = 60

tcp_keepintvl = 10

tcp_keepcnt = 6

含义是啥

admin_users = dbadmin

stats_users = dbmonitor

这两个用户需要提前在 PG 实例中创建吗

;; ============================================================

;; 预处理语句 (transaction 模式)

;; ============================================================

max_prepared_statements = 200 ;; 跟踪的预处理语句数量

含义是啥, 不是说 事务模式下 预处理语句是不支持的吗,为啥要配置这个? 还是我理解错了

答复

三个问题都很关键,特别是第三个------这是 PgBouncer 最容易混淆的概念。逐个讲透。


一、TCP Keepalive 参数含义

1.1 这组参数管什么

TCP Keepalive 是操作系统层面的 TCP 连接保活机制,用于检测对端是否还活着。PgBouncer 把这些参数应用到它建立的 socket 上。

复制代码
PgBouncer ──TCP连接──► 客户端  (client 侧 keepalive)
PgBouncer ──TCP连接──► PostgreSQL (server 侧 keepalive)

1.2 四个参数的含义

ini 复制代码
tcp_keepalive = 1       # 总开关: 启用 TCP keepalive
tcp_keepidle = 60       # 连接空闲 60 秒后, 开始发送探测包
tcp_keepintvl = 10      # 每次探测间隔 10 秒
tcp_keepcnt = 6         # 连续 6 次探测无响应, 判定连接已死

工作机制图示

复制代码
连接空闲
  │
  ▼  60秒 (tcp_keepidle)
  │  开始发第一个探测包
  ▼
对端响应? ──是──► 重置计时器, 再等 60 秒
  │
  否
  ▼  10秒 (tcp_keepintvl)
  │  发第二个探测包
  ▼
对端响应? ──是──► 重置计时器
  │
  否
  ▼  ... 重复, 共 6 次 (tcp_keepcnt)
  │
  ▼
  6 次都没响应 → 判定连接已死 → 关闭 socket

1.3 各参数详解

参数 含义 你的配置值 效果
tcp_keepalive 是否启用 TCP keepalive 1 启用
tcp_keepidle 连接空闲多久后开始探测 60 秒 60 秒无数据就发探测
tcp_keepintvl 探测包间隔 10 秒 每 10 秒探测一次
tcp_keepcnt 探测失败次数上限 6 次 6 次失败判定死亡

1.4 总死亡检测时间

复制代码
从连接空闲到判定死亡的最长时间:
  = tcp_keepidle + tcp_keepintvl × tcp_keepcnt
  = 60 + 10 × 6
  = 120 秒 (2 分钟)

也就是说:如果对端网络断了,最多 2 分钟内 PgBouncer 就能发现并清理这个死连接

1.5 为什么需要这组参数

场景 没有 keepalive 有 keepalive
客户端机器断电/拔网线 连接"假活", 永远不清理 2 分钟内清理
防火墙/NAT 超时丢连接 连接被偷偷断了, PgBouncer 不知道 探测包保活
PG 机器崩溃 客户端连接卡住 快速发现并重连

1.6 注意事项

ini 复制代码
;; 如果不设这几个参数, PgBouncer 使用系统默认值:
;; Linux 默认: keepidle=7200(2小时), keepintvl=75, keepcnt=9
;; 即默认要 2 小时 + 11 分钟才发现死连接 → 太慢!

所以生产环境建议显式设置这组参数,把检测时间缩短到分钟级。


二、admin_users 和 stats_users 是否需要在 PG 中创建

2.1 结论先行

不需要在 PG 实例中创建。但需要在 PgBouncer 的 auth_file(userlist.txt)中配置。

2.2 认证流程的区别

PgBouncer 有两种连接,认证方式不同:

复制代码
场景 A: 连普通业务库 (db_app_main 等)
  客户端 ──► PgBouncer ──► 用 userlist.txt 验证 ──► 通过 ──► 连到 PG
                                                         PG 再验证一次

场景 B: 连管理控制台 (pgbouncer 虚拟库)
  客户端 ──► PgBouncer ──► 用 userlist.txt 验证 ──► 通过 ──► 进入控制台
                                                    (不会连到 PG!)

关键认知 :连接到 pgbouncer 虚拟库时,请求根本不会转发到 PostgreSQL ,PgBouncer 自己处理。所以 admin_users / stats_users 只需要在 PgBouncer 的 auth_file 中存在即可。

2.3 配置方法

步骤 1:在 userlist.txt 中添加用户
text 复制代码
;; /etc/pgbouncer/userlist.txt

;; 业务用户 (需要连到 PG, PG 中也要创建)
"app_user" "SCRAM-SHA-256$4096:..."

;; 管理用户 (只在 PgBouncer 控制台用, PG 中不需要)
"dbadmin" "SCRAM-SHA-256$4096:..."
"dbmonitor" "SCRAM-SHA-256$4096:..."
步骤 2:在 pgbouncer.ini 中配置
ini 复制代码
[pgbouncer]
admin_users = dbadmin        ;; 有管理权限 (PAUSE/RESUME/RELOAD/KILL 等)
stats_users = dbmonitor      ;; 只有查看权限 (SHOW 命令)

2.4 权限区别

角色 能做什么 不能做什么
admin_users 所有 SHOW 命令 + PAUSE/RESUME/RELOAD/KILL/RECONNECT/SHUTDOWN -
stats_users 所有 SHOW 命令(只读) 不能执行管理命令
bash 复制代码
# dbadmin 可以执行管理命令
psql -p 6432 -U dbadmin pgbouncer -c "RELOAD;"
psql -p 6432 -U dbadmin pgbouncer -c "PAUSE db_main;"

# dbmonitor 只能查看
psql -p 6432 -U dbmonitor pgbouncer -c "SHOW POOLS;"   # ✅
psql -p 6432 -U dbmonitor pgbouncer -c "PAUSE;"        # ❌ 拒绝

2.5 安全建议

text 复制代码
;; userlist.txt 权限
chown pgbouncer:pgbouncer /etc/pgbouncer/userlist.txt
chmod 600 /etc/pgbouncer/userlist.txt
ini 复制代码
;; pgbouncer.ini
;; 生产环境建议监听内网地址, 不要 0.0.0.0
listen_addr = 10.0.0.5

2.6 总结对比表

用户类型 用途 PG 中创建 userlist.txt 中配置 说明
业务用户 连业务库 ✅ 需要 ✅ 需要 PG 和 PgBouncer 都要验证
admin_users 管理控制台 ❌ 不需要 ✅ 需要 只 PgBouncer 验证
stats_users 监控控制台 ❌ 不需要 ✅ 需要 只 PgBouncer 验证

三、max_prepared_statements:这是最容易混淆的概念

3.1 你的困惑是合理的

你看到之前教程说"transaction 模式下 PREPARE 不支持",现在又看到要配 max_prepared_statements,自然困惑。

核心原因:有两种完全不同的"预处理语句",你把它们混为一谈了。

3.2 两种预处理语句的本质区别

复制代码
┌─────────────────────────────────────────────────────────────┐
│  类型 1: SQL 级预处理语句 (SQL-level PREPARE)              │
│                                                             │
│  客户端发 SQL 命令:                                         │
│    PREPARE stmt1 AS SELECT * FROM users WHERE id = $1;     │
│    EXECUTE stmt1(1);                                        │
│    DEALLOCATE stmt1;                                        │
│                                                             │
│  特点: 这是一条普通的 SQL 语句, 在 PG 的某个连接上创建     │
│  transaction 模式: ❌ 不支持 (连接归还后语句丢失)           │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  类型 2: 协议级预处理语句 (Protocol-level prepared stmt)   │
│                                                             │
│  客户端用扩展查询协议 (Extended Query Protocol):           │
│    Parse 消息: "SELECT * FROM users WHERE id = $1"         │
│    Bind 消息: 绑定参数 1                                    │
│    Execute 消息: 执行                                       │
│                                                             │
│  特点: JDBC/Psycopg2/Go 驱动底层用这种方式, 不发 SQL 命令   │
│  transaction 模式: ✅ 支持 (需配 max_prepared_statements)  │
└─────────────────────────────────────────────────────────────┘

3.3 你的混淆点

SQL 级 PREPARE 协议级 prepared statement
怎么发 客户端发 PREPARE ... SQL 文本 客户端驱动用二进制协议消息
谁创建 PG 后端连接 PgBouncer 内部跟踪
transaction 模式 ❌ 不支持 ✅ 支持
配置参数 max_prepared_statements

3.4 max_prepared_statements 的作用

这个参数让 PgBouncer 内部跟踪协议级预处理语句,使得 transaction 模式也能用。

工作机制

复制代码
1. 客户端用扩展协议发了一个预处理语句:
   Parse "SELECT * FROM users WHERE id=$1"
   ↓
2. PgBouncer 内部记录:
   - 查询字符串: "SELECT * FROM users WHERE id=$1"
   - 分配内部名称: PGBOUNCER_1
   ↓
3. PgBouncer 转发给 PG (在当前后端连接上):
   Parse PGBOUNCER_1 "SELECT * FROM users WHERE id=$1"
   ↓
4. 事务结束, 后端连接归还到池
   下个事务可能用不同的后端连接
   ↓
5. 客户端再次执行同一个预处理语句:
   Bind/Execute PGBOUNCER_1
   ↓
6. PgBouncer 发现当前后端连接上没有准备 PGBOUNCER_1
   → 透明地先 Parse 再 Execute
   → 对客户端完全透明

图示

复制代码
客户端          PgBouncer (跟踪)         PG 连接 A    PG 连接 B
  │                │                       │            │
  │──Parse stmt1──►│                       │            │
  │                │──Parse PGBOUNCER_1──►│            │
  │                │   (在连接A上准备)      │            │
  │◄──Ready────────│                       │            │
  │                │                       │            │
  │  (事务结束, 连接 A 归还)                │            │
  │                │                       │            │
  │──Exec stmt1──►│                       │            │
  │                │──当前是连接 B, 没准备──►│            │
  │                │──Parse PGBOUNCER_1──► │            │
  │                │   (在连接B上准备)       │            │
  │                │──Execute─────────────►│            │
  │◄──Result──────│                       │            │

3.5 max_prepared_statements = 200 的含义

ini 复制代码
max_prepared_statements = 200

含义 :PgBouncer 内部最多跟踪 200 个不同的预处理语句(按查询字符串去重)。

  • 不是"每个连接 200 个"
  • 不是"每个客户端 200 个"
  • 全局共享的 200 个唯一查询字符串

配置建议

复制代码
设为应用常用预处理语句数量
  - 小应用: 50~100
  - 中型应用: 200 (默认)
  - 大型应用: 500~1000

如果设得太小,超过上限的预处理语句会被拒绝,客户端报错。

3.6 哪种驱动会用到协议级预处理语句

驱动/语言 是否默认用协议级 说明
JDBC (Java) 是(prepareThreshold=5 默认) 同一 SQL 执行 5 次后自动切换为 prepared
psycopg2 (Python) 否(默认用简单查询) 需显式设置 prepare=True
psycopg3 (Python) 默认支持
node-postgres 默认用扩展协议
Go pq / pgx 默认用扩展协议
PHP PDO 默认用扩展协议
psql 命令行 用简单查询(除非用 \prepare

结论 :大多数现代驱动默认会用协议级预处理语句,所以 transaction 模式下配 max_prepared_statements 是必要的

3.7 验证你的应用是否需要

bash 复制代码
# 启动 PgBouncer 后, 跑业务, 观察预处理语句使用情况
psql -p 6432 pgbouncer -c "SHOW STATS;"

# 或查看 PgBouncer 日志中是否有 PGBOUNCER_ 相关的 Parse
tail -f /var/log/pgbouncer/pgbouncer.log | grep -i "PGBOUNCER_"

3.8 完整对比总结

维度 SQL 级 PREPARE 协议级 prepared statement
客户端怎么发 PREPARE stmt AS ... SQL 文本 驱动用扩展查询协议
transaction 模式 ❌ 不支持 ✅ 支持
需要的配置 max_prepared_statements > 0
PgBouncer 角色 直接转发(不跟踪) 内部跟踪+重写+透明准备
跨连接复用 不能 能(PgBouncer 透明处理)
典型使用者 手动写 PREPARE SQL JDBC/psycopg3/pgx 等驱动

四、一句话总结

你的问题 答案
TCP keepalive 四参数 控制死连接检测:空闲 60 秒开始探测,每 10 秒一次,6 次失败判定死亡,总检测时间 120 秒
admin_users/stats_users 是否在 PG 创建 不需要 ,只在 PgBouncer 的 userlist.txt 中配置即可(控制台请求不转发到 PG)
max_prepared_statements 的作用 让 transaction 模式支持协议级 预处理语句(JDBC 等驱动底层用的),PgBouncer 内部跟踪透明处理。SQL 级 PREPARE 命令才是不支持的,这是两个不同的东西

问题

admin_users = dbadmin

stats_users = dbmonitor

这两个用户不需要提前在 PG 实例中创建

但是需要在 userlist 中添加

可是如果不在 PG 实例中创建这两个用户,怎么知道密码是啥呢?

回答

这个问题问到点子上了。核心困惑在于:没搞清 PgBouncer 有两套认证模型。澄清后你就明白了。


一、关键认知:两种用户,两套认证模型

复制代码
┌─────────────────────────────────────────────────────────────┐
│ 模型 A: 透传认证 (业务用户)                                  │
│                                                             │
│  客户端 ──► PgBouncer 验证 ──► PgBouncer 连到 PG ──► PG 验证 │
│                                                             │
│  密码来源: PG 的 pg_authid 表 (必须和 PG 一致)              │
│  所以: 用户必须在 PG 中存在, 密码必须和 PG 一致              │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 模型 B: PgBouncer 独立认证 (admin/stats 用户)               │
│                                                             │
│  客户端 ──► PgBouncer 验证 ──► 进入控制台 (结束)            │
│                                不连 PG!                     │
│                                                             │
│  密码来源: 你自己在 userlist.txt 中定义                      │
│  所以: 用户不在 PG 中存在, 密码你自己定                      │
└─────────────────────────────────────────────────────────────┘

核心区别

用户类型 密码从哪来 是否要在 PG 中存在
业务用户 (app_user) 从 PG 复制过来,必须一致 ✅ 必须
admin/stats 用户 你自己定义 ❌ 不需要

二、业务用户的密码怎么来(对比理解)

业务用户的密码是从 PG "搬"过来的:

sql 复制代码
-- 在 PG 中创建用户并设密码
CREATE USER app_user PASSWORD 'MyAppPass123!';

-- 查询 PG 生成的 SCRAM 密钥
SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'app_user';
-- 输出:
--  rolname  | rolpassword
--  ---------+----------------------------------------------------
--  app_user | SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey

-- 把这个密钥复制到 userlist.txt
text 复制代码
;; userlist.txt
"app_user" "SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey"

这里密码是PG 生成的,你必须从 PG 复制。


三、admin/stats 用户的密码怎么来(你问的核心)

你自己定 。PgBouncer 是认证方,密码由你在 userlist.txt 中定义,与 PG 无关。

方法 1:明文密码(最简单,推荐起步用)

text 复制代码
;; userlist.txt
"dbadmin"  "MyAdminPass2024!"
"dbmonitor" "MyMonitorPass2024!"

密码就是你写的那个字符串,客户端用它登录即可。

复制代码
psql -p 6432 -U dbadmin pgbouncer
Password for user dbadmin: MyAdminPass2024!    ← 输入你定义的密码

方法 2:MD5 哈希(如果用 MD5 认证)

ini 复制代码
;; pgbouncer.ini
auth_type = md5
text 复制代码
;; userlist.txt
;; MD5 格式: "md5" + md5(password + username)
"dbadmin" "md5a1b2c3d4e5f6..."

生成 MD5 的方法

bash 复制代码
# 假设密码是 MyAdminPass2024!, 用户名是 dbadmin
echo -n "MyAdminPass2024!dbadmin" | md5sum
# 输出: a1b2c3d4e5f6...  (取这个)
# 最终填: "md5" + 上面那串
bash 复制代码
# 一行命令搞定
PASSWORD="MyAdminPass2024!"
USERNAME="dbadmin"
echo "md5$(echo -n "${PASSWORD}${USERNAME}" | md5sum | awk '{print $1}')"
# 输出: md5a1b2c3d4e5f6...

方法 3:SCRAM-SHA-256 哈希(推荐生产用)

ini 复制代码
;; pgbouncer.ini
auth_type = scram-sha-256

SCRAM 不能手算,需要工具生成。三种方式:

方式 3a:临时在 PG 中创建用户,拷贝哈希,再删除(推荐)
sql 复制代码
-- 在 PG 中创建临时用户
CREATE USER tmp_dbadmin PASSWORD 'MyAdminPass2024!';

-- 查询生成的 SCRAM 密钥
SELECT rolpassword FROM pg_authid WHERE rolname = 'tmp_dbadmin';
-- 输出: SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey

-- 删除临时用户 (PG 中不需要它)
DROP USER tmp_dbadmin;
text 复制代码
;; userlist.txt (把用户名改成 dbadmin, 用上面拷贝的哈希)
"dbadmin" "SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey"
方式 3b:用 Python 生成
python 复制代码
# pip install psycopg2  或  pip install psycopg[c]
import psycopg

# 连到任意 PG 实例 (用完即弃)
conn = psycopg.connect("host=localhost port=5432 dbname=postgres user=postgres")
conn.autocommit = True

username = "dbadmin"
password = "MyAdminPass2024!"

# 创建临时用户
conn.execute(f"CREATE USER tmp_user PASSWORD '{password}'")

# 查询 SCRAM 哈希
row = conn.execute(
    "SELECT rolpassword FROM pg_authid WHERE rolname = 'tmp_user'"
).fetchone()
print(f'"{username}" "{row[0]}"')

# 删除临时用户
conn.execute("DROP USER tmp_user")
conn.close()

运行:

bash 复制代码
python3 gen_scram.py
# 输出: "dbadmin" "SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey"
# 直接拷贝到 userlist.txt
方式 3c:用 PgBouncer 自带的命令(1.23+)
bash 复制代码
# PgBouncer 1.23+ 提供了生成 SCRAM 的命令
pgbouncer --scram "MyAdminPass2024!"
# 输出: SCRAM-SHA-256$4096:base64salt$base64storedkey:base64serverkey

四、完整操作流程(从零开始)

场景:为 PgBouncer 创建 admin 和 stats 用户

bash 复制代码
# 1. 确定密码 (你自己定)
ADMIN_PASS="MyAdminPass2024!"
MONITOR_PASS="MyMonitorPass2024!"

# 2. 生成 SCRAM 哈希 (用 PG 临时创建法)
psql -p 5432 -U postgres -c "CREATE USER tmp_gen PASSWORD '${ADMIN_PASS}';"
ADMIN_HASH=$(psql -p 5432 -U postgres -tAc \
  "SELECT rolpassword FROM pg_authid WHERE rolname='tmp_gen'")
psql -p 5432 -U postgres -c "DROP USER tmp_gen;"

psql -p 5432 -U postgres -c "CREATE USER tmp_gen PASSWORD '${MONITOR_PASS}';"
MONITOR_HASH=$(psql -p 5432 -U postgres -tAc \
  "SELECT rolpassword FROM pg_authid WHERE rolname='tmp_gen'")
psql -p 5432 -U postgres -c "DROP USER tmp_gen;"

# 3. 写入 userlist.txt
cat >> /etc/pgbouncer/userlist.txt << EOF
"dbadmin" "${ADMIN_HASH}"
"dbmonitor" "${MONITOR_HASH}"
EOF

# 4. 设置文件权限
chown pgbouncer:pgbouncer /etc/pgbouncer/userlist.txt
chmod 600 /etc/pgbouncer/userlist.txt

# 5. 重载 PgBouncer
psql -p 6432 pgbouncer -c "RELOAD;"

# 6. 验证登录
psql -p 6432 -U dbadmin pgbouncer -c "SHOW POOLS;"
# 输入密码: MyAdminPass2024!

五、用 auth_user / auth_query 能省掉 userlist.txt 吗

不能,这是另一种机制,且只适用于业务用户。

ini 复制代码
;; 用 auth_user 机制, PgBouncer 从 PG 动态查询用户密码
auth_user = pgbouncer_auth          ;; PG 中需创建此用户
auth_query = SELECT rolname, rolpassword FROM pg_authid WHERE rolname = $1
auth_dbname = postgres

工作流程

复制代码
客户端用 app_user 连接
  ↓
PgBouncer 用 auth_user (pgbouncer_auth) 连到 PG
  ↓
执行 auth_query 查询 app_user 的密码哈希
  ↓
用查到的哈希验证客户端

但这只对业务用户有效 ------auth_query 查的是 PG 的 pg_authid 表,而 admin/stats 用户不在 PG 中 ,查不到。所以 admin/stats 用户必须userlist.txt 中手动配置。


六、两种用户的完整配置对比

维度 业务用户 (app_user) admin/stats 用户
PG 中是否存在 ✅ 存在 ❌ 不存在
密码从哪来 PG 的 pg_authid 你自己定义
userlist.txt 中是否配置 ✅ 需要 ✅ 需要
认证流程 PgBouncer 验证 + PG 验证 只 PgBouncer 验证
能否用 auth_user 机制 ✅ 可以 ❌ 不可以
密码修改方式 ALTER USER xxx PASSWORD (PG 中改) + 同步 userlist.txt 只改 userlist.txt

七、一句话总结

admin/stats 用户只在 PgBouncer 中存在,密码是你自己定义的 ------你想设什么密码就设什么。写进 userlist.txt 后 PgBouncer 用它来验证控制台登录。推荐用 SCRAM-SHA-256,生成方法:在 PG 中临时建个用户、查出哈希、删掉用户、把哈希填进 userlist.txt业务用户的密码才需要从 PG 复制,admin/stats 用户的密码完全由你做主

问题: 为什么有了应用层连接池,还需要 PgBouncer?

先搞懂一个类比

想象一家有 40 个包间的酒店,包间就是你的数据库。


场景一:只有应用层连接池(没有 PgBouncer)

假设你有 3 个服务员 (应用实例),每个服务员身上挂着 10 串钥匙(连接池大小=10),每串钥匙能开任何一个包间。

复制代码
服务员A(身上挂10串钥匙)
服务员B(身上挂10串钥匙)  
服务员C(身上挂10串钥匙)

总钥匙数 = 3 × 10 = 30 串钥匙,全部挂在服务员身上。

问题 1:钥匙浪费

服务员A 手里 10 串钥匙,某一时刻只有 2 个客人需要服务,剩下 8 串钥匙白白挂在身上。

服务员B 那边突然来了 8 个客人,但他身上只有 10 串钥匙也快不够用了。

能借吗?不能。 每个服务员各管各的,钥匙不能互相借。

问题 2:钥匙数量随服务员数量线性增长

老板说生意好,再招 7 个服务员,变成 10 个服务员:

复制代码
钥匙总数 = 10 个服务员 × 10 串钥匙 = 100 串钥匙

但实际同时服务的客人可能也就 20-30 个。钥匙严重过剩,但因为没法共享,只能每人发一份。

问题 3:客人走后钥匙不马上还

服务员A 的一个客人点完菜在聊天(会话还没结束),这串钥匙一直被占着,哪怕客人只是坐着喝茶没在用包间。

这就是"会话级共享"------只要会话不断,连接就一直占着。


场景二:加了 PgBouncer

在酒店大堂放一个 钥匙柜(PgBouncer),所有钥匙统一存在柜子里。

复制代码
钥匙柜(PgBouncer):只放 5 串钥匙

服务员A → 需要时去柜子取钥匙 → 用完马上还回去
服务员B → 需要时去柜子取钥匙 → 用完马上还回去
服务员C → 需要时去柜子取钥匙 → 用完马上还回去

好处 1:钥匙可以共享

服务员A 空闲时,他的钥匙在柜子里,服务员B 忙的时候可以取走用。跨服务员共享。

好处 2:用完马上还

客人点完菜(一个事务结束),服务员马上把钥匙还回柜子。下一个人马上能取。这就是"事务级共享"------事务一结束,连接立刻归还。

好处 3:加服务员不增加钥匙

老板又招了 7 个服务员,变成 10 个。但钥匙柜还是 5 串钥匙就够了:

复制代码
10 个服务员 → 共用柜子里的 5 串钥匙 → 完全够用

因为不可能 10 个服务员同时都在服务客人,总有闲着的。钥匙数量不随服务员数量增长。


对应到技术世界

酒店类比 技术对应
服务员 应用实例(Pod/进程)
钥匙串 数据库连接
钥匙挂在服务员身上 应用层连接池(HikariCP/GORM)
钥匙柜 PgBouncer
客人点完菜在聊天 会话空闲但连接还占着
点菜这个动作 一个事务
点完菜马上还钥匙 事务结束立即归还连接

一张图看懂核心区别

没有 PgBouncer:

复制代码
3个实例,每个池10 → PG承受 30 个连接
扩到10个实例 → PG承受 100 个连接  ← 线性增长!

有 PgBouncer:

复制代码
3个实例,每个池10 → PgBouncer后端只用 5 个 → PG承受 5 个连接
扩到10个实例 → PgBouncer后端还是 5 个 → PG承受 5 个连接  ← 不变!

最直白的总结

应用层连接池 :每个实例自己管自己的连接,跨实例没法共享,而且连接占着不还(会话级)。实例越多,数据库压力越大。

PgBouncer :所有实例的连接统一管理,跨实例共享,事务一结束就归还(事务级)。实例再多,数据库压力不变。

一句话:应用层连接池解决的是"单个实例内部复用",PgBouncer 解决的是"所有实例统一复用 + 更细粒度的复用"。

相关推荐
啵啵鱼爱吃小猫咪4 小时前
Franka机械臂安装实时内核
数据库·postgresql
小杍随笔8 小时前
【Rust全栈项目接入UUID主键实战:从选型到PostgreSQL适配(含v7优化)】
开发语言·postgresql·rust
倒流时光三十年1 天前
PostgreSQL to_json 与 row_to_json:妙用与区别详解
数据库·postgresql·json
IvorySQL1 天前
PG 技术日报|2026-07-03
数据库·postgresql·开源
甘露s1 天前
为什么 GIS 系统更青睐 PostgreSQL?
数据库·postgresql
脑子进水养啥鱼?2 天前
PostgreSQL timescaledb
数据库·postgresql
梦想的颜色4 天前
硬核实践:使用 Docker 部署生产级 PostgreSQL
运维·docker·postgresql·docker-compose·docker命令·docker安装数据库
smallyoung4 天前
数据库乐观锁深度解析:MySQL、PostgreSQL 实战 + Spring Boot 集成指南
数据库·mysql·postgresql
秉承初心19 天前
PostgreSQL 数据性能瓶颈突破实战
数据库·postgresql·oracle