2 Punkte von GN⁺ 3 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Mit Linux 7.0 wurde der bisherige Standard-Preemption-Modus PREEMPT_NONE für Server entfernt, was auf identischer Hardware zu einer schweren Performance-Regression führte, bei der sich der PostgreSQL-Durchsatz halbierte
  • Ein AWS-Ingenieur führte pgbench auf einer 96-vCPU-Graviton4-Maschine aus; dabei sanken unter Linux 7.0 im Vergleich zu Linux 6.x die Transaktionen pro Sekunde von 98.565 auf 50.751, während 55 % der CPU in einer einzigen Spinlock-Funktion verbraucht wurden
  • Ein Spinlock zum Schutz des Zugriffs auf den shared buffer pool von PostgreSQL koppelte sich mit Minor Page Faults von 4-KB-Speicherseiten; wenn während des Haltens des Locks eine Preemption durch den Scheduler erfolgt, verschwenden alle wartenden Backends CPU-Zeit durch aktives Drehen
  • Durch Aktivierung von Huge Pages (2 MB oder 1 GB) sinkt die Zahl potenzieller Page Faults von 31 Millionen auf einige Zehntausend bis einige Hundert, wodurch die Regression behoben wird
  • Auf Kernel-Seite wurde die Einführung von Restartable Sequences (rseq) vorgeschlagen, doch die PostgreSQL-Community vertritt die Position, dass ein Performance-Einbruch durch ein Kernel-Upgrade selbst gegen das Prinzip verstößt, dass man „Userspace nicht kaputtmacht“

Das Problem

  • AWS-Ingenieur Salvatore Dipietro führte pgbench auf einem 96-vCPU-Graviton4-Prozessor aus und testete eine hochgradig parallele Last mit scale factor 8.470 (Tabelle mit etwa 847 Millionen Zeilen), 1.024 Clients und 96 Threads
  • Der Durchsatz fiel von 98.565 TPS unter Linux 6.x auf 50.751 TPS unter Linux 7.0, also nahezu auf die Hälfte
  • Das Profiling mit perf zeigte, dass 55,60 % der CPU-Zeit in der Funktion s_lock verbraucht wurden
    • Aufrufpfad: StartReadBufferGetVictimBufferStrategyGetBuffers_lock

Was ist Preemption?

  • Wenn der OS-Scheduler einen laufenden Thread unterbricht und die CPU an einen anderen Thread übergibt, spricht man von Preemption
  • Vor Linux 7.0 gab es drei Optionen
    • PREEMPT_NONE: Ein Thread wird kaum unterbrochen, bis er die CPU freiwillig abgibt (syscall, I/O-Block, sleep). Das war traditionell der Standard auf Servern, mit wenigen Context Switches und hohem Durchsatz
    • PREEMPT_FULL: Ein laufender Thread kann an fast allen sicheren Punkten unterbrochen werden. Das senkt die Latenz, erhöht aber den Overhead durch Context Switches. Traditioneller Standard auf Desktops
    • PREEMPT_LAZY: Ein Kompromiss, eingeführt in Linux 6.12, der auf natürliche Grenzen wartet, aber bei Bedarf dennoch Preemption erlaubt. Er wurde so entworfen, dass er die Durchsatzcharakteristik von PREEMPT_NONE annähert
  • In Linux 7.0 wurde PREEMPT_NONE auf modernen CPU-Architekturen entfernt, sodass nur PREEMPT_FULL und PREEMPT_LAZY übrig blieben
    • Während PREEMPT_LAZY für die meiste Server-Software als Ersatz funktioniert, gibt es bei PostgreSQL einen kritischen Unterschied

Speicherverwaltung in PostgreSQL

  • PostgreSQL verwendet Seiten mit fester Größe als grundlegende Speichereinheit für Daten (data pages, standardmäßig 8 KB); Tabellenzeilen, B-Tree-Indexknoten, Metadaten usw. werden alle in diesen Seiten gespeichert
  • Um Plattenzugriffe zu reduzieren, cached PostgreSQL kürzlich gelesene Datenseiten in einem großen gemeinsam genutzten Speicherbereich, dem shared buffer pool
  • Wenn sich ein Client verbindet, wird ein dedizierter Backend-Prozess erzeugt. Fehlt eine Seite im Buffer Pool, muss sie von der Platte gelesen werden, und anschließend muss ein freier oder verdrängbarer Buffer gefunden werden
    • Die Funktion, die diese Buffer-Auswahl übernimmt, ist StrategyGetBuffer

Die Spinlocks von PostgreSQL

  • Ein Spinlock ist ein Sperrmechanismus, bei dem beim Warten auf ein Lock nicht geschlafen wird, sondern in einer Schleife ständig erneut geprüft wird
    • In sehr kurzen kritischen Abschnitten ist aktives Drehen effizienter, als Threads schlafen zu legen und wieder aufzuwecken
  • Die zentrale Annahme lautet: Der Thread, der das Lock hält, wird es sehr schnell wieder freigeben
  • StrategyGetBuffer verwendet zum Schutz der Buffer-Auswahl einen einzigen globalen Spinlock
    • In einer Umgebung mit 96 vCPUs und 1.024 Clients konkurrieren alle Backends um dasselbe Lock

