Das Betriebssystem unter PostgreSQL

TL;DR — PostgreSQL verwaltet seinen eigenen Cache, verlässt sich für alles andere aber auf den Linux-Kernel: für das Zurückschreiben schmutziger Seiten, für die Reihenfolge der I/O-Aufträge, für die Taktfrequenz der CPU und dafür, dass der Prozess überhaupt am Leben bleibt. Steht der Kernel falsch, hilft keine postgresql.conf. Dieser Artikel behandelt vier Bereiche — Speicher, I/O, CPU, Netzwerk und Grenzwerte — und vor allem, wie du misst, ob eine Änderung bei dir etwas bewirkt.

Die Schichten zwischen einer Abfrage und der Festplatte: PostgreSQL, Seitencache des Kernels, Block-Layer, Datenträger PostgreSQL shared_buffers — eigener Cache, eigene Verdrängung Seitencache des Kernels dieselben Daten ein zweites Mal — vm.swappiness, dirty_ratio Block-Layer Scheduler, read_ahead, Warteschlangentiefe Datenträger NVMe, SSD oder rotierend — jeweils andere Physik Kernel entscheidet hier mit
Abbildung 1: PostgreSQL kontrolliert nur die oberste Schicht. Die drei darunter gehören dem Kernel — und sie entscheiden mit darüber, wie schnell eine Abfrage zurückkommt.

Stand: August 2026 — geprüft gegen Kernel 6.18, PostgreSQL 17 und systemd 247. Kernel- und Distributions-Vorgaben ändern sich; lies vor jedem Setzen den Ist-Wert.

Vorab. Alle Werte hier sind Ausgangspunkte, keine Vorgaben. Was zu deinem Server passt, hängt von Arbeitslast, Speicherausstattung und Datenträger ab. Einige Eingriffe betreffen das Datenverzeichnis und das Verhalten bei Absturz oder Stromausfall — 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. Für den Betrieb deiner Systeme bist du verantwortlich.

Prüfen kommt vor Setzen. Ein großer Teil dessen, was in älteren Anleitungen als „unbedingt einstellen" steht, ist längst Vorgabe — bei manchen Werten wäre das blinde Übernehmen heute sogar eine Verschlechterung. Zu jeder Einstellung steht deshalb der Befehl dabei, mit dem du den Ist-Zustand abliest. Lies ihn zuerst.


Warum die Datenbank am Betriebssystem hängt

PostgreSQL liest und schreibt seine Datendateien über den normalen Dateisystem-Pfad und umgeht den Seitencache des Kernels nicht. Eine Datenseite liegt damit im Regelfall zweimal im Arbeitsspeicher: einmal in shared_buffers, einmal im Seitencache. Eine Direct-I/O-Variante existiert seit Version 16 als Entwickleroption debug_io_direct, ist aber ausdrücklich nicht für den Produktionsbetrieb gedacht und standardmäßig aus — für jede unterstützte Version gilt deshalb: die Seite liegt doppelt.

Das klingt nach Verschwendung und ist trotzdem die richtige Entscheidung. Der Kernel weiß besser, welche Seiten das Gesamtsystem gerade braucht, und er kann sie ohne Zutun der Datenbank freigeben. Für dich hat es eine praktische Folge: der freie Speicher, den free -h anzeigt, ist nicht der Speicher, den PostgreSQL nutzt. Wer shared_buffers so groß wählt, dass für den Seitencache nichts übrig bleibt, verlangsamt die Datenbank oft, statt sie zu beschleunigen. Die Aufteilung selbst behandelt unser Überblick der PostgreSQL-Settings; hier geht es um die Schicht darunter.

Der zweite Punkt betrifft das Schreiben. PostgreSQL schreibt Änderungen zuerst in das Write-Ahead-Log und erst beim Checkpoint in die Datendateien. Wann diese Schreibvorgänge tatsächlich auf dem Datenträger landen, entscheidet aber der Kernel — anhand von Schwellen, die auf Allzweck-Systeme ausgelegt sind, nicht auf Datenbanken.


Speicher: nicht auslagern, nicht abgeschossen werden

vm.swappiness — und warum 0 der falsche Wert ist

vm.swappiness steuert, wie bereitwillig der Kernel Speicherseiten auf die Auslagerungsdatei schreibt. Der Vorgabewert 60 ist für Arbeitsplatzrechner gedacht. Für einen Datenbankserver ist er zu hoch: ausgelagerte shared_buffers sind das Gegenteil dessen, wofür sie da sind.

