SSD Nodes Learn 🎉 VPS ab $5.50/Monat
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-08-21

Postgres-Connection-Pooling auf einem VPS

Ein 4-GB-VPS läuft wegen einzelner Postgres-Prozesse vor max_connections aus. Erfahren Sie, was ein Pooler behebt und welche Probleme er verursacht.

Warum ein kleiner VPS vor max_connections keinen Arbeitsspeicher mehr hat

Connection-Pooling für Postgres ist auf einem VPS kein Geschwindigkeits-Trick. Es hält einen Server mit 4 GB Arbeitsspeicher am Laufen, weil jede PostgreSQL-Verbindung ein eigener Betriebssystemprozess mit eigenem privatem Arbeitsspeicher ist. Ein Pooler stellt einer großen und kostengünstigen Anzahl von Client-Verbindungen eine kleine, feste Anzahl echter Backend-Prozesse gegenüber.

Der Standardwert von max_connections ist 100. Das ist ein Limit, kein Budget. PostgreSQL prüft nicht, ob der Rechner tatsächlich 100 Backends mit echten Abfragen gleichzeitig ausführen kann. Deshalb fällt der Rechner zuerst aus. Der Out-of-Memory-(OOM)-Killer des Kernels wählt einen Prozess aus. Wenn er ein Backend auswählt, startet PostgreSQL den gesamten Cluster neu, damit der Shared Memory wieder sicher verwendet werden kann. Im Log erscheint server process (PID 1234) was terminated by signal 9: Killed, danach terminating any other active server processes. Jede offene Verbindung wird beendet, auch die weiterhin funktionsfähigen.

Der Rechner hat keinen Arbeitsspeicher mehr, weil jede Verbindung ein Prozess ist und weil work_mem pro Sortier- oder Hash-Operation und nicht pro Verbindung zugewiesen wird. Beides vervielfacht den Speicherbedarf.

Jede Verbindung ist ein Prozess, und jeder Prozess benötigt Speicher

PostgreSQL verwendet einen Prozess pro Verbindung. Der Postmaster erstellt beim Verbindungsaufbau durch einen Client einen Backend-Prozess. Dieser Backend-Prozess bleibt bestehen, bis der Client die Verbindung trennt. Es handelt sich nicht um einen Thread. Der Prozess verfügt über eigene Seitentabellen, eigene Katalog-Caches und eigene zwischengespeicherte Abfragepläne. Diese Caches wachsen, wenn die Verbindung auf weitere Tabellen zugreift und mehr unterschiedliche Abfragen ausführt. Deshalb benötigt eine langlebige Verbindung in einer ausgelasteten ORM-Anwendung mehr Speicher als eine neue Verbindung.

Der Shared Memory wird tatsächlich gemeinsam genutzt. shared_buffers ist eine einzige Speicherzuweisung für den gesamten Cluster, die in jedes Backend eingeblendet wird. Privater Speicher wird nicht gemeinsam genutzt. Deshalb führt top hier in die Irre: Die Resident Set Size (RSS) eines Backends enthält die gemeinsam genutzten Seiten, auf die dieses Backend zugegriffen hat. Wenn Sie die RSS von 50 Backends addieren, zählen Sie shared_buffers 50-mal.

Messen Sie stattdessen den privaten Anteil. Die PSS (Proportional Set Size) teilt jede gemeinsam genutzte Seite durch die Anzahl der Prozesse, die sie einblenden. Die USS (Unique Set Size) zählt nur die Seiten, die ausschließlich zu diesem Prozess gehören.

sudo apt update
sudo apt install -y smem
sudo smem -k -P '^postgres'

Die Spalte USS enthält den Speicher, der freigegeben würde, wenn dieses Backend beendet wird. Das sind die tatsächlichen Kosten pro Verbindung. Veröffentlichte Werte liegen für ein inaktives Backend meist im einstelligen Megabyte-Bereich. Bei einem Backend, das umfangreiche ORM-Abfragen ausgeführt hat, können sie ein Mehrfaches davon betragen. Betrachten Sie diese Werte als typische veröffentlichte Angaben, nicht als Werte für Ihr System. Für die Planung ist nur der Wert relevant, den Sie auf Ihrem System unter Ihrer tatsächlichen Arbeitslast messen.

