Späť na blog

Kubernetes rollout bez výpadku DB: Ako zastaviť PostgreSQL connection storm

Je typ vypadku, ked je vsetko zelene a aj tak to hori. Spustis rollout, pody su Ready, a o 30 sekund neskor Postgres krici FATAL: too many connections for role "app".

Presne toto sa mi stalo, ked sme isli z 5 na 20 replik. Aplikacia bola ta ista, len rollout bol iny. To je connection storm: zdrave pody robia normalne veci naraz.

Testované na: k3d 5.x, PostgreSQL 15-16, PgBouncer 1.21+, Node.js 20+ a Java 21 aplikácie.

Čo je Connection Storm

Pri Kubernetes rollout deploymente:

  1. Rolling update - postupne terminuje staré pody a spúšťa nové
  2. Všetky nové pody sa pokúsia naraz pripojiť k databáze
  3. Staré pody ešte držia spojenia (graceful shutdown)
  4. Výsledok: 2x viac spojení než normálne
Normálny stav:     20 podov × 10 connections = 200 connections
Počas rolloutu:    20 starých + 20 nových = 400 connections
PostgreSQL limit:  max_connections = 200

Prečo to fungovalo predtým

S 5 replikami:

  • Normálne: 5 × 10 = 50 connections
  • Rollout: 10 × 10 = 100 connections (pod limitom)

S 20 replikami:

  • Normálne: 20 × 10 = 200 connections (na limite)
  • Rollout: 40 × 10 = 400 connections BOOM

Reprodukovateľný Lab

Setup s k3d

# Vytvor cluster
k3d cluster create connection-storm --agents 3

# Nainštaluj PostgreSQL
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install postgres bitnami/postgresql \
  --set auth.postgresPassword=secret \
  --set primary.resources.limits.memory=512Mi \
  --set primary.configuration="max_connections=50"

Test Aplikácia

# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 20
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
  template:
    spec:
      containers:
      - name: api
        image: your-app:v1
        env:
        - name: DATABASE_URL
          value: "postgres://postgres:secret@postgres:5432/app"
        - name: POOL_SIZE
          value: "5"  # 20 pods × 5 = 100 connections

Spusti Rollout

# Watch connections
watch -n1 "kubectl exec -it postgres-postgresql-0 -- \
  psql -U postgres -c 'SELECT count(*) FROM pg_stat_activity'"

# Trigger rollout
kubectl set image deployment/api api=your-app:v2

Výsledok: Počet connections vyskočí na 150-200 a aplikácia začne failovať.

Benchmark: Rollout bez PgBouncer

Test Scenár

20 replik, pool_size=5 per pod
max_connections=100
Rolling update 25% maxSurge

Meranie

# Počet connections počas rolloutu (každú sekundu)
while true; do
  kubectl exec -it postgres-postgresql-0 -- \
    psql -U postgres -c "SELECT count(*) FROM pg_stat_activity WHERE state != 'idle'" \
    2>/dev/null | grep -E "^\s*[0-9]+"
  sleep 1
done

Výsledky

FázaAktívne connectionsStatus
Pred rolloutom48OK
Rollout start73OK
Peak (t+15s)127FAIL (limit 100)
Stabilizácia48OK

Connection refused errors: 23 za celý rollout

Riešenie 1: PgBouncer

PgBouncer je connection pooler, ktorý:

  • Drží persistentné spojenia s PostgreSQL
  • Multiplexuje aplikačné pripojenia
  • Výrazne redukuje počet reálnych DB spojení

PgBouncer Deployment

# pgbouncer-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: pgbouncer-config
data:
  pgbouncer.ini: |
    [databases]
    app = host=postgres-postgresql port=5432 dbname=app

    [pgbouncer]
    listen_addr = 0.0.0.0
    listen_port = 5432
    auth_type = md5
    auth_file = /etc/pgbouncer/userlist.txt
    pool_mode = transaction
    max_client_conn = 1000
    default_pool_size = 20
    min_pool_size = 5
    reserve_pool_size = 5
    reserve_pool_timeout = 3
    server_lifetime = 3600
    server_idle_timeout = 600
    log_connections = 1
    log_disconnections = 1
    log_pooler_errors = 1

  userlist.txt: |
    "postgres" "secret"
---
# pgbouncer-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgbouncer
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: pgbouncer
        image: edoburu/pgbouncer:1.21.0
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: config
          mountPath: /etc/pgbouncer
        resources:
          limits:
            memory: 128Mi
            cpu: 100m
        livenessProbe:
          tcpSocket:
            port: 5432
          initialDelaySeconds: 10
        readinessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "psql -h localhost -U postgres -c 'SHOW STATS' pgbouncer"
          initialDelaySeconds: 5
      volumes:
      - name: config
        configMap:
          name: pgbouncer-config

Benchmark s PgBouncer

FázaClient connectionsServer connectionsStatus
Pred rolloutom10020OK
Rollout start15025OK
Peak (t+15s)20030OK
Stabilizácia10020OK

Connection refused errors: 0

Riešenie 2: PreStop Hook s Jitter

Aj s PgBouncером môže byť problém ak všetky pody startujú naraz. Riešenie: rozložiť štarty v čase.

PreStop Hook

spec:
  containers:
  - name: api
    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sh
          - -c
          - "sleep 5"  # Daj čas na drain
  terminationGracePeriodSeconds: 30

Startup Jitter

// V aplikácii - random delay pred prvým DB query
const jitter = Math.random() * 5000; // 0-5 sekúnd
await new Promise(resolve => setTimeout(resolve, jitter));
await db.connect();

