7. Connecting & pooling¶
Goal: connect applications to the database the right way (via Services, never pods), and put a PgBouncer pooler in front for efficiency.
The three Services the operator created¶
flowchart LR
app["App: writes"] --> rw["pg-rw"] --> P["Primary"]
appr["App: reads"] --> ro["pg-ro"] --> S1["Standby 1"]
ro --> S2["Standby 2"]
any["Any client"] --> r["pg-r"] --> P
r --> S1
r --> S2
| Service | Points to | Use for |
|---|---|---|
pg-rw |
the current primary | all writes (and reads that must be current) |
pg-ro |
standbys, load-balanced | read-only queries you want to offload |
pg-r |
any instance | read-only where you don't care which |
The crucial idea: connect to a Service, never to a pod IP. When the primary
changes during a failover, the operator re-points pg-rw to the new primary;
your app's connection string never changes.
In-cluster DNS names look like:
pg-rw.production.svc.cluster.local:5432.
Connecting from inside the cluster¶
In an application Deployment, build the connection string from the Secret:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: pg-app-credentials
key: password
- name: DATABASE_URL
value: "postgresql://app_user:$(DB_PASSWORD)@pg-rw.production.svc.cluster.local:5432/app"
- name: DATABASE_RO_URL
value: "postgresql://app_user:$(DB_PASSWORD)@pg-ro.production.svc.cluster.local:5432/app"
Make the app reconnect
In this environment the primary will move (Kured reboots). Use a driver/ pool that retries on connection loss, and keep transactions short.
Connecting from your laptop (debugging)¶
Port-forward a Service, then use psql:
kubectl port-forward svc/pg-rw 5432:5432 -n production
# in another shell:
psql -h localhost -U app_user -d app # password from the Secret
For superuser access (only because we set enableSuperuserAccess: true):
kubectl get secret pg-superuser -n production \
-o jsonpath='{.data.password}' | base64 -d; echo
psql -h localhost -U postgres -d app
Connection pooling with PgBouncer¶
Direct connections are expensive (one OS process each). A Pooler resource
deploys PgBouncer in front of the cluster.
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
name: pg-pooler-rw
namespace: production
spec:
cluster:
name: pg
instances: 2 # (1)!
type: rw # (2)!
pgbouncer:
poolMode: transaction # (3)!
parameters:
max_client_conn: "500"
default_pool_size: "25"
- Two PgBouncer pods for availability.
rwpools to the primary. Use a second pooler withtype: rofor read traffic if needed.- Transaction pooling is the most efficient mode but does not support
session-level features (prepared statements across transactions, advisory
locks,
SET). If your app needs those, usesessionmode instead.
kubectl apply -f pooler.yaml
kubectl get svc -n production | grep pooler # creates pg-pooler-rw service
Point the application at the pooler instead of the cluster:
flowchart LR
a1["app pod"] --> pool
a2["app pod"] --> pool
pool["pg-pooler-rw<br/>(PgBouncer, transaction mode)"] --> rw["pg-rw → Primary"]
Pooling modes at a glance¶
| Mode | A server connection is held for… | Use when |
|---|---|---|
session |
the whole client session | app relies on session state (prepared statements, SET, advisory locks) |
transaction |
one transaction | default for web apps; most efficient |
statement |
one statement | autocommit-only workloads; most aggressive |
What could go wrong¶
- App connects to a pod IP → breaks on every failover. Always use the Service/pooler DNS name.
- Transaction mode + prepared statements → subtle errors. Switch to
sessionmode or disable server-side prepared statements in the driver. - No
pg-superusersecret → you didn't enable superuser access; that's fine, use the app user.
Where to go deeper¶
Next: Scheduled backups.