Eine sitzungsbezogene Speicherzuweisung wird leicht übersehen. temp_buffers hat standardmäßig den Wert 8MB und wird pro Sitzung zugewiesen, sobald diese Sitzung erstmals auf eine temporäre Tabelle zugreift. Der Speicher wird erst freigegeben, wenn die Sitzung endet.

work_mem wird pro Operation gewährt, nicht pro Verbindung

Hier versagt die Arithmetik. work_mem ist standardmäßig auf 4MB gesetzt. Die PostgreSQL-Dokumentation beschreibt die Auswirkung eindeutig: „Eine komplexe Abfrage kann gleichzeitig mehrere Sortier- und Hash-Operationen ausführen. Jede dieser Operationen darf in der Regel so viel Speicher verwenden, wie dieser Wert vorgibt, bevor sie Daten in temporäre Dateien schreibt.“ Ein Ausführungsplan mit drei Sortierknoten kann innerhalb eines Backends gleichzeitig das Dreifache von work_mem verwenden.

Hash-Operationen erhalten mehr Speicher. hash_mem_multiplier ist standardmäßig auf 2.0 gesetzt. Ein Hash-Join oder eine Hash-Aggregation kann daher work_mem mal zwei verwenden, also bei den Standardeinstellungen 8MB. Parallele Abfragen vervielfachen den Bedarf erneut, weil jeder parallele Worker ein weiterer Prozess mit einem eigenen Limit ist.

Rechnen Sie das für eine 4-GB-VPS durch. Setzen Sie shared_buffers auf 1 GB, lassen Sie work_mem auf 4MB und führen Sie mit 100 Verbindungen jeweils eine Abfrage mit zwei Hash-Knoten aus. Das ergibt 100 mal 16MB, also 1.6 GB privaten Speicher zusätzlich zu 1 GB Shared Buffers, noch ohne Page Cache und ohne alle anderen Prozesse auf dem System. Erhöhen Sie work_mem nun auf 64MB, weil der Server noch freien RAM hat. Dieselben 100 Verbindungen ergeben dann 100 mal 256MB. Es gibt keine Warnung. Sie bemerken das Problem erst, wenn der OOM-Killer eingreift.

Sie können prüfen, ob work_mem zu klein ist, statt den Wert zu schätzen. Setzen Sie log_temp_files = 0 in postgresql.conf und laden Sie die Konfiguration neu. Jeder Spill auf die Festplatte schreibt dann eine Zeile mit dem Namen und der Größe der Datei, zum Beispiel temporary file: path "base/pgsql_tmp/pgsql_tmp1234.0", size 20971520. Häufige Spills bedeuten, dass ein höherer Wert für work_mem hilfreich wäre. Wenn keine Spills auftreten, bringt eine Erhöhung nichts und verbraucht Speicher, den Sie nicht haben.

Die Pool-Arithmetik, die tatsächlich Probleme verursacht

Niemand plant von vornherein 240 Verbindungen. Sie konfigurieren einen Pool mit 20 Verbindungen und führen die Anwendung dann an mehreren Stellen aus.

ChartBackends requested per app topology, pool size 20 per worker (arithmetic)
The data behind this chart
[
  {
    "config": "1 worker",
    "backends": 20
  },
  {
    "config": "4 web workers",
    "backends": 80
  },
  {
    "config": "4 web + 2 background",
    "backends": 120
  },
  {
    "config": "2 hosts x 4 workers",
    "backends": 160
  },
  {
    "config": "3 hosts x 4 workers",
    "backends": 240
  }
]

Vier Gunicorn-Worker mit jeweils einem Pool von 20 Verbindungen fordern 80 Backends an. Mit zwei Workern für Hintergrundaufgaben sind es 120. Erhöhen Sie die Anzahl auf 3 hosts x 4 workers, fordert die Anwendung 240 Backends gegenüber einem max_connections von 100 an. Keine dieser 5 Konfigurationen ist an einer einzelnen Stelle falsch konfiguriert. Der Pool gilt pro Prozess. Kein Teil der Anwendung kann die Gesamtzahl sehen.

Auch die Standardwerte der Bibliotheken wirken in dieselbe Richtung. SQLAlchemys QueuePool ist standardmäßig auf pool_size=5 mit max_overflow=10 gesetzt, also auf 15 Verbindungen pro Prozess. HikariCP verwendet standardmäßig 10. Django hatte vor 5.1 keinen integrierten Pool und verwendete eine Verbindung pro Worker-Prozess. Deshalb tritt dieses Problem bei Django-Anwendungen später auf und dann auf einmal, wenn jemand CONN_MAX_AGE setzt oder die neuere Option "pool": True aktiviert. Wenn Sie eine Django-Anwendung hinter Gunicorn und nginx betreiben, müssen Sie mit der Anzahl Ihrer Gunicorn-Worker multiplizieren, nicht mit der Anzahl Ihrer Server.