cat /proc/sys/vm/swappiness
# /etc/sysctl.d/99-postgresql.conf
vm.swappiness = 1

Ein verbreiteter Rat lautet vm.swappiness = 0. Nimm stattdessen 1. Seit Kernel 3.5 bedeutet 0 nicht mehr „so spät wie möglich auslagern", sondern faktisch: gar nicht, solange noch nennenswert Seitencache da ist. Der Kernel greift erst darauf zurück, wenn fast nichts mehr übrig ist — das Ventil öffnet dadurch so spät, dass in der Praxis oft zuerst der OOM-Killer greift. Der Wert 1 erhält das Ventil und vermeidet trotzdem alles Auslagern im Normalbetrieb.

Über den Dateinamen ist dieses Rennen nicht zu gewinnen. sysctl.d liest lexikografisch und der letzte Treffer gewinnt — aber /etc/sysctl.conf wird grundsätzlich ganz zuletzt angewandt, auf Ubuntu zusätzlich als 99-sysctl.conf dorthin verlinkt. Steht vm.swappiness noch in der alten /etc/sysctl.conf, schlägt der Wert von dort jede Datei in sysctl.d, auch eine 99-. Räum den Altwert dort aus, statt ihn überbieten zu wollen. Was am Ende gilt, sagt dir ohnehin nur eines: sysctl vm.swappiness nach einem sysctl --system.

Das Zurückschreiben schmutziger Seiten

Die zweite Speicher-Stellschraube betrifft Schreib-Bursts. Der Kernel sammelt geänderte Seiten und schreibt sie gebündelt zurück. vm.dirty_background_ratio legt fest, ab welchem Anteil er im Hintergrund beginnt; vm.dirty_ratio legt fest, ab wann schreibende Prozesse blockiert werden, bis Platz frei ist.

Bezugsgröße ist dabei nicht der gesamte Arbeitsspeicher, sondern der auslagerbare Anteil — freie Seiten plus rückgewinnbarer Seitencache. Auf einer Maschine mit viel RAM sind die Vorgaben trotzdem gefährlich, weil in der Größenordnung zweistellige Gigabyte an schmutzigen Seiten zusammenkommen können, die im ungünstigsten Moment am Stück auf den Datenträger wollen. Das Ergebnis sind Latenzspitzen, die in keiner Abfrage sichtbar werden und trotzdem jeden Nutzer treffen.

# /etc/sysctl.d/99-postgresql.conf
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10

Auf sehr großen Maschinen sind die byte-basierten Varianten vm.dirty_background_bytes und vm.dirty_bytes die genauere Wahl — sie hängen an keiner relativen Bezugsgröße und bleiben deshalb beim Hardware-Ausbau stabil. Setze immer nur eines der beiden Paare: wer einen Byte-Wert setzt, liest den entsprechenden Prozentwert danach als 0.

Huge Pages

Große Speicherseiten reduzieren den Verwaltungsaufwand der Adressumsetzung deutlich, und Transparent Huge Pages gehören auf Datenbankservern abgeschaltet, weil ihr Hintergrundprozess sporadische Stillstände verursacht. Beides ist ein Thema für sich, mit Vorher-Prüfung, Berechnung und Rückweg — es steht vollständig in unserer Anleitung zu HugePages für PostgreSQL. Wenn du an dieser Stelle nur eine Sache mitnimmst: THP ausschalten ist die risikoärmste Maßnahme in diesem ganzen Artikel, und sie wirkt ohne Neustart.

Zwischenstück: die richtige systemd-Unit

Ab hier kommen Einstellungen, die in einer systemd-Unit landen — und damit die häufigste stille Fehlerquelle auf Debian und Ubuntu. Deshalb steht sie vor den Beispielen und nicht dahinter.

postgresql.service ist dort keine echte Unit, sondern eine Sammelklammer mit Type=oneshot und ExecStart=/bin/true. Ein Drop-in unter postgresql.service.d/ wirkt damit auf /bin/true — der Datenbankprozess sieht davon nichts, und niemand bekommt eine Fehlermeldung. Die echte Unit ist die Instanz:

Distribution Unit Drop-in anlegen
Debian, Ubuntu postgresql@17-main.service systemctl edit postgresql@17-main
RHEL, Rocky, Alma (PGDG) postgresql-17.service systemctl edit postgresql-17

