文章目录
- [说明: 本文基于 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)
-
- 一、直接结论
- 二、连接增长的实际过程
- 三、池的"生"与"死"
- [四、如果想"预热":用 `min_pool_size`](#四、如果想"预热":用
min_pool_size) - 五、连接回收机制
- 六、验证方法
- 七、一句话总结
- [问题 2](#问题 2)
- [回答 2](#回答 2)
-
- 一、为什么这样做更好
- 二、推荐简化配置
-
- `pgbouncer.ini`
- [PostgreSQL 侧](#PostgreSQL 侧)
- [三、关键点:`max_db_connections` 是真正的安全网](#三、关键点:
max_db_connections是真正的安全网) - [四、通配符 `*` 的用法](#四、通配符
*的用法) - 五、事后如何发现需要单独调的库
- 六、最终对比
- 七、一句话总结
- [问题 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 + 启用跟踪(根治))
- 五、排查命令汇总(一键执行版)
- 六、一句话总结
- [一、核心认知:`max_client_conn` 管的是"便宜连接"](#一、核心认知:
- 问题
- 答复
-
- [一、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中配置)
- [步骤 1:在 `userlist.txt` 中添加用户](#步骤 1:在
- [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 生产环境最佳实践培训教程。
内容应该包括:
- 原理介绍
- 配置介绍
- 注意点(事务模式限制等)
- 运维监控
- 最佳实践
我会基于刚才对话中讨论的实际场景(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_encoding、DateStyle、Timezone 等 |
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 的maxwait和cl_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 的连接建立时机机制。我需要查源码或官方文档确认准确行为,避免臆测。
关键问题:
pool_size=2是预先分配 2 个连接吗?- 还是客户端访问时一次性建立 2 个?
- 还是一个一个按需增长,上限 2 个?
我查一下官方文档和相关源码。
好问题,涉及 PgBouncer 的连接建立时机机制。我先查官方文档和源码确认行为,避免臆测。
查到了明确的答案。结论很清晰:
pool_size=2不是预分配- 不是客户端访问时一次性申请 2 个
- 是一个个按需增长,上限 2 个
关键证据:
min_pool_size=0(默认),完全不维护最小连接数- 连接是逐个按需创建的
- 即使设
min_pool_size > 0,也只有在至少有一个客户端连接到该池时才生效 - 池在完全空闲(无客户端连接)时,所有服务器连接最终都会被关闭
所以答案是:一个个增长,最多到 2 个。
我应该清晰直接地回答,并给出实际演进过程。用户的疑问很具体,我需要:
- 直接给出答案
- 解释增长机制
- 给出实际时间线示例
- 介绍
min_pool_size如果想预热的情况 - 介绍
server_idle_timeout的回收机制
简洁明了地回答。
答案明确:一个个按需增长,最多到 2 个,且空闲会回收。PgBouncer 不会预分配、也不会批量建立。
一、直接结论
| 你的猜测 | 是否正确 |
|---|---|
| 预先分配 2 个连接 | ❌ |
| 客户端访问时一次性申请 2 个 | ❌ |
| 一个个按需增长,上限 2 个 | ✅ |
pool_size 是上限,不是预分配数量。
二、连接增长的实际过程
以 db_tenant_01 配 pool_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仅在以下情况生效:
- 该 database 配置了 forced user(在
[databases]段指定了user=),或- 至少有一个客户端已连接到该池
也就是说,即使配了 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?
这是个很实际的简化配置问题。我应该:
- 明确回答:可以,且推荐这么做
- 给出简化配置
- 解释如何通过 max_db_connections 作为安全网
- 解释事后如何通过观察 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 > 0 且 max_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 跟踪 + 重写 + 透明准备 │
│ → 切换连接后自动在新连接上准备 → 不报错 │
└─────────────────────────────────────────────────┘
检查你的应用用哪种:
| 检查方式 | 操作 |
|---|---|
| 看应用代码 | 搜 PREPARE、EXECUTE、DEALLOCATE 关键字 |
| 看驱动配置 | 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_protocol 或 prefer_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 解决的是"所有实例统一复用 + 更细粒度的复用"。