Was Connection Pooling für Postgres auf einem VPS tatsächlich verändert

Ein Pooler ist ein Prozess, der auf einer Seite mit Ihrer Anwendung über das PostgreSQL-Wire-Protokoll kommuniziert und auf der anderen Seite eine kleine Anzahl echter Serververbindungen hält. Er beschleunigt keine Abfragen. Er verändert, wer die Kosten für eine Verbindung trägt und wie viele echte Backends existieren.

Zwei Dinge verbessern sich. Der Verbindungsaufbau kostet keinen fork-Prozess und keine Katalogabfragen mehr, die den leeren Backend-Cache füllen, weil der Pooler die Verbindung des Clients selbst beantwortet. Wichtiger ist jedoch, dass die Anzahl der echten Backends nicht mehr der Anzahl der Anwendungsverbindungen entspricht. Dadurch können sich 500 Clients 20 Backends teilen.

Das Warten ist die eigentliche Funktion. Genau diesen Punkt akzeptieren viele nur ungern. Ohne Pooler erhält jede von 500 gleichzeitig ausgeführten Abfragen ein Backend und alle laufen gleichzeitig auf zwei CPU-Kernen. Dadurch wird jede Abfrage langsam, und der gesamte Speicher wird im selben Moment beansprucht. Mit einem Pooler laufen 20 Abfragen, während die übrigen einige Millisekunden warten. Dadurch erhält jede laufende Abfrage einen angemessenen Anteil der CPU und wird schneller abgeschlossen. Eine Warteschlange vor einem kleinen Pool ist besser als keine Warteschlange vor einem großen Pool.

Ein Pooler begrenzt jedoch nichts anderes auf dem Rechner. Wenn Postgres den VPS mit einem Anwendungsserver oder mit einer Vektordatenbank auf demselben VPS gemeinsam nutzt, schützt der Pooler Postgres nur vor Ihrer Anwendung. Mehr leistet er nicht. Begrenzen Sie daher auch die Nachbarn mit einem festen Limit: Sie können mit systemd den Speicher und die CPU begrenzen, die ein Dienst verwenden darf, damit ein außer Kontrolle geratener Prozess die Datenbank nicht ebenfalls beendet. Wo die Datenbank selbst läuft, beeinflusst die Konfiguration dieser Limits. Das ist der praktische Unterschied zwischen Postgres in Docker oder direkt auf dem Host auszuführen.

Sitzungspooling gegenüber Transaktionspooling

Eine Einstellung entscheidet über alles Weitere: pool_mode.

Beim Sitzungspooling wird eine Serververbindung für die gesamte Lebensdauer der Clientverbindung einem Client zugewiesen. Sie wird freigegeben, sobald der Client die Verbindung trennt. Alles funktioniert, weil der Pooler nur als einfacher Proxy arbeitet. Sie sparen den Aufwand für den Verbindungsaufbau, aber sonst nichts. Wenn die Anwendung 200 Verbindungen öffnet, benötigen Sie weiterhin 200 Backends.

Beim Transaktionspooling wird eine Serververbindung einem Client nur für die Dauer einer Transaktion zugewiesen. Bei COMMIT oder ROLLBACK wird sie an den Pool zurückgegeben, und der nächste wartende Client erhält sie. Dadurch werden aus 500 Clients 20 Backends. Genau das führt jedoch zu Problemen, und zwar beabsichtigt: Ihre nächste Anweisung kann auf einem anderen Backend ausgeführt werden als die vorherige.

Der Standardwert von PgBouncer ist pool_mode = session. Wenn Sie PgBouncer installieren und nichts ändern, erhalten Sie nur die günstige Hälfte ohne den eigentlichen Vorteil. Ein dritter Modus, statement, gibt die Verbindung nach jeder einzelnen Anweisung zurück und lehnt Transaktionen mit mehreren Anweisungen ab. Lassen Sie diese Einstellung unverändert, sofern Sie nicht genau wissen, warum Sie sie benötigen.

Welcher Transaktionsmodus Probleme verursacht und warum

