小Cの已经记不起来的博客

用 Docker 搭建连接池的完整流程

上周线上 PostgreSQL 连接数突然飙到 198(默认 max_connections 是 200),三个微服务同时重启,新连接全拿不到,日志里清一色的 FATAL: too many connections for role。排查了一圈,发现有个定时任务每次跑完没释放连接,但短期先止血肯定不能等代码修,得把连接池架起来。

本来想直接在宿主机装 PgBouncer,但转念一想,这玩意儿以后迁移或者回滚都不方便,还是丢 Docker 里跑吧,反正配置也就一个 ini 文件,映射出来就行。

拉镜像并跑起来

直接 docker pull docker.1ms.run/pgbouncer/pgbouncer:latest,我这里又用了加速域名,拉的时候报 404 或者超时的话把 docker.1ms.run/ 删了试原生源,不保证你用的时候这域名还活着。

然后一条命令先跑起来看看能不能正常启动:

docker run -d --name pgbouncer \
  -v /opt/pgbouncer:/etc/pgbouncer \
  -p 6432:6432 \
  -e PGHOST=pg.internal.host \
  -e PGPORT=5432 \
  -e PGUSER=pgbouncer_svc \
  -e PGPASSWORD=your_password_here \
  docker.1ms.run/pgbouncer/pgbouncer:latest

-d 后台跑,--name 方便后面 docker logs pgbouncer 看日志。注意那个 PGHOST,如果你的 Postgres 也在 Docker 网络里,填容器名或者 docker inspect 出来的内网 IP 就行,别填 localhost,容器里的 localhost 是它自己。

写 pgbouncer.ini

第一次 docker exec -it pgbouncer /bin/sh 进去,ls /etc/pgbouncer/ 会发现配置文件是空的或者只有默认模板,得自己写。回到宿主机编辑 /opt/pgbouncer/pgbouncer.ini

[databases]
myapp_db = host=pg.internal.host port=5432 dbname=myapp_db

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 25
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 5
server_lifetime = 3600
server_idle_timeout = 600
log_connections = 1
log_disconnections = 1

重点说几个参数。pool_mode = transaction 是我最常用的,一个事务结束就归还连接给池子,适合绝大多数 REST 接口。如果你的应用里有跨事务持有连接的行为(比如某些 ORM 的 lazy loading 或者长连接 session),那得用 session 模式,但连接复用率会降不少。default_pool_size = 25 意思是每个后端数据库最多开 25 条物理连接,2000 个客户端来请求也给你排队复用,这就是连接池的意义。

auth_type = md5 对应的用户密码文件是 userlist.txt,格式长这样:

"pgbouncer_svc" "md5hash_from_psql"
"myapp_user" "md5password_here"

生成 md5hash 的话去 Postgres 里跑 SELECT usename, passwd FROM pg_shadow WHERE usename = 'pgbouncer_svc'; 拿到 md5 开头的字符串直接贴进去。

让应用连池子而不是直连

改完配置,docker restart pgbouncer 重启容器。然后应用侧的连接串改一下:

# 改之前
postgresql://myapp_user:pass@pg.internal.host:5432/myapp_db

# 改之后
postgresql://myapp_user:pass@pgbouncer-host:6432/myapp_db

端口从 5432 换成 6432,host 换成 PgBouncer 容器所在主机或者 Docker 网络里的服务名。如果应用也在 Docker 里,用 docker network connect 把 pgbouncer 和应用容器放同一个网络,直接用服务名 pgbouncer 就行。

验证有没有生效

连接池到底有没有在复用连接,去 Postgres 那边看一眼:

SELECT state, count(*) FROM pg_stat_activity WHERE application_name = 'pgbouncer' GROUP BY state;

如果 idle 状态的数量一直在 25 上下浮动而不是持续增长到 200,说明池子在正常工作。也可以在 PgBouncer 的 6066 端口看统计(需要在 ini 里开 admin_db = pgbouncer),用 psql -h localhost -p 6066 pgbouncer 进去跑 SHOW POOLS; 能看到每个数据库池的 cl_activesv_activesv_idle,直观得很。

踩过的坑

第一个坑是 reserve_pool_timeout = 5,意思是连接不够的时候等 5 秒再给备用池里的连接。之前我设了 30 秒,结果有个慢查询把池子占满了,其他请求全部排队等 30 秒才拿到连接,接口直接超时。改回 5 秒之后体感好了很多。

第二个坑是 server_lifetime。设了 3600 表示物理连接活一小时就回收,本来是好意防止连接泄漏,但有个服务用了 SET 语句设了 work_mem,回收之后新连接不会继承这些 session 级别的参数,导致部分查询 OOM。如果你的应用有这类 session 变量设置,要么设成 ignore_startup_parameters = extra_float_digits 之类的白名单管理好,要么干脆把 server_lifetime 设大点或者用 session 模式。

第三个,别在 auth_file 里留 admin_users 为空。默认情况下 PgBouncer 的 admin 端口(6066)是开放的,如果 auth_typetrust 或者 admin 用户没设,谁都可以在那上面执行 RECONNECTPAUSE 这些命令,生产环境记得至少加个密码或者用防火墙封了 6066 端口。

整体来说这套东西不复杂,一个容器一个 ini 文件一个 userlist,十分钟能跑起来。但参数调得对不对,还是得看自己业务的连接行为, transaction 模式虽然省连接,但对某些带游标或者 advisory lock 的场景不兼容,上线前最好先在日常环境压一把。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放