Späť na blog

PostgreSQL OOM by Design: work_mem × Parallel Workers × Plan Nodes

Paralelne query su fajn, kym nezabiju box OOM-om. “PostgreSQL OOMKilled ale work_mem je len 256MB!” Matematika: 256MB × 4 parallel workers × 3 plan nodes = 3GB. To nie je bug—tak fungujú PostgreSQL parallel queries. A zabilo to náš kontajner.

Prostredie: PostgreSQL 15, Kubernetes so 4GB memory limit, analytické queries na 100M+ riadkových tabuľkách

Problém

Klamlivá Konfigurácia

-- Vyzerá rozumne, nie?
SHOW work_mem;
-- 256MB

-- Kontajner má 4GB pamäte
-- PostgreSQL shared_buffers = 1GB
-- Zostávajúce ~3GB by mali stačiť pre work_mem... nie?

-- Potom táto query beží:
EXPLAIN ANALYZE
SELECT customer_id, SUM(amount)
FROM orders
WHERE order_date > '2024-01-01'
GROUP BY customer_id
ORDER BY SUM(amount) DESC;

-- Kontajner: OOMKilled
-- PostgreSQL: terminated
-- Aplikácia: 500 errors všade

Prečo 256MB Bolo 3GB

Rozbor query plánu:

┌─────────────────────────────────────────────────┐
│ Gather Merge (parallel coordinator)             │
│   Workers Planned: 4                            │
│   ├─ Sort (work_mem použité tu)                 │
│   │    └─ Parallel Hash Aggregate               │
│   │         (work_mem použité tu)               │
│   │         └─ Parallel Seq Scan                │
│   │              (work_mem pre bitmap tu)       │
└─────────────────────────────────────────────────┘

Výpočet pamäte:

work_mem per operácia:            256 MB
Plan nodes používajúce work_mem:  × 3 (sort + hash agg + scan bitmap)
Parallel workers:                 × 4
Plus coordinator proces:          × 1.25

Celková možná pamäť:
256 MB × 3 × 4 × 1.25 = 3.84 GB

Limit kontajnera: 4 GB
Shared buffers: 1 GB
Dostupné: 3 GB
Potrebné: 3.84 GB

Výsledok: OOM!

Príčina

Ako work_mem Skutočne Funguje

-- Bežná mylná predstava:
-- "work_mem je celková pamäť ktorú PostgreSQL používa na sorting/hashing"

-- Realita:
-- work_mem je per-operácia, per-worker, per-plan-node

-- Jedna query môže použiť work_mem VIACKRÁT:

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT ...
FROM table1
JOIN table2 ON ...      -- Hash join: work_mem
JOIN table3 ON ...      -- Hash join: work_mem
WHERE complex_condition  -- Bitmap scan: work_mem
GROUP BY ...            -- HashAggregate: work_mem
ORDER BY ...            -- Sort: work_mem
LIMIT 100;

-- Každá z týchto operácií dostane vlastnú work_mem alokáciu
-- A s parallel query, vynásob počtom workers!

Parallel Query Multiplikátor

Bez paralelizmu:
┌────────────────────────────────┐
│ Sort (256MB)                   │
│  └─ HashAggregate (256MB)      │
│       └─ Seq Scan              │
└────────────────────────────────┘
Celkom: ~512MB worst case

S parallel_workers = 4:
┌────────────────────────────────────────────────────┐
│ Gather Merge                                       │
│  ├─ Worker 0: Sort (256MB) + HashAgg (256MB)      │
│  ├─ Worker 1: Sort (256MB) + HashAgg (256MB)      │
│  ├─ Worker 2: Sort (256MB) + HashAgg (256MB)      │
│  └─ Worker 3: Sort (256MB) + HashAgg (256MB)      │
│  └─ Leader:   Sort (256MB) + HashAgg (256MB)      │
└────────────────────────────────────────────────────┘
Celkom: ~2.5GB worst case (a to sú len 2 operácie!)

Diagnostika

Krok 1: Skontroluj Skutočné Využitie Pamäte

-- Pozri využitie pamäte v query plánoch
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT ...;

-- Hľadaj:
-- "Sort Method: external merge  Disk: 1234kB"  -- Spilled na disk
-- "Sort Method: quicksort  Memory: 25kB"       -- Zmestilo sa do pamäte
-- "HashAggregate  Batches: 1  Memory Usage: 2457kB"

-- Batches > 1 znamená že spilled na disk (dobré pre pamäť, zlé pre rýchlosť)

Krok 2: Monitoruj Per-Query Pamäť

-- Zapni tracking memory context (PostgreSQL 14+)
SET log_statement_stats = on;
SET log_parser_stats = on;
SET log_planner_stats = on;
SET log_executor_stats = on;

-- Skontroluj pg_stat_activity pre info o pamäti
SELECT
    pid,
    query,
    pg_size_pretty(pg_backend_memory_contexts.total_bytes)
