Späť na blog

PostgreSQL Replication Slot Bloat: Ako Jeden Neaktívny Slot Naplnil 500GB Disk

|
| postgresql, replication, wal, disk-bloat, logical-replication, monitoring

Disk usage rastol a vinnik bol jeden zabudnuty slot. “Disk na 95%, databáza bude read-only za 30 minút.” Skontrolujem pg_wal: 400GB WAL súborov. Jeden neaktívny replication slot ich drží 5 dní.

Replication sloty sú nevyhnutné pre logical replication, ale jeden zabudnutý slot môže zhodiť tvoju databázu.

Testované na: PostgreSQL 16.1, logical replication do Debezium/Kafka

Ako Replication Slots Fungujú

WAL Retention

Normálny WAL lifecycle:
1. Transakcia → WAL zapísaný
2. Checkpointer označí WAL ako kompletný
3. Staré WAL súbory vymazané (podľa wal_keep_size)

S replication slotom:
1. Transakcia → WAL zapísaný
2. Slot sleduje "Potrebujem WAL od pozície X"
3. PostgreSQL drží VŠETOK WAL od pozície X
4. Aj keď je slot neaktívny celé dni!

Typy Slotov

-- Physical replication slot (streaming replication)
SELECT pg_create_physical_replication_slot('replica1');

-- Logical replication slot (Debezium, pg_logical, atď.)
SELECT pg_create_logical_replication_slot('debezium', 'pgoutput');

-- Oba bránia WAL cleanup!

Problém

Scenár: Debezium Spadne

Deň 1, 00:00: Debezium normálne konzumuje zmeny
Deň 1, 14:00: Debezium pod crashne, nevšimnuté
Deň 1, 14:00 → Deň 5: Replication slot prestane pokračovať
Deň 5, 03:00: Disk na 95%, alerty sa spustia

Nahromadený WAL: 400GB
Čas do katastrofy: 30 minút

Kontrola Stavu Slotu

-- Zobraz všetky replication sloty
SELECT
    slot_name,
    slot_type,
    database,
    active,
    restart_lsn,
    confirmed_flush_lsn,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag_size,
    age(now(), pg_last_xact_replay_timestamp()) AS lag_time
FROM pg_replication_slots;

-- Príklad výstupu (problémový slot):
-- slot_name  | active | lag_size
-- -----------+--------+---------
-- debezium   | f      | 412 GB   ← Neaktívny, masívny lag!

Prevencia

1. max_slot_wal_keep_size (PostgreSQL 13+)

-- postgresql.conf
-- Maximum WAL držaného slotmi (v MB)
max_slot_wal_keep_size = '50GB'

-- Keď prekročené:
-- - Slot je zneplatnený
-- - WAL je uvoľnený
-- - Consumer musí re-sync (plný snapshot)

2. Monitoring Slot Lag

-- Prometheus query pre slot lag
SELECT
    slot_name,
    pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes
FROM pg_replication_slots;
# Alert rule
groups:
- name: replication
  rules:
  - alert: ReplicationSlotLagHigh
    expr: pg_replication_slot_lag_bytes > 10737418240  # 10GB
    for: 30m
    labels:
      severity: warning
    annotations:
      summary: "Replication slot {{ $labels.slot_name }} lag > 10GB"

  - alert: ReplicationSlotInactive
    expr: pg_replication_slot_active == 0
    for: 1h
    labels:
      severity: critical
    annotations:
      summary: "Replication slot {{ $labels.slot_name }} neaktívny 1h"

3. Slot Timeout (Custom Riešenie)

-- Žiadny built-in slot timeout, ale môžeš implementovať cez cron

-- Vytvor helper funkciu
CREATE OR REPLACE FUNCTION drop_inactive_slots(max_inactive_hours int)
RETURNS TABLE(dropped_slot text) AS $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN
        SELECT slot_name
        FROM pg_replication_slots
        WHERE NOT active
        AND age(now(), pg_last_xact_replay_timestamp()) > make_interval(hours => max_inactive_hours)
    LOOP
        PERFORM pg_drop_replication_slot(r.slot_name);
        dropped_slot := r.slot_name;
        RETURN NEXT;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- Spusti cez pg_cron
SELECT cron.schedule('drop-stale-slots', '0 * * * *',
    $$SELECT drop_inactive_slots(24)$$);

Recovery Playbook

Okamžite: Uvoľni Disk

-- 1. Identifikuj problémový slot
SELECT slot_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;

-- 2. Dropni slot (consumer bude potrebovať plný resync!)
SELECT pg_drop_replication_slot('debezium');

