#!/usr/bin/env bash # 013-install-pgbouncer - Connection pooling for serverless clients. # # Workload: a handful of TPS, but many (serverless) clients -> many short # connections. PgBouncer fronts PostgreSQL in transaction mode; PostgreSQL # itself keeps max_connections=30 (010). Server connections go over TCP # 127.0.0.1:5432 so the existing pg_hba `host ... scram-sha-256` line applies # unchanged (PgBouncer re-uses the client's SCRAM verifier from userlist.txt). # # Listens on 127.0.0.1:6432 ONLY - remote access is a later migration # (roadmap): flip listen_addr, populate userlist.txt with an app role, open # the firewall. Until then nothing can connect through it; that is expected. # # Populating userlist.txt when the app role exists (SCRAM pass-through): # runuser -u postgres -- psql -tAc \ # "SELECT '\"'||rolname||'\" \"'||rolpassword||'\"' \ # FROM pg_authid WHERE rolname='APP' AND rolpassword LIKE 'SCRAM%'" \ # | sudo tee -a /etc/pgbouncer/userlist.txt && sudo systemctl reload pgbouncer # # up: install package (PGDG/Debian repo), write ini + empty userlist, # enable + restart service, verify it accepts connections # down: stop + disable service, purge package, remove config set -euo pipefail INI=/etc/pgbouncer/pgbouncer.ini USERLIST=/etc/pgbouncer/userlist.txt LOG=/var/log/postgresql/pgbouncer.log write_ini() { install -d -o postgres -g postgres -m 750 /etc/pgbouncer cat > "$INI" <<'EOF' ;; managed by sysmig 013-install-pgbouncer - serverless connection pooler [databases] ;; server side: TCP + scram against stock pg_hba (127.0.0.1/32 line) iwanhae = host=127.0.0.1 port=5432 dbname=iwanhae [pgbouncer] ;; --- client side: localhost only until the remote-access migration --- listen_addr = 127.0.0.1 listen_port = 6432 unix_socket_dir = /var/run/postgresql auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt admin_users = iwanhae ;; --- pooling: many short serverless clients, few real workers --- pool_mode = transaction max_client_conn = 300 default_pool_size = 20 reserve_pool_size = 5 reserve_pool_timeout = 3 ;; named prepared statements in transaction mode (ORMs need this, >=1.22) max_prepared_statements = 200 ignore_startup_parameters = extra_float_digits ;; --- server connection lifecycle --- server_reset_query = DISCARD ALL server_idle_timeout = 60 ;; --- process --- logfile = /var/log/postgresql/pgbouncer.log pidfile = /var/run/postgresql/pgbouncer.pid user = postgres EOF chown postgres:postgres "$INI" chmod 640 "$INI" } case "${1:-}" in up) if ! dpkg -s pgbouncer >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y pgbouncer >/dev/null echo " pgbouncer installed" else echo " pgbouncer already installed" fi write_ini # empty placeholder; populated by the future remote-access migration touch "$USERLIST" chown postgres:postgres "$USERLIST" chmod 640 "$USERLIST" systemctl enable pgbouncer >/dev/null systemctl restart pgbouncer ok=0 for _ in $(seq 1 10); do if pg_isready -h 127.0.0.1 -p 6432 -q 2>/dev/null; then ok=1; break; fi sleep 1 done [[ $ok -eq 1 ]] || { echo "FATAL: pgbouncer not accepting on 127.0.0.1:6432 - check $LOG" >&2; exit 1; } echo " listening on 127.0.0.1:6432 (transaction pool, ready for userlist entries)" ;; status) [[ -f $INI ]] || { echo "DRIFT $INI missing"; exit 1; } systemctl is-active --quiet pgbouncer || { echo "DRIFT pgbouncer service not active"; exit 1; } pg_isready -h 127.0.0.1 -p 6432 -q 2>/dev/null \ || { echo "DRIFT nothing accepting on 127.0.0.1:6432"; exit 1; } grep -q '^pool_mode = transaction' "$INI" || { echo "DRIFT pool_mode not transaction"; exit 1; } grep -q '^listen_addr = 127.0.0.1' "$INI" || { echo "DRIFT listen_addr changed (remote access migration?)"; exit 1; } echo "OK pgbouncer on 127.0.0.1:6432, transaction mode" ;; down) systemctl disable --now pgbouncer 2>/dev/null || true export DEBIAN_FRONTEND=noninteractive apt-get purge -y pgbouncer >/dev/null rm -rf /etc/pgbouncer "$LOG" echo " pgbouncer stopped, purged, config removed" ;; esac