Welche auf deinem System läuft, sagt systemctl list-units 'postgres*'. In allen folgenden Beispielen steht der Debian-Name — ersetze ihn durch deinen. systemctl edit legt das Drop-in am richtigen Ort an und führt das nötige systemctl daemon-reload gleich mit aus.

Der OOM-Killer

Geht dem System der Speicher aus, sucht der Kernel einen Prozess zum Abschießen — und wählt gern den mit dem größten Speicherverbrauch. Das ist der Postmaster. Stirbt er, reißt er alle Kindprozesse mit und PostgreSQL fährt eine Wiederherstellung.

Zwei Wege stehen zur Wahl, und sie sind nicht gleichwertig:

Zwei Wege gegen den OOM-Killer: overcommit_memory=2 wirkt systemweit, oom_score_adj schützt gezielt den Postmaster vm.overcommit_memory = 2 wirkt auf das ganze System + der OOM-Killer läuft gar nicht erst an − nutzbarer Speicher sinkt spürbar − falsche overcommit_ratio führt zu Allokationsfehlern im Normalbetrieb − trifft auch jeden anderen Dienst oom_score_adj am Postmaster wirkt gezielt auf einen Prozess + schützt den Elternprozess + kostet keinen nutzbaren Speicher + auf Debian/Ubuntu bereits gesetzt ⚠ wirkt nur mit PG_OOM_ADJUST_FILE, sonst erben die Backends den Wert
Abbildung 2: Die Abwägung. Der gezielte Weg löst das eigentliche Problem — dass der falsche Prozess stirbt — ohne den Preis der systemweiten Variante. Er hat aber eine Bedingung, ohne die er ins Gegenteil kippt.

Der gezielte Weg ist meist der bessere. Der systemweite kostet spürbar nutzbaren Speicher, erzeugt bei falsch gewählter overcommit_ratio schon im Normalbetrieb Allokationsfehler, und er trifft jeden anderen Dienst auf der Maschine mit — für einen Server, auf dem neben der Datenbank noch etwas anderes läuft, ist das ein hoher Preis für ein Problem, das eine einzelne Prozess-Einstellung löst.

Auf Debian und Ubuntu ist dieser gezielte Weg bereits eingerichtet: das Paket setzt OOMScoreAdjust=-900 in der Unit, und pg_ctlcluster setzt beim Start PG_OOM_ADJUST_FILE. Prüfen statt setzen:

systemctl cat postgresql@17-main | grep -i oom

Das zeigt allerdings nur die halbe Wahrheit: pg_ctlcluster setzt die entscheidende Umgebungsvariable im Startskript, nicht in der Unit — im systemctl cat taucht sie deshalb nicht auf. Der Beweis steht im laufenden Prozess. Der Postmaster muss −900 zeigen, ein Backend 0:

cat /proc/$(head -1 /var/lib/postgresql/17/main/postmaster.pid)/oom_score_adj
pgrep -u postgres -f 'postgres:' | head -1 | xargs -I{} cat /proc/{}/oom_score_adj

Die Umgebungsvariable ist der entscheidende Teil, und sie fehlt in vielen Anleitungen. OOMScoreAdjust allein vererbt sich auf jedes Backend — dann steht die ganze Instanz auf −900 und der OOM-Killer trifft irgendeinen anderen Prozess auf dem System. Erst PG_OOM_ADJUST_FILE weist PostgreSQL an, den Wert für Kindprozesse zurückzusetzen, sodass im Zweifel eine Verbindung stirbt statt der Instanz. Seit PostgreSQL 9.5 ist das eine Laufzeit-Umgebungsvariable; die früher nötige Build-Option gibt es nicht mehr. Falls du selbst Hand anlegst, gehören beide Teile zusammen:

# systemctl edit postgresql@17-main
[Service]
OOMScoreAdjust=-900
Environment=PG_OOM_ADJUST_FILE=/proc/self/oom_score_adj
Environment=PG_OOM_ADJUST_VALUE=0

I/O: die Schicht, in der die meiste Zeit verloren geht

Der Scheduler

Der I/O-Scheduler sortiert Aufträge, bevor sie den Datenträger erreichen. Diese Sortierung war sinnvoll, solange ein Schreib-Lese-Kopf physisch über eine Scheibe fahren musste. Bei NVMe ist sie reiner Aufwand ohne Gegenwert.

