PostgreSQL HugePages aktivieren — Leitfaden

Wie du PostgreSQL mit HugePages 5-15 % schneller machst: shared_buffers, vm.nr_hugepages, THP — komplett mit Rollback-Plan.

Status des Servers (Stand 2026-04-14): Linux mit 125 GB RAM,
Postgres 18 mit shared_buffers = 256 MB, effective_cache_size = 32 GB,
huge_pages = try. HugePages sind praktisch nicht konfiguriert
(HugePages_Total = 4 → 8 MB). Transparent HugePages (THP) aktiv mit
4,5 GB allokiert für anonyme Pages.

TL;DR

PostgreSQL profitiert deutlich von Explicit HugePages — typisch
5-15 % Speedup bei Queries, die viel shared memory berühren. Bei deiner
aktuellen Konfiguration (256 MB shared_buffers auf einem 125-GB-Server) ist
der Hebel sogar doppelt: **erst shared_buffers erhöhen, dann HugePages
aktivieren**. Ohne den ersten Schritt bringen HugePages kaum etwas.


1. Was sind HugePages?

Dein Linux-Kernel organisiert RAM in kleinen Einheiten — den sogenannten
Pages. Standard-Größe ist 4 KB. Jede einzelne Page wird über eine
Adress-Übersetzungstabelle (den "TLB" = Translation Lookaside Buffer) auf
physischen Speicher gemappt. Der TLB hat aber nur begrenzten Platz (typisch
64-1024 Einträge je CPU-Core).

HugePages sind größere Pages (2 MB oder 1 GB statt 4 KB). Der Effekt:

  • Weniger TLB-Einträge für die gleiche Menge RAM
  • Weniger TLB-Misses
  • Schnellere Adress-Übersetzung bei großen Memory-Regionen

Postgres' shared_buffers ist exakt so eine **große, konstant genutzte
Memory-Region** — perfekter Kandidat für HugePages.

Grafik: 4 KB Pages vs. 2 MB HugePages

TLB-Hits bei 2 MB Memory-Zugriff Standard 4 KB Pages 512 Pages — 512 TLB-Einträge nötig ≈ 512 TLB-Lookups 2 MB HugePages 1 Page — 1 TLB-Eintrag 2 MB ≈ 1 TLB-Lookup ~500× weniger TLB-Lookups pro 2 MB Memory-Zugriff → deutlich weniger CPU-Overhead

2. Warum PostgreSQL so stark profitiert

Postgres legt shared_buffers (den DB-internen Cache für Pages) in
shared memory ab — eine große, konstant genutzte Region. Jede SELECT,
INSERT oder UPDATE-Operation greift auf diese Region zu. Ohne HugePages
muss der Kernel pro Zugriff TLB-Einträge laden, was CPU-Zyklen kostet.

Messbarer Effekt in der Praxis:

Szenario Ohne HugePages Mit HugePages
Random-OLTP (viele kleine Queries) Baseline +5 %
Analytische Joins (große CTEs) Baseline +10 bis 15 %
Bulk-Insert Baseline +3 bis 5 %

Besonders CompositeOptimizationV2 profitiert: Die Queries scannen
Millionen Zeilen in recommendation_history und stock_prices — genau
das Szenario mit großen CTEs und vielen shared_buffer-Hits.


3. Aktueller Status prüfen

3.1 Kernel-Seite

grep -i huge /proc/meminfo

Deutung:

  • HugePages_Total → Anzahl Explicit HugePages (konfigurierbar via vm.nr_hugepages)
  • HugePages_Free → davon frei
  • HugePages_Rsvd → reserviert (z. B. von Postgres)
  • AnonHugePages → Transparent HugePages (automatisch vom Kernel)
  • Hugepagesize → üblicherweise 2048 kB (2 MB)

Wenn HugePages_Total = 0 oder sehr klein: Postgres nutzt keine Explicit HugePages.

3.2 Postgres-Seite

SHOW huge_pages;          -- 'try' | 'on' | 'off'
SHOW shared_buffers;      -- typisch 25 % RAM bei dedizierten DB-Servern
SHOW effective_cache_size;
SHOW max_connections;

huge_pages = try (Default) bedeutet: **PG versucht HugePages zu nutzen,
fällt aber lautlos auf 4 KB Pages zurück wenn keine da sind**. Perfekter
Modus für "probieren schadet nicht" — aber man merkt dann nicht dass der
Effekt fehlt.

