1 Punkte von GN⁺ 4 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • 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_reuseport an 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.4xlarge erreichte 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_reuseport und 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_reuseport als 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_reuseport kann 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_conn und max_db_connections werden 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 ab
    • FATAL: 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: pgbench auf einem dritten Server
    • Workload: read-only (select-only), Transaction-Pooling-Modus
  • 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

 
GN⁺ 4 시간 전
Hacker-News-Kommentare
  • Man kann https://github.com/yandex/odyssey verwenden. Odyssey ist ein skalierbarer PgBouncer

    • Wir haben zunächst PgBouncer, die am besten erprobte und natürlich zu PostgreSQL passende Lösung, richtig abgestimmt und eingesetzt; auch lange bestehende Probleme wie die Unterstützung vorbereiteter Anweisungen wurden gelöst, sodass es sehr gut funktioniert
      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
    • Eine damit verbundene interessante Tatsache: Auch ClickHouse wurde ursprünglich von Yandex entwickelt
    • Yandex ist ein russisches Unternehmen, daher würde ich es eher nicht verwenden
  • 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

    • Ich frage mich, welchen Einfluss ein zusätzlicher Netzwerk-Hop von PgBouncer zu PostgreSQL auf den Durchsatz hat. Ich würde erwarten, dass der Unterschied gering ist, aber es wäre einen Test wert
  • 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_reuseport Client-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?

  • Ich nutze pgdog, und es passt sehr gut zu dem, wofür ich es brauche

    • Pgdog wurde entwickelt, um die Schwächen von PgBouncer zu beheben, auf die wir vor langer Zeit bei Instacart gestoßen sind, und um eine solidere Grundlage für horizontale Skalierung von PostgreSQL zu bieten, nämlich Sharding
    • Wir werden künftig auch ein pgdog-Angebot prüfen. Warum wir PgBouncer gewählt haben, ist unter https://news.ycombinator.com/item?id=48873867 ausführlicher beschrieben
    • In letzter Zeit werden abwechselnd PostgreSQL-Reverse-Proxy-Projekte vorgestellt. Warum Pgdog entwickelt wurde, sieht man hier: https://news.ycombinator.com/item?id=48819308
  • 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?

    • Query-Abbrüche werden getrennt von der bestehenden Query-Verbindung übermittelt, daher muss PgBouncer die Anfrage an den richtigen Server und die richtige Verbindung senden
      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

    • Wenn pro Host die Backend-Anwendung als ein großes Binary läuft, stimmt das. Wenn das Backend aber aus vielen Fork-Prozessen besteht, die keinen Zustand teilen, braucht man es eindeutig
  • PgBouncer ist großartige Software. Wir nutzen es intensiv, und es macht den Datenbankbetrieb deutlich einfacher

  • Ich habe so_reuseport gerade 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?

    • Es hat sich deutlich verbessert und kommt inzwischen auch mit Tausenden Verbindungen ziemlich gut zurecht: https://techcommunity.microsoft.com/blog/adforpostgresql/imp...
      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
    • Damals lieferten mehrere Unternehmen zum ersten Mal brauchbare Antworten auf das von Java vorgegebene Threading-Modell. Es war im Grunde das Windows-Modell mit zusätzlichen Funktionen; selbst Solaris hatte Schwierigkeiten mit Java-Workloads, HP-UX noch mehr, und ich weiß nicht mehr, ob SGI besser als HP war
      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