Auf aktuellen Kerneln wählt der Block-Layer das meist schon richtig: none für Geräte mit mehreren Hardware-Warteschlangen wie NVMe, mq-deadline für Geräte mit einer. Nachsehen lohnt trotzdem, vor allem virtualisiert:

cat /sys/block/nvme0n1/queue/scheduler

Interessant ist der Fall, in dem der Default nicht passt: liegt der Datenträger auf einem SAN oder einem Hypervisor-Volume, sortiert die Gegenstelle bereits. Eine zweite Sortierung im Gast bringt dann nichts und kann Latenz hinzufügen — none ist dort oft die bessere Wahl, auch wenn der Gast das Gerät für rotierend hält.

Über echo gesetzte Werte überleben keinen Neustart. Dauerhaft gehört das in eine udev-Regel:

# /etc/udev/rules.d/60-io-scheduler.rules
ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="nvme[0-9]*n[0-9]*", ATTR{queue/scheduler}="none"
ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"

Die Sternchen in den Gerätenamen sind kein Schmuck: nvme[0-9]n[0-9] ohne sie verfehlt den elften Controller ebenso wie die zehnte Namensraum-Nummer, und sd[a-z] endet bei sdz. Auf Systemen mit vielen Datenträgern fällt sonst genau der Teil durchs Raster, der die Datenbank trägt. Das ENV{DEVTYPE}=="disk" hält die Regel von Partitionen fern — die haben kein queue/-Verzeichnis, und der Schreibversuch landet sonst bei jedem Ereignis als Fehler im udev-Protokoll.

Vorauslesen

read_ahead bestimmt, wie viel der Kernel über den angeforderten Bereich hinaus liest. Die Vorgabe beträgt 128 KiB. Zwei Ablesestellen, zwei Einheiten — das ist die Stolperfalle:

cat /sys/block/nvme0n1/queue/read_ahead_kb    # KiB       -> 128
blockdev --getra /dev/nvme0n1                 # Sektoren  -> 256

Für Arbeitslasten mit vielen sequentiellen Durchläufen — Berichte, Analysen, große Aggregationen — kann ein höherer Wert helfen. Für kurze, wahlfreie Zugriffe bringt er nichts und belegt Seitencache mit Daten, die niemand anfordert. Das ist eine Stellschraube, die du nur mit Messung anfassen solltest; sie gehört zu den wenigen, bei denen der falsche Wert messbar schadet.

Dateisystem und Mount-Optionen

ext4 und xfs sind beide geeignet und werden beide breit eingesetzt. Wichtiger als die Wahl zwischen ihnen ist, wie du sie einhängst:

/dev/nvme0n1p1  /var/lib/postgresql  xfs  noatime  0 0

noatime unterdrückt das Zurückschreiben der Zugriffszeit bei jedem Lesen — eine Schreiboperation, die für eine Datenbank keinerlei Nutzen hat. Ein zusätzliches nodiratime braucht es nicht, noatime schließt es bereits ein. Die letzte Spalte steht bei XFS auf 0, weil fsck.xfs ohnehin nichts tut.

Schreib-Barrieren nicht abschalten. Der Ratschlag nobarrier kursiert seit Jahren: er nimmt dem Dateisystem die Möglichkeit, eine Schreibreihenfolge zu erzwingen. Bei einem Stromausfall oder Kernel-Absturz kann das WAL dann inkonsistent zurückbleiben — genau der Fall, gegen den es geschrieben wurde. Auf XFS hat sich das Thema erledigt, dort wurde die Option mit Kernel 4.19 entfernt und ein Mount damit schlägt fehl. Auf ext4 gibt es sie weiterhin, und dort gilt die Regel unverändert: Finger weg.

Wenn du dich an dieser Stelle fragst, welche Wartungsarbeiten die Datenbankseite dazu beiträgt: das behandelt unser Beitrag zu VACUUM, REINDEX und CLUSTER.


CPU: Taktfrequenz und Speichernähe

Der Governor

Moderne Kernel takten CPUs zur Stromersparnis herunter und wieder herauf. Das Heraufregeln kostet Zeit — wenig, aber bei kurzen Abfragen im Verhältnis viel. Für einen Datenbankserver, der ohnehin durchläuft, ist die Ersparnis meist den Latenzpreis nicht wert:

cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