-- 3. Spusti checkpoint na urýchlenie WAL cleanup
CHECKPOINT;

-- 4. Over že WAL sa čistí
-- (počkaj na ďalší checkpoint cyklus)
SELECT count(*), pg_size_pretty(sum(size))
FROM pg_ls_waldir();

Consumer Recovery

Po dropnutí slotu:

1. Debezium/Consumer potrebuje PLNÝ snapshot
2. Toto môže trvať hodiny pre veľké databázy
3. Plánuj na:
   - Zvýšenú záťaž databázy počas snapshotu
   - Dočasný lag dát v downstream systémoch
   - Možné duplicitné spracovanie (idempotencia!)

Debezium Špecifické

# Debezium config pre nový snapshot
{
  "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
  "snapshot.mode": "initial",  # Plný snapshot pri štarte
  "slot.drop.on.stop": "false", # Ponechaj slot pri zastavení (opatrne!)

  # Alternatíva: Čistý reštart
  "snapshot.mode": "initial_only"  # Snapshot, potom stop
}

Vylepšenia Architektúry

1. Separátny Replikačný User

-- Dedikovaný user pre replikáciu
CREATE USER replication_user REPLICATION LOGIN;
GRANT USAGE ON SCHEMA public TO replication_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO replication_user;

-- Monitoruj spojenia tohto usera
SELECT * FROM pg_stat_replication WHERE usename = 'replication_user';

2. Health Check Endpoint

// health_check.go
func checkReplicationSlots(db *sql.DB) error {
    var slotName string
    var lagBytes int64

    rows, err := db.Query(`
        SELECT slot_name, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
        FROM pg_replication_slots
        WHERE NOT active
    `)
    if err != nil {
        return err
    }
    defer rows.Close()

    for rows.Next() {
        rows.Scan(&slotName, &lagBytes)
        if lagBytes > 10*1024*1024*1024 { // 10GB
            return fmt.Errorf("slot %s has %d bytes lag", slotName, lagBytes)
        }
    }
    return nil
}

3. Kubernetes Liveness Probe

# Pre Debezium deployment
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: debezium
        livenessProbe:
          httpGet:
            path: /connectors/postgres-connector/status
            port: 8083
          initialDelaySeconds: 60
          periodSeconds: 30
          failureThreshold: 3

Monitoring Dashboard

Kľúčové Metriky

# Veľkosť WAL adresára
pg_wal_directory_size_bytes

# Slot lag (per slot)
pg_replication_slot_wal_lsn_diff{slot_name=~".*"}

# Počet neaktívnych slotov
count(pg_replication_slot_active == 0)

# WAL generation rate
rate(pg_wal_lsn_diff(pg_current_wal_lsn())[5m])

Grafana Panel

{
  "panels": [
    {
      "title": "Replication Slot Lag",
      "type": "timeseries",
      "targets": [
        {
          "expr": "pg_replication_slot_wal_lsn_diff",
          "legendFormat": "{{ slot_name }}"
        }
      ]
    },
    {
      "title": "WAL Directory Size",
      "type": "stat",
      "targets": [
        {
          "expr": "pg_wal_directory_size_bytes"
        }
      ]
    }
  ]
}

Checklist

## Replication Slot Management

### Prevencia
- [ ] Nastav max_slot_wal_keep_size (PostgreSQL 13+)
- [ ] Alert na slot lag > 10GB
- [ ] Alert na neaktívne sloty > 1 hodina
- [ ] Implementuj automatické cleanup starých slotov

### Monitoring
- [ ] Dashboard so slot lag per slot
- [ ] Sledovanie veľkosti WAL adresára
- [ ] Consumer health checks (Debezium status)

### Recovery Plán
- [ ] Zdokumentuj slot drop procedúru
- [ ] Poznaj resync čas consumera pre plný snapshot
- [ ] Testuj recovery procedúru v stagingu

### Consumer Setup
- [ ] Liveness probes pre Debezium/consumers
- [ ] Auto-restart pri zlyhaní
- [ ] Alerting na zlyhania consumera

Záver

Replication sloty sú tichí zabijaci:

  1. Jeden neaktívny slot môže naplniť tvoj disk
  2. Nastav max_slot_wal_keep_size ako safety limit
  3. Alert na neaktívne sloty do 1 hodiny
  4. Maj recovery plán - consumer bude potrebovať plný resync

Slot na ktorý si zabudol je ten čo zhodí produkciu.


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. "PostgreSQL Replication Slot Bloat: Ako Jeden Neaktívny Slot Naplnil 500GB Disk". https://www.michal-drozd.com/sk/blog/postgresql-replication-slot-bloat/ (Publikované 8. júna 2025).