Alle folgenden Funktionen schlagen aus demselben Grund fehl. Ihr Zustand liegt innerhalb eines einzelnen Backends, und Transaction Pooling garantiert nicht, dass Sie zweimal dasselbe Backend erhalten.

  • SET und RESET auf Sitzungsebene. SET search_path, SET statement_timeout, SET TIME ZONE und SET ROLE werden auf dem Backend ausgeführt, das die jeweilige Anweisung bedient, und sind in Ihrer nächsten Transaktion nicht mehr vorhanden. Verwenden Sie SET LOCAL innerhalb einer expliziten Transaktion. Diese Einstellung gilt nur für die betreffende Transaktion und ist daher sicher.
  • LISTEN. Benachrichtigungen werden von dem Backend zugestellt, auf dem LISTEN ausgeführt wurde. Dieses Backend wird unmittelbar nach Ende der Transaktion einem anderen Client zugewiesen. NOTIFY funktioniert im Transaktionsmodus weiterhin. Das macht diesen Fehler schwer erkennbar: Das Senden ist erfolgreich, aber der Empfang erfolgt nie. Wenn Sie LISTEN benötigen, öffnen Sie eine zusätzliche Verbindung direkt zu Port 5432 und umgehen Sie den Pooler.
  • Advisory Locks auf Sitzungsebene. pg_advisory_lock() wird von der Sitzung gehalten und beim Ende der Sitzung freigegeben. Beim Transaction Pooling wird Ihr Aufruf zum Aufheben der Sperre auf einem anderen Backend ausgeführt. Die Sperre bleibt daher bestehen, bis PgBouncer diese Serververbindung aus dem Pool entfernt. Standardmäßig geschieht das nach server_lifetime, also nach einer Stunde. Verwenden Sie pg_advisory_xact_lock(). Diese Sperre wird am Ende der Transaktion von demselben Backend freigegeben, das sie gesetzt hat.
  • PREPARE und DEALLOCATE, die SQL-Anweisungen. Im Transaktionsmodus nie verfügbar.
  • WITH HOLD-Cursor sowie alle serverseitigen Cursor, die über das Ende ihrer Transaktion hinaus bestehen sollen.
  • Temporäre Tabellen, die einen Commit überdauern sollen. CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWS legt die Tabelle im temporären Schema eines Backends an. Ihre nächste Transaktion kann jedoch auf einem anderen Backend laufen.
  • LOAD.

Protokollbasierte vorbereitete Anweisungen sind die eine Ausnahme. PgBouncer 1.21.0 fügte dafür Unterstützung im Transaktionsmodus hinzu. Ab Version 1.24.0 ist sie standardmäßig aktiviert, indem max_prepared_statements auf 200 gesetzt wird. Ältere Builds lassen diesen Wert auf 0, wodurch die Funktion deaktiviert ist. Ubuntu 24.04 wird mit PgBouncer 1.22.0 ausgeliefert. Die Funktion ist daher vorhanden, aber Sie müssen max_prepared_statements selbst setzen. Wenn Sie nicht sicher sind, wie sich Ihr Build verhält, ist die sichere Einstellung auf Clientseite: psycopg 3 verwendet keine serverseitig vorbereiteten Anweisungen mehr, wenn Sie prepare_threshold auf None setzen.

Django bezeichnet seine eigene Variante dafür ebenfalls. In der Dokumentation steht, dass „die Verwendung eines Connection Poolers im Transaction-Pooling-Modus (z. B. PgBouncer) das Deaktivieren serverseitiger Cursor für diese Verbindung erfordert“, weil „serverseitige Cursor nur über die Verbindung zugänglich sind, auf der sie erstellt wurden“. Setzen Sie DISABLE_SERVER_SIDE_CURSORS im Eintrag dieser Datenbank auf True. Andernfalls schlägt jeder Aufruf von .iterator() sporadisch fehl. Das Problem tritt nur unter Last auf.

Der Transaktionsmodus ist eine sinnvolle Option, setzt aber einen verbindlichen Vertrag voraus. Lesen Sie die Liste, prüfen Sie Ihr ORM und Ihre Bibliothek für Hintergrundaufgaben darauf und wechseln Sie anschließend.

PgBouncer installieren und die Anwendung darauf verweisen

Die folgende Konfiguration führen Sie auf Ihrem eigenen Server aus: Ubuntu 24.04 mit PostgreSQL, das bereits auf 127.0.0.1 an Port 5432 lauscht.