Der passende Wert heißt performance, auch auf Systemen mit intel_pstate. Dauerhaft am einfachsten über tuned, das auf der RHEL-Familie ohnehin läuft und in Debian und Ubuntu verfügbar ist:

tuned-adm profile throughput-performance

Ohne tuned geht es mit cpupower frequency-set -g performance (Paket linux-tools-common bzw. kernel-tools), dauerhaft über eine kleine systemd-Unit oder /etc/default/cpufrequtils. Bei virtuellen Maschinen greift die Einstellung im Gast oft gar nicht — dort entscheidet der Hypervisor, und der Wert im Gast ist Zierde.

NUMA

Auf Maschinen mit mehreren Prozessorsockeln ist Arbeitsspeicher nicht gleich Arbeitsspeicher: jeder Sockel hat eigenen, direkt angebundenen Speicher, und der Zugriff auf den des Nachbarsockels ist langsamer. Läuft PostgreSQL ungünstig verteilt, liegt ein Teil von shared_buffers dauerhaft „auf der falschen Seite".

numactl --hardware                    # gibt es überhaupt mehrere Knoten?
cat /proc/sys/vm/zone_reclaim_mode

Zwei Dinge sind hier wichtig. Erstens: zone_reclaim_mode muss 0 sein. Steht er auf 1, gibt der Kernel lieber Seitencache frei, als Speicher vom Nachbarknoten zu nehmen — für eine Datenbank die schlechtestmögliche Wahl. Auf aktuellen Kerneln ist 0 bereits Vorgabe, geprüft gehört es trotzdem, weil ältere Installationen abweichen.

Zweitens: eine Verteilung über alle Knoten ist für eine einzelne große Instanz meist besser als die Bindung an einen. Das erledigt systemd selbst, ohne Eingriff in den Startbefehl:

# systemctl edit postgresql@17-main
[Service]
NUMAPolicy=interleave
NUMAMask=all

NUMAPolicy= setzt systemd 243 voraus, NUMAMask=all sogar 247 — auf älteren Ständen wie Ubuntu 20.04 oder RHEL 8 wird der Wert kommentarlos verworfen, die Unit startet normal und die Politik gilt nicht. Genau das stille Muster, das dieser Artikel sonst anprangert. Nachsehen, ob sie angekommen ist:

systemctl show -p NUMAPolicy -p NUMAMask postgresql@17-main

Kommt NUMAMask= leer zurück, hat systemd den Wert nicht verstanden.

⚠ Der ältere Weg, ExecStart durch einen numactl-Aufruf zu ersetzen, ist auf Debian und Ubuntu ein Fehlschlag: er bricht das Zusammenspiel aus Type=forking und PIDFile, und der Datenpfad allein genügt dort nicht, weil die Konfiguration unter /etc/postgresql/ liegt. Die beiden Direktiven oben brauchen weder numactl noch einen Eingriff in den Startbefehl.

Auf Maschinen mit nur einem Sockel — und das sind die meisten — ist der ganze Abschnitt gegenstandslos. numactl --hardware beantwortet das in einer Sekunde.


Netzwerk und Grenzwerte

Zwei Grenzen fallen erst unter Last auf, und dann unangenehm.

Die Annahme-Warteschlange. Kommen viele Verbindungen gleichzeitig an, landen sie in einer Warteschlange, deren Länge net.core.somaxconn begrenzt. Ist sie voll, werden Verbindungen abgewiesen — die Anwendung sieht einen Verbindungsfehler, die Datenbank sieht gar nichts.

sysctl net.core.somaxconn

⚠ Hier ist Nachsehen wichtiger als Setzen. Seit Kernel 5.4 liegt die Vorgabe bei 4096; alle aktuellen Distributionen liegen also bereits darüber. Viele Anleitungen empfehlen bis heute Werte um 1024, weil der alte Default 128 betrug — das Übernehmen würde die Warteschlange heute kürzen statt verlängern. Nur auf Kerneln vor 5.4 ist etwas zu tun, und dann auf denselben Wert:

# /etc/sysctl.d/99-postgresql.conf — nur bei Kernel < 5.4 nötig
net.core.somaxconn = 4096

Dateideskriptoren. PostgreSQL braucht Deskriptoren für Verbindungen, für offene Relationsdateien und für temporäre Dateien. Die Standardgrenze von 1024 pro Prozess ist für einen Server mit vielen Verbindungen und vielen Tabellen zu knapp:

# systemctl edit postgresql@17-main
[Service]
LimitNOFILE=65536

⚠ Diese Grenze setzt man in der systemd-Unit, nicht in /etc/security/limits.conf. Dienste, die systemd startet, durchlaufen keine Anmeldesitzung und damit auch nicht das Modul, das limits.conf auswertet. Das ist eine der häufigsten Ursachen dafür, dass eine sorgfältig gesetzte Grenze wirkungslos bleibt — und sie gehört auf die Instanz-Unit, nicht auf die Sammelklammer.

Was hier nicht hilft, aber oft empfohlen wird: das Abschalten von IPv6 oder das Feintuning von TCP-Puffergrößen. Beides bewegt bei einer Datenbank im lokalen Netz in aller Regel nichts Messbares.


Messen statt raten

Der wichtigste Abschnitt, und der am häufigsten übersprungene. Jede Einstellung oben ist eine Hypothese, bis du sie an deiner Arbeitslast geprüft hast.

Vier Schritte: Ausgangswert erheben, eine Sache ändern, erneut messen, behalten oder zurücknehmen 1 Ausgangswert unter echter Last 2 Eine Sache ändern nur eine 3 Erneut messen gleicher Zeitraum 4 Behalten oder zurück und dokumentieren
Abbildung 3: Wer zwei Dinge gleichzeitig ändert, weiß hinterher nicht, welches gewirkt hat — und ob sich die beiden vielleicht aufheben.

Was du erhebst, hängt davon ab, was du vermutest:

Vermutung Womit du misst
Schreib-Bursts blockieren sar -b, iostat -x 1, Spalten %util und await
Auslagerung findet statt vmstat 1, Spalten si und so — beide müssen 0 bleiben
Checkpoints kosten zu viel pg_stat_bgwriter, Verhältnis checkpoints_timed zu checkpoints_req
Der Datenträger ist der Engpass fio mit einem Profil, das deiner Last ähnelt — nicht mit dem Standardprofil
Abfragen sind langsamer geworden pg_stat_statements, verglichen über denselben Zeitraum

Zwei Fallstricke, die Messungen regelmäßig entwerten. Erstens: ein Vergleich zwischen Montagvormittag und Sonntagnacht misst den Wochentag, nicht die Einstellung. Zweitens: pgbench mit Standardparametern misst pgbench, nicht deine Anwendung — der Zugriffsverteilung nach ist es eine gleichverteilte Punktabfrage, und die hat mit den meisten echten Arbeitslasten wenig gemein.

Wer diese Systematik nicht nebenbei aufbauen will, findet sie im Seminar Datenbankübergreifendes Performance-Tuning als eigenes Thema: Performance-Optimierung relationaler Datenbanken, unabhängig vom eingesetzten System.


Was dieser Artikel nicht behandelt

Bewusst ausgeklammert bleibt die Datenbankseite: shared_buffers, work_mem, effective_cache_size, WAL- und Checkpoint-Einstellungen. Sie stehen in unserem Überblick der Performance-Settings und gehören dort auch hin — dieser Artikel endet an der Grenze zwischen Kernel und Datenbank.

Ebenfalls nicht behandelt: Replikation, Verbindungs-Pooling und Hochverfügbarkeit. Das sind Architekturfragen. Sie werden relevant, nachdem die Grundlagen hier stimmen — vorher verlagert man ein Problem nur auf mehr Maschinen.


Häufige Fragen

Bringt das alles zusammen mehr als ein besserer Index?

In aller Regel nicht. Ein fehlender Index verändert den Aufwand einer Abfrage strukturell; die Einstellungen hier verändern ihn nicht, sie nehmen Reibung und Ausreißer heraus. Zwei Ausnahmen bestätigen das eher, als sie es widerlegen: ausgelagerte shared_buffers und THP-Stillstände kosten im Einzelfall sehr viel mehr — aber sie sind Störungen, keine Optimierungen. Die Reihenfolge ist deshalb: erst Abfragen und Indizes, dann die Datenbankeinstellungen, dann der Kernel. Wer umgekehrt anfängt, optimiert die Unterlage eines schiefen Tisches.

Muss ich nach jeder Änderung neu starten?

