Wie PgBouncer den Durchsatz um das Vierfache gesteigert hat
(clickhouse.com)- ClickHouse Managed Postgres betreibt standardmäßig eine Multi-Prozess-Flotte, deren Größe proportional zur Anzahl der Kerne ist, um die Einschränkung zu vermeiden, dass der Single-Thread-PgBouncer an einen CPU-Kern gebunden ist
- Alle Prozesse binden sich mit
so_reuseportan denselben Port, und der Kernel verteilt neue Verbindungen, sodass es für Clients wie ein einzelner Endpoint aussieht - Postgres-Cancel-Requests, die über separate Verbindungen eingehen, können bei einem Prozess landen, der die Session nicht besitzt; per Prozess-Peering werden sie an den tatsächlichen Session-Eigentümer weitergeleitet
- Auf einer 16-vCPU-Instanz
c7i.4xlargeerreichte ein einzelner Prozess seinen Peak bei etwa 87.000 TPS, während eine Flotte aus 16 Prozessen auf etwa 336.000 TPS kam und damit rund den vierfachen Durchsatz erzielte - Bei wenigen Verbindungen kann ein einzelner Prozess ähnlich schnell oder etwas schneller sein; bei hoher Nebenläufigkeit wird jedoch ein Kern zum Bottleneck. Daher sollte das Verbindungsbudget über die gesamte Flotte verteilt werden, um Durchsatz und Verbindungslimits ohne Überlastung von Postgres durch zu viele Verbindungen zu skalieren
Der CPU-Bottleneck durch einen einzelnen Prozess
- PgBouncer ist single-threaded, daher nutzt ein einzelner Prozess unabhängig von der CPU-Anzahl des Systems nur einen Kern
- Selbst auf einem 16-vCPU-Server konzentriert sich das Connection-Pooling auf einen Kern, während die übrigen 15 Kerne idle bleiben
- Dadurch begrenzt der Pooler den Gesamtdurchsatz, bevor die Verarbeitungskapazität von Postgres ausgeschöpft ist
- ClickHouse Managed Postgres betreibt eine Prozess-Flotte, bei der die Anzahl der PgBouncer-Prozesse proportional zur Anzahl der verfügbaren Kerne festgelegt wird
Verbindungsverteilung mit so_reuseport
- Alle Prozesse der Flotte aktivieren
so_reuseportund binden sich an denselben Port - Der Kernel verteilt eingehende Verbindungen auf die einzelnen Prozesse, sodass Clients sich mit einem Endpoint verbinden und nicht wissen müssen, dass dahinter mehrere PgBouncer-Prozesse arbeiten
- Auch die PgBouncer-Dokumentation empfiehlt
so_reuseportals Methode, um mit mehreren single-threaded Prozessen mehrere Kerne zu nutzen
Prozess-Peering zur Sicherstellung von Query-Cancels
- Cancel-Requests von Postgres kommen zusammen mit einem Cancel-Key über eine neue Verbindung herein, die von der Verbindung der laufenden Query getrennt ist
- Bei Verwendung von
so_reuseportkann der Kernel die neue Cancel-Verbindung an einen anderen Prozess weiterleiten als den, der die betreffende Session besitzt- Wenn der empfangende Prozess die Query nicht kennt, wird der Cancel nicht ausgeführt
- Durch Peering zwischen PgBouncer-Prozessen können Requests, die beim falschen Prozess landen, an den Prozess weitergegeben werden, der die Session tatsächlich besitzt
- Dadurch funktionieren Query-Cancels über die gesamte Flotte hinweg korrekt, unabhängig davon, welcher Prozess den Request entgegennimmt
Transaction-Pooling und Verbindungsbudget
- Das Pooling läuft im Transaction-Modus; sobald eine Transaktion committet wird, wird die Serververbindung in den Pool zurückgegeben
max_client_connundmax_db_connectionswerden durch die Anzahl der Prozesse geteilt und jedem Prozess zugewiesen- Durch das Aufteilen des Verbindungsbudgets lässt sich das aggregierte Verbindungslimit erhöhen, während die Gesamtzahl der Verbindungen der Flotte innerhalb sicherer Postgres-Grenzen bleibt
- Ein einzelner Prozess lehnt neue Clients, die sein eigenes
max_client_connüberschreiten, mit folgendem Fehler abFATAL: no more connections allowed (max_client_conn)
Benchmark auf identischer Hardware
- Beide Konfigurationen wurden in derselben AWS-EC2-Umgebung gemessen
- Pooler-Server: 16-vCPU
c7i.4xlarge - Postgres: separater Server
- Load-Generator:
pgbenchauf einem dritten Server - Workload: read-only (select-only), Transaction-Pooling-Modus
- Pooler-Server: 16-vCPU
- Verglichen wurden ein einzelner PgBouncer-Prozess und eine Flotte aus 16 Prozessen; Instanztyp, Postgres und Workload waren identisch
- Die Client-Verbindungen wurden von 8 auf 256 erhöht; dabei wurden Durchsatz und CPU-Auslastung des 16-Kern-Servers gemessen
| Clients | TPS einzelner Prozess | Server-CPU einzelner Prozess | Flotten-TPS | Server-CPU Flotte |
|---|---|---|---|---|
| 8 | 8,910 | 0.8% | 6,450 | 2.9% |
| 32 | 54,203 | 5.2% | 64,244 | 12.3% |
| 64 | 86,570 | 8.3% | 219,439 | 31.9% |
| 128 | 83,463 | 8.1% | 320,547 | 45.9% |
| 256 | 76,893 | 7.7% | 336,469 | 48.9% |
Ergebnisse bei Durchsatz und CPU-Auslastung
- Der einzelne Prozess erreichte seinen Peak bei etwa 87.000 TPS und fiel bei steigender Last zurück; bei 256 Clients lag er bei rund 77.000 TPS
- Die Flotte aus 16 Prozessen nutzte mehr Kerne und stieg auf etwa 336.000 TPS, was gegenüber dem einzelnen Prozess ungefähr dem vierfachen Durchsatz entspricht
- In
pidstat-Messungen nutzte ein einzelner PgBouncer-Prozess unter Last mit rund 97% CPU einen Kern nahezu vollständig, während die Gesamtauslastung des 16-vCPU-Servers unter 10% blieb - Die Flotte verteilte die Arbeit über den Server und nutzte etwa 8 Kerne; selbst als Postgres und der Load-Generator an ihre Grenzen kamen, blieb zusätzliche Kapazität übrig
- In einer Messung mit konstant 256 Clients nutzte der Server mit Einzelprozess dauerhaft etwa 9% CPU, der Flotten-Server etwa 52% CPU
- Bei externer Messung über EC2 CloudWatch lag die durchschnittliche CPUUtilization der Instanz mit Einzelprozess bei etwa 16%, die der Flotte bei etwa 60%
- Die CloudWatch-Werte liegen etwas höher als die Messungen innerhalb des Guests, aber der Unterschied bleibt derselbe: Ein einzelner PgBouncer kann den Großteil der 16 vCPUs nicht nutzen
Wahl der Konfiguration je nach Nebenläufigkeit
- Bei niedriger Verbindungszahl gibt es keine Arbeit zu parallelisieren, und die Verbindungen der Flotte werden über mehrere Prozesse verteilt; daher kann ein einzelner Prozess ausreichen oder etwas schneller sein
- Bei 8 Clients erreichte der einzelne Prozess 8.910 TPS, die Flotte 6.450 TPS
- Wenn die Nebenläufigkeit steigt, wird der eine vom Einzelprozess belegte Kern zur Durchsatzgrenze, und der Performance-Abstand zur Flotte wächst
- In Umgebungen, in denen Postgres vor PgBouncer an seine Grenzen stößt, ist auch ein einzelner Prozess ein sinnvoller Default
- Wenn der Pooler beginnt, den Durchsatz zu begrenzen, müssen eine an der Kernanzahl ausgerichtete Prozess-Flotte,
so_reuseport-basiertes Port-Sharing und Prozess-Peering zusammen eingesetzt werden, um den Bottleneck zu beseitigen - Alle ClickHouse Managed Postgres-Server bieten diese Konfiguration standardmäßig
1 Kommentare
Hacker-News-Kommentare
Man kann https://github.com/yandex/odyssey verwenden. Odyssey ist ein skalierbarer PgBouncer
Es gibt viele Kunden, die PostgreSQL-Verbindungen auf über 10.000 skaliert haben, und künftig werden wir auch Alternativen wie Odyssey oder pgdog prüfen. Persönlich halte ich allerdings nicht viel davon, mehr als 10.000 Verbindungen zu PostgreSQL zu haben; ich denke, dass man auch mit einigen Hundert ausreichend skalieren kann
Wenn man PgBouncer in Kubernetes betreibt, ist es einfach, mehrere Prozesse auf einer Maschine zu starten, und ebenso einfach, sie über mehrere Maschinen zu verteilen
Das ist besonders nützlich bei einer Multi-Maschinen-Konfiguration, weil Azure bei VM-Wartungen gerne nacheinander Ausfälle über eine ganze Serverflotte verursacht
Das Konzept des hier erwähnten Peerings verstehe ich, habe es aber noch nie mit PostgreSQL verwendet
A) Gibt es in PostgreSQL einen Modus oder eine Einstellung, mit der sich das einfach konfigurieren lässt? Ich stelle mir vor, dass Abbruchanforderungen nacheinander an Peers weitergereicht werden, bis kein Fehler mehr auftritt, oder dass die Abbruchanforderung Metadaten enthält, mit denen ein fälschlich angesprochener Prozess den richtigen Prozess identifizieren kann
B) Wenn alle PostgreSQL-Prozesse mit
so_reuseportClient-Anfragen annehmen, scheint für die Kommunikation zwischen Peers ein separates Mittel zur Interprozesskommunikation (IPC) nötig zu sein. Welcher Mechanismus wird dafür tatsächlich verwendet?Wenn man die PgBouncer-Prozess-ID in den Abbruchschlüssel aufnimmt, ließe sich das wohl relativ einfach umsetzen
Siehe ab Folie 26: https://www.pgevents.ca/events/pgconfdev2024/sessions/sessio...
Es gibt auch ein Video des Vortrags von Jelte, dem Maintainer von PgBouncer: https://www.youtube.com/watch?v=X-nCHcZ6vQU
Ich nutze pgdog, und es passt sehr gut zu dem, wofür ich es brauche
Ich frage mich, warum PgBouncer sich überhaupt um Abbruchanforderungen kümmern muss. Könnte man sie nicht einfach an PostgreSQL weiterleiten, und wenn PostgreSQL für die abgebrochene Query statt einer normalen Antwort einen Fehler zurückgibt, behandelt das der PgBouncer, dem diese Verbindung gehört?
PgBouncer gibt dem Client eine gefälschte, aber nachverfolgbare Abbruch-PID und ein Secret; wenn die Abbruchanforderung zurückkommt, sucht es die PID und das Secret des tatsächlichen Servers/Prozesses heraus und leitet sie weiter. Außerdem kann es Situationen geben, in denen der Client glaubt, die Query laufe noch, PgBouncer aber weiß, dass sie bereits beendet ist und die Verbindung wiederverwendet wurde; daher muss geprüft werden, ob diese Verbindung und PID weiterhin genau diese Query ausführen
Geht es darum, in Microservices den Zugriff auf den PostgreSQL-Server über einen Connection Pool zu kontrollieren? Bei einem monolithischen Backend scheint das nicht nötig zu sein
Die meisten brauchbaren Backend-Frameworks haben Connection Pooling eingebaut, und für 98 % der Anwendungsfälle, in denen Microservices weder nötig noch empfehlenswert sind, reicht das aus
PgBouncer ist großartige Software. Wir nutzen es intensiv, und es macht den Datenbankbetrieb deutlich einfacher
Ich habe
so_reuseportgerade erst kennengelernt; interessant. Der Kern der Konfiguration scheint das zusammen mit Peering zu sein. Ist Peering in PgBouncer eingebaut und einfach zu konfigurieren?Kann man Peering auch in Kubernetes verwenden? In dieser Umgebung müsste man den Port wohl nicht wiederverwenden; ich frage mich auch, ob dann jeder Pod einen eigenen Connection Pool hat und unabhängig arbeitet
Ich bin jetzt 46, und ich erinnere mich noch, wie schockiert ich mit 23 über das schwere Verbindungsmodell von PostgreSQL war. Hat sich das seitdem nicht verbessert?
Allerdings muss PostgreSQL ohne Connection Pool für neue Verbindungen Prozesse forken, daher kostet der Aufbau einer neuen Verbindung immer Dutzende Millisekunden oder mehr. Anwendungen, die ohne Connection Pool geschrieben sind, sind zwar nicht ideal, aber weit verbreitet
Auch Anwendungs-Frameworks haben sich verändert, und Serverless-Architekturen können Zehntausende Verbindungen erzeugen, bei denen PostgreSQL anfängt, Probleme zu bekommen. Persönlich mag ich mehr als ein paar Hundert Verbindungen nicht, aber heutzutage ist das durchaus ein realistisches Szenario
Um Java-Kompatibilität zu erreichen, mussten viele Unternehmen ihre Art der Nebenläufigkeitsverarbeitung erheblich ändern. PostgreSQL und SQLite wurden etwa zur gleichen Zeit entworfen, aber sie wurden von Branchenveteranen gebaut, die schon vor diesen Threading-Debatten Hochlastsysteme ausgeliefert hatten und dabei auch Nutzer älterer Hardware unterstützen mussten