sudo apt update
sudo apt install -y pgbouncer
pgbouncer --version

Ubuntu 24.04 stellt PgBouncer 1.22.0 bereit. Upstream ist Stand August 2026 bei 1.25.2. Prüfen Sie, welche Version Sie verwenden, da das Verhalten vorbereiteter Anweisungen davon abhängt.

Erstellen Sie eine Rolle, deren einzige Aufgabe die Anmeldung an der PgBouncer-Administrationskonsole ist. Erstellen Sie anschließend die Passwortdatei. PgBouncer benötigt die SCRAM-Geheimnisse (salted challenge response authentication mechanism) aus pg_authid. Nur ein Superuser kann diese Tabelle lesen.

sudo -u postgres psql -c "CREATE ROLE pgb_admin LOGIN PASSWORD 'change-this'"
sudo -u postgres psql -At -c \
  'SELECT format($$"%s" "%s"$$, rolname, rolpassword) FROM pg_authid WHERE rolpassword IS NOT NULL' \
  > /tmp/userlist.txt
sudo install -o postgres -g postgres -m 640 /tmp/userlist.txt /etc/pgbouncer/userlist.txt
rm /tmp/userlist.txt

Durch das Kopieren der Geheimnisse statt einer erneuten Eingabe der Passwörter funktioniert dieses Verfahren. PgBouncer kann ein SCRAM-Geheimnis nur dann für die Anmeldung bei PostgreSQL wiederverwenden, wenn der Client sich ebenfalls mit SCRAM authentifiziert hat, wenn das Geheimnis in der Datei Byte für Byte dem Eintrag in pg_authid entspricht (gleicher Salt und gleiche Anzahl von Iterationen, nicht nur dasselbe Passwort) und wenn die Zeile [databases] kein user= festlegt. Fügen Sie dieser Zeile user=appuser hinzu, benötigt PgBouncer stattdessen ein Klartextpasswort. Prüfen Sie mit systemctl show pgbouncer -p User, ob der Eigentümer der Datei dem Konto entspricht, unter dem der Dienst ausgeführt wird. Wenn Sie ein Passwort in PostgreSQL ändern, müssen Sie diese Datei neu erzeugen. Andernfalls liefert die nächste Verbindung password authentication failed.

Schreiben Sie nun /etc/pgbouncer/pgbouncer.ini.