FROM pg_stat_activity
JOIN pg_backend_memory_contexts ON pg_stat_activity.pid = pg_backend_memory_contexts.pid
WHERE state = 'active';

Krok 3: Skontroluj Parallel Query Nastavenia

-- Aktuálne parallel nastavenia
SHOW max_parallel_workers_per_gather;  -- napr. 4
SHOW max_parallel_workers;             -- napr. 8
SHOW max_worker_processes;             -- napr. 8
SHOW parallel_tuple_cost;
SHOW parallel_setup_cost;
SHOW min_parallel_table_scan_size;

-- Vypočítaj worst case:
-- work_mem × max_parallel_workers_per_gather × (odhadované plan nodes)

Riešenie

Možnosť 1: Limituj Parallel Workers

-- Globálne zníž parallel workers
ALTER SYSTEM SET max_parallel_workers_per_gather = 2;
SELECT pg_reload_conf();

-- Alebo per-query pre problematické queries
SET max_parallel_workers_per_gather = 0;  -- Vypni paralelizmus
SELECT ...;
RESET max_parallel_workers_per_gather;

Možnosť 2: Zníž work_mem

-- Vypočítaj bezpečné work_mem:
-- (Dostupná pamäť) / (max workers × odhadované plan nodes)
-- (3GB) / (4 × 4) = ~192MB

ALTER SYSTEM SET work_mem = '128MB';
SELECT pg_reload_conf();

-- Trade-off: viac disk spilling, pomalšie queries
-- Ale: nebude OOM!

Možnosť 3: Per-Query work_mem Kontrola

-- Pre špecifické drahé queries, zníž work_mem
BEGIN;
SET LOCAL work_mem = '64MB';
SET LOCAL max_parallel_workers_per_gather = 1;

-- Spusti drahú query
SELECT ... ;

COMMIT;
-- Nastavenia sa automaticky resetujú

Možnosť 4: Použi hash_mem_multiplier

-- PostgreSQL 13+: kontroluj hash operation memory separátne
SET hash_mem_multiplier = 1.0;  -- Default je 2.0

-- Toto limituje HashAggregate a Hash Join špecificky
-- work_mem × hash_mem_multiplier = max pre hash operations

Možnosť 5: Aplikačná Úroveň Kontrola

// V aplikácii, obal analytické queries
func runAnalyticalQuery(ctx context.Context, db *sql.DB, query string) error {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // Zníž pamäť pre túto session
    _, err = tx.ExecContext(ctx, "SET LOCAL work_mem = '64MB'")
    if err != nil {
        return err
    }

    _, err = tx.ExecContext(ctx, "SET LOCAL max_parallel_workers_per_gather = 2")
    if err != nil {
        return err
    }

    // Teraz spusti query bezpečne
    rows, err := tx.QueryContext(ctx, query)
    // ...

    return tx.Commit()
}

Monitoring

Prometheus Alerts

groups:
  - name: postgresql-memory
    rules:
      - alert: PostgreSQLHighMemoryQuery
        expr: |
          pg_stat_activity_backend_memory_bytes > 500000000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "PostgreSQL query používa >500MB na {{ $labels.instance }}"

      - alert: ContainerNearOOM
        expr: |
          (container_memory_working_set_bytes / container_spec_memory_limit_bytes) > 0.9
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "PostgreSQL kontajner blízko OOM"

Checklist

## PostgreSQL work_mem OOM

### Symptómy
- [ ] Kontajner OOMKilled počas queries
- [ ] work_mem vyzerá malé ale pamäť exploduje
- [ ] Stáva sa s analytickými/reportovacími queries
- [ ] Parallel queries zapojené

### Diagnostika
- [ ] Skontroluj work_mem nastavenie
- [ ] Skontroluj max_parallel_workers_per_gather
- [ ] EXPLAIN ANALYZE problematické queries
- [ ] Vypočítaj: work_mem × workers × plan_nodes

### Riešenia
- [ ] Zníž work_mem globálne
- [ ] Zníž max_parallel_workers_per_gather
- [ ] Použi SET LOCAL pre špecifické queries
- [ ] Implementuj aplikačné kontroly
- [ ] Nastav hash_mem_multiplier = 1.0

### Prevencia
- [ ] Dimenzuj work_mem konzervatívne
- [ ] Monitoruj container memory usage
- [ ] Testuj analytické queries v stagingu
- [ ] Dokumentuj očakávanú pamäť per query type

Záver

Lekcia: work_mem je per-operácia, per-worker, per-plan-node. To nevinne vyzerajúce 256MB nastavenie môže legitímne konzumovať gigabajty keď parallel query nastúpi.

Fix nie je “daj PostgreSQL viac pamäte”—je to pochopenie multiplikačného faktora a nastavenie konzervatívnych defaultov s per-query overrides keď je to potrebné.


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 OOM by Design: work_mem × Parallel Workers × Plan Nodes". https://www.michal-drozd.com/sk/blog/postgresql-work-mem-parallel-oom/ (Publikované 28. decembra 2024).