Alebo v Kubernetes:

spec:
  containers:
  - name: api
    command:
    - /bin/sh
    - -c
    - |
      # Random delay 0-10 sekúnd
      sleep $((RANDOM % 10))
      exec node server.js

Benchmark s Jitter

MetrikaBez jitterS jitter (0-5s)
Peak connections200140
Connection errors30
Rollout duration45s52s

Riešenie 3: Connection Pool Sizing

Výpočet správnej veľkosti poolu

Pravidlo palca:
pool_size = (2 × CPU cores) + disk_spindles

Pre typický cloud workload:
pool_size = 5-10 per pod

Dynamický pool sizing

// Počet connections podľa prostredia
const poolConfig = {
  development: { min: 2, max: 5 },
  staging: { min: 5, max: 10 },
  production: { min: 10, max: 20 }
};

const pool = new Pool({
  ...poolConfig[process.env.NODE_ENV],
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
});

Connection Limiting per Pod

# HPA s custom metrics
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 5
  maxReplicas: 50
  metrics:
  - type: Pods
    pods:
      metric:
        name: db_connections_active
      target:
        type: AverageValue
        averageValue: 8  # Scale keď avg > 8 connections per pod

Production Checklist

## DB-Safe Kubernetes Deployment Checklist

### Connection Management
- [ ] PgBouncer alebo podobný pooler pred databázou
- [ ] Pool mode: transaction (nie session)
- [ ] max_client_conn > (max_pods × pool_size_per_pod × 2)
- [ ] Server-side pool limit: default_pool_size < max_connections / expected_apps

### Deployment Strategy
- [ ] maxSurge: 25% alebo menej pre DB-heavy apps
- [ ] maxUnavailable: 25% (nie 0 - spôsobuje accumulation)
- [ ] terminationGracePeriodSeconds: min 30s
- [ ] preStop hook: sleep 5-10s

### Application
- [ ] Connection pool s min/max limits
- [ ] Connection timeout: max 5s
- [ ] Idle timeout: 30-60s
- [ ] Startup jitter: random 0-5s delay

### Monitoring
- [ ] Alert: pg_stat_activity count > 80% max_connections
- [ ] Alert: pgbouncer waiting clients > 0 sustained
- [ ] Dashboard: connections over time počas rolloutov
- [ ] Metric: connection_acquire_time_seconds

### Testing
- [ ] Load test s produkčným počtom replik
- [ ] Chaos test: rollout počas peak traffic
- [ ] Verify: zero connection errors počas rollout

Monitoring Setup

PostgreSQL Metrics

-- Aktuálne connections per user/database
SELECT usename, datname, count(*)
FROM pg_stat_activity
GROUP BY usename, datname;

-- Waiting queries (connection starvation)
SELECT count(*) FROM pg_stat_activity
WHERE wait_event_type = 'Client';

PgBouncer Metrics

# Pripoj sa na PgBouncer admin
psql -p 6432 -U postgres pgbouncer

# Štatistiky poolov
SHOW POOLS;

# Aktuálni klienti
SHOW CLIENTS;

# Servery (reálne DB connections)
SHOW SERVERS;

Prometheus Queries

# Connection usage %
pg_stat_activity_count / pg_settings_max_connections * 100

# Spike detection
increase(pg_stat_activity_count[1m]) > 50

# PgBouncer waiting clients
pgbouncer_pools_cl_waiting

Alternatívy k PgBouncer

Odyssey

Yandex fork, lepší pre veľké scale:

# Výhody
- Multi-threaded (PgBouncer je single-threaded)
- Lepší pre 10k+ connections
- TLS passthrough

# Nevýhody
- Menej dokumentácie
- Menšia komunita

pgcat

Cloudflare project:

# Výhody
- Rust (memory safe)
- Built-in sharding support
- Prometheus metrics native

# Nevýhody
- Relatívne nový
- Breaking changes medzi verziami

Záver

Connection storm je zákerný problém - všetko funguje až kým nezačneš škálovať. Kľúčové opatrenia:

  1. PgBouncer - nikdy sa nepripájaj priamo k PostgreSQL z aplikačných podov
  2. Transaction pooling - nie session pooling
  3. Jitter - rozlož štarty v čase
  4. Monitoring - sleduj connections počas každého rolloutu
  5. Load testing - testuj s produkčným počtom replik

Investícia do správneho connection managementu sa vráti pri každom deployi.

FAQ

Prečo nestačí zvýšiť max_connections?

PostgreSQL nie je navrhnutý na tisíce aktívnych spojení. Každé spojenie spotrebúva pamäť (~10MB) a context switching degraduje výkon. PgBouncer je efektívnejší.

Môžem použiť connection pooler v aplikácii namiesto PgBouncer?

Aplikačný pool neriešuje problém - každý pod má svoj pool, čo v súčte = rovnaký počet connections. Potrebuješ centralizovaný pooler.

Koľko PgBouncer replik potrebujem?

Pre väčšinu workloadov stačia 2 repliky (HA). PgBouncer je extrémne efektívny - jeden proces zvládne tisíce client connections.

Čo ak používam managed DB (RDS, Cloud SQL)?

Managed databázy majú striktnejšie limity. RDS default je 100-200 connections. Pooler je ešte dôležitejší.


Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "Kubernetes rollout bez výpadku DB: Ako zastaviť PostgreSQL connection storm". https://www.michal-drozd.com/sk/blog/k8s-postgresql-connection-storm/ (Publikované 1. apríla 2025).