[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgb_admin
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
max_db_connections = 80
max_prepared_statements = 200
ignore_startup_parameters = extra_float_digits

listen_addr = 127.0.0.1 hält den Pooler vom öffentlichen Internet fern. Das ist wichtig, weil ein von außen erreichbarer Pooler einen Authentifizierungsendpunkt darstellt, den Sie nicht veröffentlichen wollten. max_client_conn gibt an, wie viele Anwendungsverbindungen PgBouncer akzeptiert. Dieser Wert ist kostengünstig und kann daher hoch sein. default_pool_size gibt an, wie viele echte Backends ein Datenbank-Benutzer-Paar belegen darf. Dieser Wert ist kostenintensiv. max_db_connections begrenzt die gesamte Datenbank auf 80 und lässt unter max_connections noch Platz für psql, Backups und Monitoring. ignore_startup_parameters = extra_float_digits verhindert, dass PgBouncer Treiber zurückweist, darunter den JDBC-Treiber, die diesen Parameter beim Verbindungsaufbau senden.

sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer --no-pager
sudo journalctl -u pgbouncer -n 20 --no-pager

Bei einem erfolgreichen Start wird eine Zeile protokolliert, die angibt, dass PgBouncer auf 127.0.0.1:6432 lauscht. Schlägt der Start wegen der Passwortdatei fehl, wird der Pfad protokolliert, den PgBouncer nicht lesen konnte. Fast immer liegt dann ein Problem mit Berechtigungen oder dem Eigentümer vor, nicht mit der Syntax. Ändern Sie anschließend die Verbindungszeichenfolge der Anwendung von Port 5432 auf Port 6432 und starten Sie die Anwendung neu. An der Anwendung selbst müssen Sie nichts weiter ändern.

So prüfen Sie, ob der Pool ordnungsgemäß arbeitet

PgBouncer verfügt über eine Administrationskonsole, die über eine virtuelle Datenbank namens pgbouncer erreichbar ist.

psql -h 127.0.0.1 -p 6432 -U pgb_admin pgbouncer
SHOW POOLS;
SHOW STATS;
SHOW CLIENTS;

SHOW POOLS ist der wichtigste Wert. cl_active bezeichnet die Clients, die derzeit an eine Serververbindung gebunden sind. cl_waiting bezeichnet die Clients, die auf eine Serververbindung warten. sv_active und sv_idle geben an, wie viele tatsächliche Backends verwendet werden beziehungsweise frei sind. maxwait gibt an, wie lange der Client an der Spitze der Warteschlange bereits wartet, in Sekunden. Unter normaler Last sollte cl_waiting den Wert 0 und maxwait ebenfalls den Wert 0 haben. Ein maxwait, das über ein oder zwei Sekunden steigt, bedeutet, dass der Pool zu klein ist oder die Abfragen zu langsam sind. Dafür sind unterschiedliche Maßnahmen erforderlich.

Prüfen Sie zunächst, welcher Fall vorliegt, bevor Sie default_pool_size erhöhen.

SELECT state, count(*) FROM pg_stat_activity
  WHERE backend_type = 'client backend' GROUP BY state;

Wenn sich die meisten Backends in idle in transaction befinden, ist die Poolgröße nicht das Problem. Die Anwendung öffnet eine Transaktion und führt anschließend darin eine langsame Operation aus, beispielsweise einen HTTP-Aufruf. Dadurch bleibt jedes Backend belegt, ohne eine Abfrage auszuführen. idle_in_transaction_session_timeout beendet solche Transaktionen, aber die eigentliche Lösung liegt im Anwendungscode. Wenn sich dagegen alle Backends in active befinden, ist der Pool tatsächlich ausgelastet. Dann sollten Sie die Abfragen mit EXPLAIN (ANALYZE, BUFFERS) untersuchen, bevor Sie weitere Verbindungen bereitstellen.

Als Ausgangspunkt für die Dimensionierung wird häufig die HikariCP-Heuristik genannt: ungefähr die doppelte Anzahl der CPU-Kerne plus eins. Bei einer VPS mit 2 Kernen ergibt das 5. Verwenden Sie diesen Wert als veröffentlichten Ausgangspunkt, setzen Sie default_pool_size in seiner Nähe und passen Sie ihn anhand von maxwait an. Kleine Pools wirken zunächst falsch, liefern aber normalerweise bessere Messwerte. Ein wartendes Backend verursacht keine Kosten, während ein laufendes Backend CPU-Zeit und Arbeitsspeicher benötigt und die Konkurrenz um Sperren erhöht.

Auswahl zwischen PgBouncer, PgDog und Pgpool-II

PgBouncer ist für den normalen Fall die richtige Wahl: ein PostgreSQL-Server, ein VPS und eine Anwendung, die mehr Verbindungen öffnet, als der Server verwalten kann. PgBouncer erfüllt eine einzige Aufgabe, seine Konfiguration besteht aus einer einzelnen INI-Datei, und Debian sowie Ubuntu stellen dafür Pakete bereit. Die Verarbeitung der Verbindungen läuft in einem einzelnen Thread. Für eine VPS-typische Auslastung reicht das aus. Erst auf deutlich größeren Systemen wird dies zum begrenzenden Faktor.

PgDog ist interessant, wenn die Routing-Entscheidung im selben Netzwerk-Hop wie das Pooling erfolgen soll. Das Projekt beschreibt PgDog als Proxy zur Skalierung von PostgreSQL. PgDog ist in Rust geschrieben und unterstützt Transaktions- und Session-Pooling sowie die Trennung von Lese- und Schreibzugriffen durch Parsen der Abfragen. Außerdem bietet es Sharding mit Multi-Shard-Routing und Two-Phase-Commit. Verwenden Sie PgDog, wenn Sie einen Primary und eine oder mehrere Replicas haben und Lesezugriffe an Replicas senden möchten, ohne der Anwendung die Existenz dieser Replicas bekannt zu machen. Dabei sind zwei Punkte zu beachten. PgDog steht unter der AGPLv3. Die Klausel zur Nutzung über ein Netzwerk muss daher mit der zuständigen Person in Ihrem Unternehmen geklärt werden, bevor PgDog in Produktion eingesetzt wird. Nach eigener Aussage des Projekts führen interne Nutzung und private Änderungen nicht zu einer Pflicht zur Veröffentlichung des Quellcodes. PgDog ist außerdem noch jung, mit wöchentlichen Releases und 0.x-Versionsnummern. Fixieren Sie daher ein Release-Tag, statt main zu folgen.

git clone https://github.com/pgdogdev/pgdog
cd pgdog
cargo build --release
./target/release/pgdog --config pgdog.toml --users users.toml

Für den Build aus dem Quellcode benötigen Sie eine aktuelle stabile Rust-Toolchain, CMake und einen C/C++-Compiler. Auf der Releases-Seite stehen außerdem vorkompilierte Linux-Binaries und Debian-Pakete bereit. Ein Container-Image ist unter ghcr.io/pgdogdev/pgdog verfügbar. Die Konfiguration ist auf zwei Dateien aufgeteilt. Die erste Datei enthält die allgemeinen Einstellungen und einen Eintrag pro Datenbank. Diese Einträge werden hier als TOML-Array aus Inline-Tabellen geschrieben, damit die beiden Formen leicht zu unterscheiden bleiben.

databases = [
  { name = "appdb", host = "127.0.0.1" },
]

[general]
port = 6432
default_pool_size = 10

Die zweite Datei enthält einen Eintrag pro Benutzer, ebenfalls in dieser Array-Form.

users = [
  { name = "appuser", database = "appdb", password = "change-this" },
]

PgDog lauscht standardmäßig auf Port 6432, also auf demselben Port wie PgBouncer. Daher können beide auf einem Host nicht gleichzeitig den Standard-Port verwenden.

Pgpool-II bietet mit Stand Juni 2026 in Version 4.7.2 neben Pooling auch Load-Balancing und einen Watchdog für automatisches Failover. Die zusätzlichen Funktionen bringen zusätzliche Fehlerquellen mit sich. Vor der Auswahl muss daher das Pooling-Modell verstanden werden. Pgpool-II startet vorab num_init_children Kindprozesse. Jeder Kindprozess hält bis zu max_pool Serververbindungen im Cache. Das Limit für Backends ergibt sich daher aus num_init_children multipliziert mit max_pool. Jeder Kindprozess bedient jeweils einen Client. Die maximale Zahl der akzeptierten Clients entspricht daher num_init_children und wird beim Start festgelegt. Auch ein inaktiver Client belegt weiterhin einen Kindprozess. Wenn Sie num_init_children auf 100 und max_pool auf 4 setzen, erlauben Sie 400 Backends. Genau das ist normalerweise das Problem, zu dessen Lösung Sie einen Pooler eingerichtet haben. Wählen Sie Pgpool-II, wenn Sie dessen Failover- und Query-Routing-Funktionen benötigen, und führen Sie diese Multiplikation dann sorgfältig durch. Wenn Sie lediglich die Zahl der Backends reduzieren möchten, ist Pgpool-II für diese Aufgabe umfangreicher als nötig.

Die Frage nach dem verwalteten Proxy und das selbst gehostete Gegenstück

Verwaltete Plattformen bieten dies als separates Produkt an. AWS setzt RDS Proxy vor RDS, und Supabase setzt seinen eigenen Pooler Supavisor vor Supabase Postgres. Beide erfüllen die hier beschriebene Aufgabe: Sie halten Clientverbindungen kostengünstig offen und vergeben eine kleinere Anzahl echter Backend-Verbindungen. Supavisor ist Open Source und kann selbst gehostet werden. Die Entscheidung ist daher nicht auf proprietäre Software gegenüber kostenloser Software beschränkt.

Das selbst gehostete Gegenstück zu einem verwalteten Proxy ist keine andere Idee. Es ist dieselbe Idee mit der Konfigurationsdatei in Ihrer Hand: PgBouncer im Transaktionsmodus, auf demselben VPS wie die Datenbank, mit einer Bindung an 127.0.0.1. Zwei Unterschiede sind relevant. Ein verwalteter Proxy befindet sich einen Netzwerk-Hop entfernt. Dadurch entsteht zusätzliche Latenz, und der Proxy hält Clientverbindungen weiter offen, während die Datenbank im Hintergrund neu startet. PgBouncer auf dem Datenbankhost verursacht nur einen Loopback-Hop, der praktisch keine Kosten verursacht, und fällt aus, wenn dieser Host ausfällt. Wenn das Verhalten bei einem Neustart erhalten bleiben soll, benötigen Sie zusätzlich eine Failover-Infrastruktur. Hier beginnen sich der Watchdog von Pgpool-II oder die Health Checks von PgDog durch ihre zusätzliche Komplexität zu rechtfertigen.

Eine weitere Option gehört auf die Liste. Wenn die Anzahl der Verbindungen der Hauptgrund für die Komplexität Ihrer Bereitstellung ist, gibt es bei einer eingebetteten Datenbank kein Verbindungsmodell, das Sie poolen müssen. Sie ist eine Bibliothek in Ihrem Prozess und kein Server, der an einem Port lauscht. Für einen einzelnen Anwendungsserver mit moderatem Schreibaufkommen beseitigt der produktive Betrieb von SQLite auf einem VPS dieses gesamte Problem, statt es zu verwalten. Wenn Sie einen echten Server benötigen, dimensionieren Sie den Pool, bevor Sie die Maschine dimensionieren.

FAQ

Benötige ich PgBouncer noch, wenn meine Anwendung bereits einen Verbindungspool verwendet?

In der Regel ja, weil ein Anwendungspool pro Prozess gilt und die anderen Pools nicht sehen kann. Vier Gunicorn-Worker mit jeweils einem Pool aus 20 Anfrage-80-Backends ergeben zusammen 120 Backends, wenn zusätzlich zwei Hintergrund-Worker laufen. Nur PgBouncer sieht die Gesamtsumme und kann sie begrenzen. Die sinnvolle Kombination besteht aus beidem: einem kleinen Pool in jedem Worker, damit Anfragen nicht für jede TCP-Verbindung Zeit benötigen, und PgBouncer im Transaktionsmodus, der die dahinterliegenden tatsächlichen Backends begrenzt.

Was genau funktioniert nicht mehr, wenn ich PgBouncer in den Transaktionsmodus schalte?

Alles, was seinen Zustand über mehrere Transaktionen hinweg in einem Backend speichert. Dazu gehören SET und RESET auf Sitzungsebene, LISTEN- und WITH HOLD-Cursor, die SQL-Anweisungen PREPARE und DEALLOCATE, Advisory Locks auf Sitzungsebene, temporäre Tabellen, die einen Commit überdauern müssen, sowie LOAD. NOTIFY funktioniert weiterhin. Dadurch wirkt ein defektes LISTEN wie ein Zustellungsfehler und nicht wie ein Pooling-Problem. Setzen Sie bei Django DISABLE_SERVER_SIDE_CURSORS auf True. Verwenden Sie bei psycopg 3 entweder prepare_threshold mit dem Wert None oder PgBouncer 1.22 oder neuer mit max_prepared_statements größer als 0. Ersetzen Sie pg_advisory_lock() durch pg_advisory_xact_lock().

Wie groß sollte default_pool_size auf einer VPS mit 2 Kernen sein?

Kleiner, als es zunächst sinnvoll erscheint. Die häufig veröffentlichte HikariCP-Heuristik liegt ungefähr beim Doppelten der Kernanzahl plus eins, also bei zwei Kernen ungefähr bei 5. Das ist jedoch ein Ausgangspunkt und kein fertiger Wert. Setzen Sie den Wert und prüfen Sie dann unter realer Last maxwait und cl_waiting in SHOW POOLS. Wenn beide Werte 0 sind, ist der Pool ausreichend groß. Ein steigender Wert für maxwait bedeutet, dass Clients warten. Bevor Sie den Wert erhöhen, prüfen Sie pg_stat_activity: Backends, die in idle in transaction festhängen, weisen auf einen Fehler in der Anwendung hin, den zusätzliche Verbindungen nur verdecken würden.

PgBouncer oder PgDog?

PgBouncer für einen PostgreSQL-Server auf einer VPS. Das entspricht den meisten Bereitstellungen. PgBouncer ist in Ubuntu paketiert, sein Verhalten ist gut dokumentiert, und die gesamte Konfiguration befindet sich in einer einzigen ini-Datei. PgDog eignet sich, wenn das Aufteilen von Lese- und Schreibzugriffen auf Replikate oder das Sharding im selben Verarbeitungsschritt wie das Pooling erfolgen soll, damit die Anwendung die Topologie nicht kennen muss. Klären Sie vor der Entscheidung für PgDog die Frage zur AGPLv3 mit der zuständigen Person für Lizenzen in Ihrer Organisation. Legen Sie außerdem eine bestimmte Version fest, weil das Projekt weiterhin Versionsnummern im Bereich 0.x verwendet und wöchentlich neue Versionen veröffentlicht.

#postgres#pgbouncer#pgdog#connections#performance