PgBouncer in action

2021-12-14 08:26:48 浏览数 (1)

原本我是 MySQL 的忠实粉丝,后来命运使然,接手了一个 PostgreSQL 项目,一边用一边学,遇到了不少问题,本文说说其中的连接池问题。

有人曾经问我「为什么 PostgreSQL 没有 MySQL 流行呢?」,我说是因为 PostgreSQL 没有服侍好 PHP。当然话有戏谑的成分在,不过仔细想想也不无道理,开发那些简单的增删改查程序,PHP 和 MySQL 是绝配,而 PHP 和 PostgreSQL 则明显不搭,其中最重要的一点是:PostgreSQL 连接是进程级的,而不是像 MySQL 那样是线程级的,也就是说,当一个请求过来时,PostgreSQL 需要 fork 出一个进程!

于是乎,当你使用 PHP 和 PostgreSQL 的双象组合时,数据库连接池是终将面对的一个问题,目前主流的解决方案有 Pgpool 和 PgBouncer,选择哪个好呢?不妨从它们各自的定位来判断,Pgpool 是一个整体解决方案,它不仅实现了连接池,还实现了负载均衡等等高级功能,而 PgBouncer 则仅仅专注于连接池。所以说,如果你除了连接池还需要负载均衡等功能,那么 Pgpool 通常是不错的选择,如果你只想要连接池功能,那么就和我一样使用小而美的 PgBouncer 吧。

在安装前需要考虑的到底在哪安装 PgBouncer:Web 服务器还是 DB 服务器?通常安装在 Web 服务器上是一个更好的选择,因为这样会把连接造成的延迟最小化;不过如果你有很多的 Web 服务器,如果挨个安装 PgBouncer 的话,很明显不利于集中管理,此时安装在 DB 服务器上是更明智的选择;还有些时候,两者都安装也不错。

实际安装很简单,以从源码安装为例,不过需要注意先安装好 libevent-devel 库:

代码语言:javascript复制
shell> yum install libevent-devel
shell> tar zxvf pgbouncer-1.6.1.tar.gz
shell> cd pgbouncer-1.6.1
shell> ./configure
shell> make
shell> make install

安装完毕后注意看,会告诉你稍后会用到的关键文件的安装路径:

代码语言:javascript复制
INSTALL  pgbouncer /usr/local/bin
INSTALL  etc/pgbouncer.ini /usr/local/share/doc/pgbouncer
INSTALL  etc/userlist.txt /usr/local/share/doc/pgbouncer

一个比较简单的可运行配置(pgbouncer.ini)大概如下所示:

代码语言:javascript复制
[databases]

* = host=... port=... user=... password=...

[pgbouncer]

logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid

listen_addr = 127.0.0.1
listen_port = 6432

auth_type = md5
auth_file = /usr/local/share/doc/pgbouncer/userlist.txt

pool_mode = session
server_reset_query = DISCARD ALL

max_client_conn = 100
default_pool_size = 20

log_connections = 0
log_disconnections = 0

在 databases 里配置的是数据库的信息,星号表示匹配所有数据库,其它无需多言。在 pgbouncer 里配置的信息需要着重解释,下面让我们挑重点说明一下。

  • listen_addr 和 listen_port:监听的地址和端口,listen_addr 如果设置为星号表示监听所有地址,不过我强烈建议你不要这样做,因为服务一旦暴露在外网之上,便再无安全可言,相对安全的做法是只监听本地或内网地址。
  • auth_type 和 auth_file:认证的类型及文件,通常 auth_type 使用 md5 的方式,至于 auth_file 可以通过源代码里提供的 mkauth.py 脚本来生成。
  • pool_mode 和 server_reset_query:连接池有三种模式,当设置为 session 模式的时候,在会话结束的时候连接会放回连接池;当设置为 transaction 的时候,在结束一个事务的时候连接会放回连接池;当设置为 statement 的时候,在每条查询完成的时候连接会放回连接池。可见 session 的方式最安全,但是连接的利用率相对比较低;statement 的方式最激进,但是连接的利用率相对比较高;至于 transaction 则介于二者之间,当然具体细节还有一些诧异,如果你不确定就用 session 模式。当使用 session 模式的时候,记得配置 server_reset_query 初始化连接环境。
  • max_client_conn 和 default_pool_size:它们配置的都是连接数,max_client_conn 配置的是全局的连接数,default_pool_size 配置的是单个池的连接数。
  • log_connections 和 log_disconnections:缺省配置下,系统会记录每一次连接和关闭日志,对一个繁忙的服务器来说,干扰太大了,所以建议关闭。

我在实际使用的过程中,遇到一个案例,一并说说,具体表现:访问突发高峰,系统高负载,接着报错:「ERROR:  no more connections allowed (max_client_conn)」,然后就一直缓不过来,访问都没有了也缓不过来,此时「show clients」能看到 PgBouncer 还保持着大量连接没有释放,所以我怀疑是某些 timeout 设置不当导致的,查了一下:

;; Dangerous. Server connection is closed if query does not return ;; in this time. Should be used to survive network problems, ;; _not_ as statement_timeout. (default: 0) ;query_timeout = 0

;; Dangerous. Client connection is closed if the query is not assigned ;; to a server in this time. Should be used to limit the number of queued ;; queries in case of a database or network failure. (default: 0) ;query_wait_timeout = 0

;; Dangerous. Client connection is closed if no activity in this time. ;; Should be used to survive network problems. (default: 0) ;client_idle_timeout = 0

虽然配置文件提示着 Dangerous,但是我设置了合理的 timeout 后,问题解决。为什么会这样?我们不妨设想一下:一个请求到达了连接池,然后关闭连接,但是因为某些原因,比如说网络故障或者服务器本身原因,导致连接池没有收到关闭,此时如果没有设置一个合理的超时时间的话,那么连接池将永远不会释放这个连接。

有意思的是,在新版 1.7 的 Changelog 里,我们能看到一个配置的变化:

Set query_wait_timeout to 120s by default. Current default (0) causes infinite queueing, which is not useful. That means if client has pending query and has not been assigned to server connection, the client connection will be dropped.

此外,还有一些参数也需要注意,比如 listen_backlog,表示连接池被沾满后系统还能 hold 住多少个连接,缺省 128,如果你的访问量比较大,那么加大它。

0 人点赞