PostgreSQL HugePages aktivieren — Leitfaden

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

Beispiel-Ausgangslage: Ein Linux-Server mit viel RAM (z. B. 128 GB), Postgres 18 mit dem Default shared_buffers = 256 MB, effective_cache_size = 32 GB, huge_pages = try. HugePages sind faktisch nicht konfiguriert (HugePages_Total = 4 → 8 MB). Transparent HugePages (THP) ist aktiv. Diese Konstellation — viel RAM, aber Postgres läuft noch mit den Auslieferungs-Defaults — ist in der Praxis extrem häufig und genau der Fall, in dem sich Tuning am meisten lohnt.

TL;DR

PostgreSQL profitiert deutlich von Explicit HugePages — typisch 5–15 % Speedup bei Queries, die viel Shared Memory berühren. Wenn dabei shared_buffers noch auf dem 256-MB-Default steht (auf einem Server mit dutzenden GB RAM), 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 %

Welche Queries und Funktionen profitieren konkret?

Faustregel: **Je größer der gleichzeitig berührte Datenbereich, desto größer der HugePages-Gewinn.** Eine Abfrage, die nur ein paar Zeilen über einen Index liest, merkt fast nichts. Eine Abfrage, die Millionen Zeilen scannt und große Zwischenergebnisse im Speicher aufbaut, merkt den Unterschied deutlich.

Starke Profiteure:

  • Große Aggregationen/Joins über Millionen Zeilen (Reports, Auswertungen)
  • Wiederkehrende Batch-/Nacht-Jobs, die dieselben großen Tabellen komplett scannen
  • Funktionen (SQL/PL/pgSQL), die über große Mengen iterieren oder aggregieren
  • Lang laufende Berechnungen mit mehreren großen CTEs oder Window-Functions

Kaum Profiteure:

  • Einzelsatz-Lookups über Primärschlüssel oder Index
  • Kleine OLTP-Queries mit winzigem Working-Set

Beispiel 1 — der typische Gewinner: große analytische Auswertung

Stell dir eine Tabelle verkauf mit zig Millionen Zeilen vor. Eine Auswertung, die daraus Tages-Umsätze rollt und einen gleitenden 30-Tage- Schnitt bildet, berührt riesige Speicherbereiche und baut große Zwischenergebnisse auf — genau das Szenario, in dem HugePages CPU-Overhead spart:

WITH tagesumsatz AS (
  SELECT date_trunc('day', verkauft_am) AS tag
       , kategorie_id
       , SUM(betrag)                     AS umsatz
    FROM verkauf
   WHERE verkauft_am >= CURRENT_DATE - INTERVAL '2 years'
   GROUP BY 1, 2
),
gleitend AS (
  SELECT tag
       , kategorie_id
       , AVG(umsatz) OVER (
           PARTITION BY kategorie_id
           ORDER BY tag
           ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
         ) AS schnitt_30t
    FROM tagesumsatz
)
SELECT tag, kategorie_id, schnitt_30t
  FROM gleitend
 WHERE schnitt_30t > 10000
 ORDER BY tag;

Solche Queries scannen Millionen Zeilen und treffen den shared_buffers- Bereich tausendfach. Ohne HugePages zahlt die CPU bei jedem dieser Treffer einen kleinen TLB-Aufschlag — über Millionen Zugriffe summiert sich das.

Beispiel 2 — eine Funktion, die über eine große Tabelle aggregiert

Eine Kennzahl-Funktion, die bei jedem Aufruf eine große Tabelle komplett durchrechnet (typisch für nächtliche Batch-Berechnungen), profitiert aus demselben Grund:

CREATE OR REPLACE FUNCTION kennzahl_pro_kunde()
RETURNS TABLE(kunde_id int, score numeric)
LANGUAGE sql
AS $$
  SELECT kunde_id
       , SUM(betrag) / NULLIF(COUNT(*), 0) AS score
    FROM verkauf
   WHERE storniert_am IS NULL
   GROUP BY kunde_id
$$;

Jeder Aufruf liest die gesamte große Tabelle erneut über shared_buffers — viel berührter Speicher, viele TLB-Lookups, klarer HugePages-Kandidat.

Beispiel 3 — der Gegenpol: kaum Nutzen

Eine Punktabfrage über den Primärschlüssel liest nur eine Handvoll Pages. Hier ist der TLB-Anteil verschwindend gering — HugePages helfen praktisch nicht:

SELECT * FROM kunde WHERE kunde_id = 4711;

Merke: HugePages beschleunigen nicht „die Datenbank" pauschal, sondern gezielt die speicherintensiven Großabfragen und Batch-Funktionen.


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!)

Der Default von 256 MB ist für einen Server mit dutzenden GB RAM deutlich zu niedrig. Postgres-Faustregeln:

Server-Rolle shared_buffers
Dedizierter DB-Server 25 % RAM
Server mit weiteren großen Diensten (z. B. zweite DB-Engine, Mail-Stack) 10–15 % RAM
Shared Hosting 5–10 % RAM

Auf einem Server mit ~128 GB RAM, der sich die Maschine mit anderen Diensten teilt, ist shared_buffers = 16 GB ein vernünftiger Mittelweg:

  • Genug, um große Verlaufs- und Auswertungstabellen großzügig zu cachen
  • Lässt 100+ GB für OS-Filesystem-Cache und andere Services

Wichtig: shared_buffers darf nicht größer als dein verfügbares RAM sein — und sollte nie höher als effective_cache_size liegen.

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 Batch-Jobs ebenfalls. Plane ein Zeitfenster von 10–15 Min.

5.1 Vorbereitung

Baseline-Messung — zum späteren Vergleich:

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 per Default massiv unterkonfiguriert!
# 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 für ALLE Background-Worker (parallele Queries, Autovacuum, logische Replikation, etc.). PG-Default ist 8 — auf einem 16-Core-Server viel zu niedrig. Wenn 8 parallele Sessions jeweils 4 Parallel-Worker wollen, braucht der Pool 32 Slots + ~3 für Autovacuum = 35. Ohne die Erhöhung laufen große Analyse-Queries effektiv seriell statt mit 4-facher Parallelität. Gewinn-Schätzung: Query-Zeit pro Durchlauf fällt 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 einmalig 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 von 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 — am besten eine deiner großen Aggregationen (siehe Abschnitt 2):

EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
  FROM verkauf
 WHERE verkauft_am >= CURRENT_DATE - 30;

Interessant: shared hit=xxx bleibt hoch (gut!), die 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 Keins 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 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

Wartungsfenster (15 Min) — alle Änderungen 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 nächsten rechenintensiven Analyse- oder Batch-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 Parallele Queries bekommen ihre 4-fache Parallelität voll → Faktor 2–3 Speedup

Kombinierter Speedup: Faktor 3–5.

Ein mehrstündiger, CPU-gebundener Batch-Lauf, dessen parallele Worker bisher verhungern (etwa eine nächtliche Auswertung über große Verlaufstabellen), kann so von z. B. 40–50 h auf realistische 10–15 h fallen. Wer einen Batch betreibt, der seinen Fortschritt zwischenspeichern und wieder aufnehmen kann (Resume), kann das Wartungsfenster sogar mittendrin ziehen.


Referenzen