3.3 THP-Status

cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag

Output typisch: always never. Die Klammern zeigen den aktiven
Modus. Für Datenbank-Server ist never empfohlen — siehe Abschnitt 8.


4. Entscheidung: Was einstellen?

Schritt A — shared_buffers erhöhen (erst das!)

Deine aktuellen 256 MB sind für einen 125-GB-Server deutlich zu niedrig.
Postgres-Faustregeln:

Server-Rolle shared_buffers
Dedizierter DB-Server 25 % RAM
Server mit anderen großen Services (wie dein Setup mit mariadb, amavi, …) 10-15 % RAM
Shared Hosting 5-10 % RAM

Bei deinem Setup (125 GB RAM, aber auch mariadb + Mail-Stack) empfehle ich
shared_buffers = 16 GB als vernünftigen Mittelweg:

  • Genug für großzügigen Cache der recommendation_history + stock_prices
  • Lässt 100+ GB für OS-Filesystem-Cache und andere Services

Wichtig: shared_buffers darf nicht mehr als dein verfügbares RAM sein
— und 125 GB sind komfortabel. Aber nie höher als effective_cache_size.

Schritt B — HugePages passend berechnen

Formel:

HugePages_nötig = (shared_buffers + Overhead) / 2 MB

Overhead berücksichtigt PG-Metadaten (WAL-Buffer, shared_preload_libraries,
etc.). Faustregel: +10 %.

Rechnung für shared_buffers = 16 GB:

16 GB / 2 MB = 8192 Pages
+ 10 % Puffer = 9011 → aufgerundet 9100

Für andere Werte:

shared_buffers vm.nr_hugepages (mit Puffer)
4 GB 2250
8 GB 4500
16 GB 9100
32 GB 18000

Schritt C — huge_pages = on statt try

Empfehlung: nach erfolgreichem Testlauf von try auf on wechseln.
Vorteil: PG startet nicht mehr, wenn HugePages fehlen — du bemerkst
Konfigurations-Regressionen sofort statt stiller Degradation.


5. Durchführung (Schritt für Schritt)

⚠️ Wartungsfenster nötig — PG muss neu gestartet werden. Alle Queries
brechen ab, laufende Backtests/Batch-Jobs ebenfalls. Plane ein Zeitfenster
von 10-15 Min.

5.1 Vorbereitung

Baseline-Messung — vergleich später:

SELECT pid, query_start, state, substring(query, 1, 80) AS q
FROM pg_stat_activity
WHERE state = 'active' AND backend_type = 'client backend'
ORDER BY query_start;

Vorhandene Swap-/Memory-Auslastung notieren:

free -h
grep -i huge /proc/meminfo

5.2 Postgres-Konfiguration

In /etc/postgresql/18/main/postgresql.conf:

# Memory
# Vorher: shared_buffers = 256MB
shared_buffers = 16GB

# Vorher: huge_pages = try
huge_pages = on

# Optional: Effective-Planner-Settings anpassen
# effective_cache_size = 64GB   (25-50 % RAM, wenn Server stark Postgres-zentriert)

# Parallel-Worker — auf 16 CPU-Cores massiv unterkonfiguriert per Default!
# Vorher: max_worker_processes = 8
max_worker_processes = 32
# Vorher: max_parallel_workers = 12
max_parallel_workers = 16
# max_parallel_workers_per_gather = 4 (bleibt)

Hintergrund zu den Parallel-Workern: max_worker_processes ist der harte
Cap fuer ALLE Background-Worker (parallel queries, autovacuum, logical
replication, etc.). PG-Default ist 8 — auf einem 16-Core-Server viel zu
niedrig. Wenn 8 parallele Sessions jeweils 4 parallel workers wollen,
braucht der Pool 32 Slots + ~3 fuer Autovacuum = 35. Ohne die Erhoehung
laufen V2-Queries effektiv seriell statt mit 4x Parallelitaet.
Gewinn-Schaetzung: Query-Zeit pro CIK faellt von ~1.5 min auf ~20-40 Sek
(Faktor 2-3 Speedup).

Noch nicht neustarten — erst Kernel vorbereiten.

5.3 Kernel-Seite: HugePages allokieren

