diff --git a/README.md b/README.md index 7c810fd..c20cd20 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,22 @@ For a private repo you need an auth strategy: | 006 | install-postgres | PG 18 via PGDG repo, cluster on the disk | | 007 | postgres-admin-role | iwanhae OS user -> PG superuser (peer auth) | | 008 | create-db-iwanhae | database `iwanhae` owned by admin role | +| 010 | tune-postgres | 1GB stability: conns=30, cache hint, timeouts, temp_file_limit | +| 011 | postgres-observability | pg_stat_statements, slow-log 500ms, io timing | +| 012 | tune-cpu | parallel caps + jit=off for shared 2 vCPU | +| 013 | install-pgbouncer | :6432 transaction pooling, localhost-only | + +## PgBouncer notes (013) + +Serverless clients (many connections, few TPS) connect to PgBouncer +(`127.0.0.1:6432`, transaction mode) which opens at most ~25 real +PostgreSQL backends - matching `max_connections=30` from 010 with room +for admin sessions. Remote access is deliberately not opened yet; when +it is: create an app role with a SCRAM password, export its verifier into +`/etc/pgbouncer/userlist.txt` (snippet in 013's header), flip +`listen_addr` and open the firewall. ## Roadmap -- `010-tune-postgres` - 1GB tuning (shared_buffers=128MB, work_mem=4MB, - max_connections=30, ...) +- `015-remote-access` - app role + SCRAM credentials, pgbouncer + `listen_addr` + userlist.txt, pg_hba for the app CIDR, TLS, firewall diff --git a/migrations/013-install-pgbouncer.sh b/migrations/013-install-pgbouncer.sh new file mode 100644 index 0000000..c3b1415 --- /dev/null +++ b/migrations/013-install-pgbouncer.sh @@ -0,0 +1,112 @@ +#!/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