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
- PostgreSQL HOT Updates Index Pasca - Ďalší PostgreSQL gotcha
- Container Memory Limits - Správne dimenzovanie kontajnerov
Súvisiace články
Redis Memory Fragmentácia: Keď maxmemory Nestačí
Váš Redis má 4GB maxmemory ale RSS ukazuje 6GB. OOM killer zasiahne. Vysvetlím jemalloc fragmentáciu s reprodukciou a tuningom activedefrag.
Index Ktorý Zabil Write Performance: Strata PostgreSQL HOT Updates
Pridanie indexu pre výkon spôsobilo 10x pomalšie zápisy. Kontra-intuitívna príčina: nový index rozbil HOT updaty, meniaci lacné in-place updates na drahé full-row rewrites s masívnym bloatom.
Java OOMKilled So Stabilným Heapom: Native Memory, Direct Buffers a glibc Arenas
Heap metriky vyzerajú dobre, GC je spokojný, ale kontajner stále umiera. Vinník: native memory z direct buffers, JNI a glibc memory allocator fragmentácia.
PostgreSQL Read Replica Konflikty: Prečo sa vaše dotazy rušia
Dotazy na read replikách zlyhávajú s 'canceling statement due to conflict with recovery'. Riešenie závisí od toho, ktorý z 5 typov konfliktov máte - tu je návod ako diagnostikovať a vyriešiť každý z nich.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.