PostgreSQL HugePages aktivieren — Leitfaden

Wie HugePages speicherintensive Abfragen entlasten: shared_buffers, vm.nr_hugepages, THP — komplett mit Rollback-Plan.

Vorab. Alle Werte hier sind Ausgangspunkte, keine Vorgaben. Was zu deinem Server passt, hängt von Arbeitslast, Speicherausstattung und Datenträger ab. Einige Schritte greifen tief ein — Kernel-Speicher reservieren, Caches leeren, den Dienst neu starten. Probiere sie zuerst auf einem Testsystem und nur mit einer aktuellen, wiederherstellbar geprüften Sicherung. Ändere eine Sache pro Schritt, miss davor und danach, und sorge dafür, dass du jeden Eingriff zurücknehmen kannst — der Rollback-Plan steht in Abschnitt 7. Für den Betrieb deiner Systeme bist du verantwortlich.

Beispiel-Ausgangslage: Ein Linux-Server mit viel RAM (z. B. 128 GB), Postgres 18, auf dem shared_buffers = 256 MB eingestellt ist, 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 — begegnet uns in Seminaren regelmäßig und ist genau der Fall, in dem sich Tuning am meisten lohnt.

HugePages

PostgreSQL profitiert von Explicit HugePages — spürbar bei Abfragen, die viel Shared Memory gleichzeitig berühren, kaum bei kurzen Index-Zugriffen. 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

Vergleich der Adressumsetzung für zwei Megabyte Speicher: mit 4-KB-Seiten sind rund 512 TLB-Einträge nötig, mit einer 2-MB-HugePage genügt ein einziger Vergleich zweier Wege, 2 MB Speicher zu adressieren: mit Standard-4-KB-Seiten braucht es 512 Seiten und damit 512 TLB-Einträge; mit einer einzigen 2-MB-HugePage genügt ein einziger TLB-Eintrag. Weniger Einträge bedeuten weniger Verwaltungsaufwand bei jedem Speicherzugriff. Adressumsetzung für 2 MB Speicher Standard: 4-KB-Seiten 512 Seiten, 512 Einträge nötig 512 TLB-Einträge belegt 2-MB-HugePage eine Seite, ein Eintrag 2 MB ein TLB-Eintrag belegt Weniger Einträge heißt weniger Verwaltungsaufwand bei jedem Speicherzugriff.
Abbildung 1: Dieselbe Speichermenge, zwei Seitengrößen. Der Unterschied wirkt sich überall dort aus, wo eine Abfrage viel Speicher gleichzeitig berührt.

2. Wo PostgreSQL profitiert — und wo nicht

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.

Wo der Effekt liegt — die Reihenfolge ist belastbar, die Größenordnung hängt an deiner Hardware und Arbeitslast. Miss es an deinem System, statt eine Zahl aus einer Tabelle zu übernehmen:

Szenario Erwartbarer Gewinn
Analytische Joins, große CTEs, breite Scans am größten — hier berührt eine Abfrage viel Speicher auf einmal
Random-OLTP mit vielen kleinen Abfragen gering — wenige Seiten je Abfrage, wenig zu gewinnen
Bulk-Insert gering bis mäßig, abhängig von der Stapelgröße

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 [madvise] 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 die Background-Worker (parallele Abfragen, logische Replikation, Erweiterungen). Autovacuum zählt nicht hinein — dafür gibt es autovacuum_max_workers mit eigenen Slots. PG-Default ist 8 — auf einem 16-Core-Server viel zu niedrig. Wenn 8 parallele Sessions jeweils 4 Worker wollen, braucht der Pool 32 Slots; max_parallel_workers muss dann entsprechend mitwachsen, sonst greift die kleinere der beiden Grenzen. Ohne die Erhöhung laufen große Analyse-Abfragen effektiv seriell statt mit vierfacher Parallelität.

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
# ⚠ Leert den KOMPLETTEN Seitencache. Die Datenbank liest danach für einige
#   Zeit kalt von Platte — nicht zur Hauptlastzeit ausführen.
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'
[Unit]
Description=Disable Transparent Huge Pages
After=sysinit.target local-fs.target

[Service]
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"

[Install]
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:     9100
HugePages_Rsvd:     8200       ← so viele hat PostgreSQL zugesagt bekommen
AnonHugePages:         0 kB    ← THP aus; sollte nach dem Neustart gegen 0 gehen

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.

THP ist dabei nur eine von mehreren Kernel-Einstellungen, die unter einer Datenbank mitentscheiden. Wie sie zusammenspielen — Auslagerungsneigung, Zurückschreib-Schwellen, I/O-Scheduler und die richtige systemd-Unit — steht in unserem Beitrag zum Betriebssystem unter PostgreSQL. Für Windows-Server gibt es unseren Beitrag zu Windows unter dem Datenbankserver.


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 — leert den Seitencache, danach liest die DB kalt
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 sind kein Allheilmittel: ob sie sich lohnen, hängt nicht an der Größe von shared_buffers, sondern daran, wie viel Speicher eine einzelne Abfrage berührt (siehe Abschnitt 2).


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 weniger TLB-Einträge je Speicherzugriff → weniger Verwaltungsaufwand; spürbar nur bei speicherintensiven Abfragen
shared_buffers deutlich größer mehr Treffer im eigenen Cache — sofern der häufig gelesene Teil danach hineinpasst
max_worker_processes erhöht parallele Abfragen bekommen ihre geplanten Worker wirklich, statt still seriell zu laufen

Zusammengenommen sind Faktor 3–5 realistisch — der weitaus größte Anteil kommt dabei aus den Parallel-Workern und dem vergrößerten shared_buffers, nicht aus den HugePages. Diese Größenordnung gilt für die oben beschriebene Ausgangslage: ein Server mit viel RAM, auf dem PostgreSQL noch mit den Auslieferungswerten läuft. Wer bereits sinnvoll konfiguriert hat, holt entsprechend weniger heraus — und die Rechnung setzt voraus, dass die Last CPU-gebunden und parallelisierbar ist. Ein Lauf, der auf den Datenträger wartet, gewinnt durch mehr Worker nahezu nichts.

Ein langer, CPU-gebundener Batch-Lauf, dessen parallele Worker bisher verhungern (etwa eine nächtliche Auswertung über große Verlaufstabellen), kann sich dadurch deutlich verkürzen — wie stark, hängt am parallelisierbaren Anteil und lässt sich nur am eigenen Lauf messen. Wer einen Batch betreibt, der seinen Fortschritt zwischenspeichern und wieder aufnehmen kann (Resume), kann das Wartungsfenster sogar mittendrin ziehen.


Referenzen


Im Seminar vertiefen

Huge Pages sind eine Stellschraube von vielen, und sie wirkt nur, wenn shared_buffers dazu passt. Das Zusammenspiel von Arbeitsspeicher, Dateicache und Planer behandelt das Seminar PostgreSQL Performance-Tuning; für Installation und Betrieb der Instanz ist PostgreSQL Administration der passende Kurs.