Nein, die meisten nicht. sysctl-Werte greifen mit sysctl --system sofort, Scheduler-Einstellungen beim Schreiben in /sys. Einen Neustart des Dienstes brauchen die Einstellungen aus der systemd-Unit — und dort gilt: das Drop-in wird erst nach systemctl daemon-reload gelesen, danach systemctl restart postgresql@17-main. Wer mit systemctl edit arbeitet, bekommt das Reload geschenkt. Einen Neustart des Systems braucht praktisch nichts davon. Wichtig ist der umgekehrte Fall: Werte, die du nur zur Laufzeit gesetzt hast, sind nach dem nächsten Neustart weg — genau deshalb steht zu jeder Einstellung auch der dauerhafte Ort.

Gilt das auch in einer virtuellen Maschine oder im Container?

Teilweise. Die Speicher-Einstellungen und die Grenzwerte wirken auch im Gast. Der CPU-Governor meist nicht, weil der Hypervisor die Taktung bestimmt. Der I/O-Scheduler wirkt, aber unter anderen Vorzeichen: sortiert die darunterliegende Schicht bereits, ist none im Gast oft die bessere Wahl. In Containern teilst du dir den Kernel mit dem Wirtssystem — sysctl-Werte gehören dann dorthin, nicht in den Container.

Woran erkenne ich, dass THP das Problem ist?

Am typischen Muster: die Datenbank läuft normal und steht dann für Bruchteile von Sekunden bis wenige Sekunden still, ohne dass Last, Abfragen oder I/O das erklären. Diese Stillstände treten unregelmäßig auf und verschwinden nach dem Abschalten von THP. Der Nachweis gelingt über die Verweildauer im Speicher-Kompaktierungspfad; die Einzelheiten stehen in der HugePages-Anleitung.

Ist noatime wirklich sicher?

Ja, für ein PostgreSQL-Datenverzeichnis. Die Zugriffszeit einer Datei wertet dort niemand aus. Vorsicht ist nur geboten, wenn auf demselben Dateisystem Anwendungen liegen, die sie tatsächlich lesen — klassisch einige Mail-Zustellsysteme. Auf einem eigenen Datenträger für die Datenbank stellt sich die Frage nicht.

Warum wird von nobarrier so oft abgeraten und trotzdem so oft empfohlen?

Weil es messbar schneller war und der Preis erst im Ernstfall sichtbar wird. Die Empfehlungen stammen überwiegend aus einer Zeit mit anderer Hardware und wurden seitdem weitergereicht. Ohne Barrieren kann der Datenträger Schreibvorgänge umsortieren; das WAL verliert damit die Eigenschaft, auf die sich die Wiederherstellung stützt. Auf XFS hat der Kernel die Entscheidung inzwischen abgenommen und die Option entfernt; auf ext4 liegt sie weiter bei dir.

Warum wirkt meine systemd-Einstellung nicht?

Mit hoher Wahrscheinlichkeit, weil sie an der falschen Unit hängt. Auf Debian und Ubuntu ist postgresql.service nur eine Sammelklammer, die /bin/true ausführt; die Datenbank läuft unter postgresql@17-main.service. Ein Drop-in auf der Sammelklammer wird ohne Fehlermeldung wirkungslos. systemctl cat postgresql@17-main zeigt dir, was für die laufende Instanz tatsächlich gilt — inklusive aller Drop-ins.

In welcher Reihenfolge gehe ich vor, wenn ich heute anfange?

Erst lesen, dann schreiben: sysctl vm.swappiness, sysctl net.core.somaxconn, cat /sys/block/<dev>/queue/scheduler, systemctl cat postgresql@17-main. Danach die fünf Eingriffe mit dem besten Verhältnis aus Aufwand und Risiko: THP abschalten, vm.swappiness auf 1, Zurückschreib-Schwellen senken, noatime setzen, LimitNOFILE an der richtigen Unit anheben. Das ist in einer Stunde erledigt und lässt sich einzeln zurücknehmen. Alles Weitere — NUMA, Vorauslesen, Governor — lohnt erst, wenn eine Messung dafür spricht.


Wer diese Zusammenhänge nicht nur nachlesen, sondern systematisch aufbauen will: das Seminar PostgreSQL Administration behandelt Konfiguration, Performanz, Monitoring, Backup und Recovery im Zusammenhang — also die Datenbankseite dessen, was hier auf Kernel-Ebene beschrieben ist. Es findet als Inhouse-Veranstaltung statt, die Übungsumgebung bringe ich als vorbereitete virtuelle Maschinen mit: kein Aufwand für deine IT, und niemand übt an einem Produktivsystem.