# Aktuell allokieren (läuft sofort, nicht persistent)
sudo sysctl vm.nr_hugepages=9100

# Verifizieren
grep -i huge /proc/meminfo
#   HugePages_Total:    9100
#   HugePages_Free:     9100
#   HugePages_Rsvd:        0

Wichtig: Wenn das System gerade stark Memory-Fragmentierung hat, kann
der Kernel weniger als die angeforderte Anzahl allokieren. Dann:

# RAM entfragmentieren via Drop-Caches
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
sudo sysctl vm.nr_hugepages=9100   # erneut versuchen

Wenn immer noch nicht genug → System erstmalig neu starten (sauberer RAM)
und dann erneut allokieren.

5.4 Transparent HugePages (THP) deaktivieren

THP und Explicit HugePages können nebeneinander laufen, aber für
DB-Server wird THP abgeraten:

# Live deaktivieren
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag

Persistent via GRUB (Debian/Ubuntu):

sudo nano /etc/default/grub
# Ergänze transparent_hugepage=never in GRUB_CMDLINE_LINUX_DEFAULT:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash transparent_hugepage=never"

sudo update-grub
# Reboot erst beim nächsten Maintenance-Fenster

Alternativ via systemd-Unit (wirkt beim nächsten Reboot):

sudo tee /etc/systemd/system/disable-thp.service > /dev/null <<'EOF'

Description=Disable Transparent Huge Pages
After=sysinit.target local-fs.target


Type=oneshot
ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled"
ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/defrag"


WantedBy=basic.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable disable-thp.service
sudo systemctl start disable-thp.service

5.5 Persistent machen (sysctl)

echo 'vm.nr_hugepages = 9100' | sudo tee -a /etc/sysctl.d/99-postgres-hugepages.conf
echo 'vm.swappiness = 10'      | sudo tee -a /etc/sysctl.d/99-postgres-hugepages.conf

# Aktivieren ohne Reboot
sudo sysctl --system

5.6 Postgres neu starten

sudo systemctl restart postgresql@18-main

# Status prüfen
sudo systemctl status postgresql@18-main
sudo tail -50 /var/log/postgresql/postgresql-18-main.log

Achte im Log auf Zeilen wie:

LOG:  database system is ready to accept connections

Bei Fehlern → siehe Abschnitt 9 (Troubleshooting).


6. Verifikation

6.1 HugePages sind reserviert

grep -i huge /proc/meminfo

Erwartung:

HugePages_Total:    9100
HugePages_Free:      900       ← <<< 9100, d.h. PG hat sie reserviert
HugePages_Rsvd:     8200       ← PG's Reservierung
AnonHugePages:      150000 kB  ← THP (sollte niedrig sein nach Deaktivierung)

Wenn HugePages_Rsvd deutlich > 0 → Postgres nutzt HugePages erfolgreich.

6.2 Postgres-Log

sudo grep -i huge /var/log/postgresql/postgresql-18-main.log | tail -5

Erwartung: Keine Warnung à la "could not map anonymous shared memory:
Cannot allocate memory". Wenn PG mit huge_pages = on nicht starten kann,
steht das hier.

6.3 Performance-Smoketest

Gleiche Query vor/nach messen (z. B. aus deinem V2-Backtest-Kontext):

EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*) FROM market.recommendation_history
WHERE calculated_date >= CURRENT_DATE - 30;

Interessant: shared hit=xxx bleibt hoch (gut!), Execution Time sollte
spürbar niedriger sein.


7. Rollback

Wenn etwas schiefgeht, zurück zum Ursprungszustand:

# 1. postgresql.conf zurück auf huge_pages = try, shared_buffers = 256MB
sudo nano /etc/postgresql/18/main/postgresql.conf

# 2. Kernel-HugePages freigeben
sudo sysctl vm.nr_hugepages=0

# 3. sysctl-Custom-File entfernen
sudo rm /etc/sysctl.d/99-postgres-hugepages.conf
sudo sysctl --system

# 4. THP wieder aktivieren (falls gewünscht)
echo always | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

# 5. Postgres neu starten
sudo systemctl restart postgresql@18-main

8. Transparent HugePages (THP) — warum ausschalten?

THP ist nicht das Gleiche wie Explicit HugePages:

Feature Explicit HugePages Transparent HugePages (THP)
Allokation Vom Admin fest reserviert Automatisch vom Kernel
Für shared memory (PG) ✅ Ja ❌ Nein (nur anonyme Pages)
Vorhersehbar ✅ Ja ❌ Kann plötzlich zurückfallen
Stall-Risiko Keine Gelegentliche mehrhundert-ms-Stalls beim Defrag
Empfehlung für DB ✅ Aktivieren ⚠️ Deaktivieren

Der "Stall"-Effekt von THP: Wenn der Kernel beschließt, anonyme Pages in
2-MB-Blöcke zu konsolidieren (khugepaged-Daemon), kann das kurz zu
CPU-Spitzen und Latenz-Peaks führen. Für eine OLTP-DB mit garantierter
Response-Zeit ist das schlecht.

Dass khugepaged läuft erkennst du an CPU-Spitzen des gleichnamigen
Prozesses in top. Nach Deaktivierung verschwindet er.


9. Troubleshooting

9.1 Postgres startet nicht mit huge_pages = on

Fehler: FATAL: could not map anonymous shared memory: Cannot allocate memory

Ursache: Zu wenig HugePages allokiert.

Fix:

grep -i hugepages_total /proc/meminfo
# Wert mit shared_buffers / 2048 kB vergleichen
# Bei zu wenig: vm.nr_hugepages erhöhen oder huge_pages = try zurück

9.2 HugePages_Total lässt sich nicht setzen

Symptom: sudo sysctl vm.nr_hugepages=9100 → nur 4000 werden angezeigt

Ursache: Memory-Fragmentierung. Kernel findet keine zusammenhängenden
2-MB-Blöcke.

Fix (in dieser Reihenfolge):

# 1. Caches dropen
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches

# 2. Erneut probieren
sudo sysctl vm.nr_hugepages=9100

# 3. Wenn immer noch nicht genug: Neustart einplanen
#    (HugePages am besten früh im Bootup allokieren → sysctl.d reicht aus)

9.3 Speicher-Druck durch zu viele HugePages

Symptom: Andere Services (mariadb etc.) bekommen plötzlich OOM-Kills.

Ursache: vm.nr_hugepages × 2 MB ist fest reserviert und **nicht
austauschbar**. Wenn das zu viel des RAMs bindet, bleibt für Nicht-PG-Prozesse
zu wenig.

Fix: HugePages-Zahl reduzieren (z. B. von 9100 auf 5000) → entsprechend
shared_buffers kleiner.

9.4 huge_pages = on erzwingt, aber Performance ist schlechter

Unwahrscheinlich, aber möglich bei ungeeignetem Workload (sehr kleine
shared_buffers, dominant OS-Cache-Zugriffe). Dann zurück auf try und
vm.nr_hugepages = 0. HugePages ist kein Silver Bullet — bei richtig
großen shared_buffers (≥ 4 GB) zahlen sie sich aber fast immer aus.


10. Zusammenfassung & Empfehlung für diesen Server

Wartungsfenster (15 Min) — alle Aenderungen zusammen:

  • shared_buffers von 256 MB → 16 GB
  • huge_pages von tryon
  • max_worker_processes von 8 → 32 (wichtiger Parallel-Hebel bei 16 Cores)
  • max_parallel_workers von 12 → 16
  • vm.nr_hugepages = 9100
  • THP → never
  • vm.swappiness = 10
  • Postgres restart

Erwarteter Effekt beim naechsten V2-Backtest-Lauf:

Hebel Gewinn
HugePages TLB-Misses reduziert → ~5-10% weniger CPU-Overhead
shared_buffers 64× größer Buffer-Hit-Rate steigt dramatisch, weniger Disk-Reads
max_worker_processes 32 Parallel-Queries bekommen ihre 4× Parallelitaet voll → Faktor 2-3 speedup

Kombinierter Speedup: Faktor 3-5.

Bei heutiger V2-Laufzeit von 40-50h (CPU-bound, parallel workers verhungern):
kuenftig 10-15 h realistisch. Bei aktiv laufendem V2 mit Resume-Feature
kann man das Maintenance-Fenster sogar mittendrin ziehen — die Queue
behaelt 'done'-Eintraege, 'processing' wird per Stale-Releaser wieder auf
'pending' gesetzt.


Referenzen