Virtueller Speicher und TLB

  • Alle Prozesse verwenden virtuelle Speicheradressen, die die Hardware über Seitentabellen (mehrstufige Baumstruktur) in physische Adressen übersetzt
  • Da ein Durchlaufen der Seitentabellen bei jedem Zugriff langsam wäre, besitzt die CPU einen TLB (Translation Lookaside Buffer), der aktuelle Übersetzungen cached
    • Bei einem TLB-Hit ist der Zugriff schnell; bei einem TLB-Miss ist ein Page-Table-Walk nötig, was Zeit kostet
  • Linux verwendet das Prinzip der lazy allocation: Bei der Zuweisung von virtuellem Speicher werden reale physische Seiten erst beim ersten Zugriff gemappt
    • Beim ersten Zugriff tritt ein Minor Page Fault auf: Der Kernel weist eine physische Seite zu und speichert das Mapping, was einige Mikrosekunden langsamer ist als ein normaler Lese- oder Schreibzugriff

Das Problem mit 4-KB-Seiten

  • Im Benchmark war shared_buffers auf 120 GB gesetzt; bei 4-KB-Speicherseiten entspricht das etwa 31 Millionen Speicherseiten, also 31 Millionen potenziellen First-Touch-Page-Faults
  • In einem lang laufenden Benchmark mit einem 120-GB-shared-buffer-pool gelangen ständig neue Speicherbereiche in das Working Set, sodass Page Faults nicht nur beim Start, sondern dauerhaft auftreten
  • Wenn innerhalb von StrategyGetBuffer bei gehaltenem Spinlock auf Shared Memory zugegriffen wird und dieser Bereich noch nicht gemappt ist, tritt ein Minor Page Fault auf
  • PREEMPT_NONE (vor Linux 7.0): Selbst wenn Backend A in den Page-Fault-Handler eintritt, vermeidet es freiwillige Rescheduling-Punkte, sodass die Wahrscheinlichkeit gering ist, vor Behebung des Faults vom Scheduler entfernt zu werden. Die Wartezeit wird zwar länger als erwartet, der Schaden bleibt aber begrenzt
  • PREEMPT_LAZY (ab Linux 7.0): Der Scheduler kann Backend A innerhalb des Page-Fault-Handlers preempten und einen anderen Prozess einplanen. Selbst wenn die Fault-Behandlung abgeschlossen ist, entsteht zusätzliche Wartezeit t, bis der Scheduler die Kontrolle zurückgibt
    • Diese zusätzliche Wartezeit ist nicht einfach nur t, sondern verstärkt sich zu Anzahl aller aktuell drehenden Backends × t an verschwendeter CPU-Zeit
    • In einer Umgebung mit 96 vCPUs und Hunderten von Backends ist dieser Multiplikatoreffekt fatal; dadurch werden letztlich 56 % der CPU in s_lock verbraucht

Die Lösung über Huge Pages

  • Bei shared_buffers von 120 GB sinkt die Zahl potenzieller Page Faults drastisch, wenn die Speicherseitengröße geändert wird
    • 4-KB-Seiten: ~31.000.000 potenzielle Page Faults
    • 2-MB-Huge-Pages: ~61.440
    • 1-GB-Huge-Pages: ~120
  • Größere Seiten reduzieren nicht nur die Zahl der Page Faults, sondern entspannen auch den TLB-Druck: Derselbe Speicher wird mit weit weniger TLB-Einträgen abgedeckt, wodurch TLB-Misses und Page-Table-Walks sinken
  • Dadurch erzeugt StrategyGetBuffer beim Halten des Locks keine Faults mehr; der Lock-Inhaber wird schnell fertig, und andere Backends warten statt Millisekunden nur noch Mikrosekunden. Die Regression verschwindet
  • In PostgreSQL wird die Einstellung für Huge Pages über den Parameter huge_pages gesteuert
    • Unterstützt werden die drei Werte off, on, try (Standard)
    • try verwendet Huge Pages, wenn möglich, und fällt sonst still auf 4 KB zurück, wodurch das Risiko besteht, eine Fehlkonfiguration nicht zu bemerken
    • Wird on gesetzt, startet PostgreSQL nicht, wenn Huge Pages nicht verwendet werden können, sodass das Problem sofort sichtbar wird
  • Trade-off: Huge Pages werden vorab allokiert und reserviert; auch wenn PostgreSQL sie nicht vollständig nutzt, steht dieser Speicher dem restlichen System nicht mehr zur Verfügung. Werden Seiten nur teilweise genutzt, geht der Rest verloren. In Produktionsumgebungen mit großem shared_buffers ist dieser Trade-off meist dennoch vertretbar

Wie es weitergeht

  • Peter Zijlstra, Intel-Kernel-Ingenieur und Mitgestalter der Preemption-Änderung, schlug vor, dass PostgreSQL Restartable Sequences (rseq) übernehmen sollte
    • rseq ist eine Linux-Kernel-Funktion, mit der Userspace-Code erkennen kann, ob während eines kritischen Abschnitts eine Preemption oder Migration stattgefunden hat, und den Abschnitt dann neu starten kann
    • Wenn rseq auf den Spinlock-Pfad von PostgreSQL angewendet wird, ließe sich das Szenario vermeiden, in dem ein preempteter Lock-Inhaber alle wartenden Backends verzögert
  • Die Reaktion der PostgreSQL-Community fiel negativ aus
    • Es sei schwer akzeptabel, zum Wiedererlangen von Performance, die es vor Linux 7.0 kostenlos gab, eigens eine zusätzliche Kernel-Funktion übernehmen zu müssen
    • Aus ihrer Sicht verstößt dies gegen das langjährige Kernel-Prinzip, „Userspace nicht kaputtzumachen“ (Software, die vor einem Kernel-Upgrade korrekt funktionierte, sollte auch danach korrekt funktionieren)

1 Kommentare

 
cafedead 37 분 전

„Brich den Userspace nicht“ vs. „Verwende keine Spinlocks im Userspace“