Datenbankverbindungen effizient wiederverwenden
Klassisches PHP im shared-nothing Modell kennt keinen echten Connection Pool im Speicher eines einzelnen Prozesses. Wer trotzdem den Overhead ständiger Neuverbindungen vermeiden will, braucht entweder persistente Verbindungen mit klaren Grenzen, einen externen Connection Pooler wie ProxySQL, oder ein Long-Running-Prozessmodell wie Swoole.
Inhaltsverzeichnis
- 1. Warum Connection Pooling in PHP eine Besonderheit ist
- 2. Den Overhead einer Datenbankverbindung messen
- 3. Persistent Connections mit PDO: Chancen und Risiken
- 4. Externe Connection Pooler: ProxySQL und PgBouncer
- 5. Ein echter Connection Pool in Long-Running-Prozessen
- 6. Verbindungslimits und Timeouts richtig konfigurieren
- 7. Health Checks und Reconnect-Strategien
- 8. Monitoring von Connection Pools
- 9. Pooling-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Connection Pooling in PHP eine Besonderheit ist
Connection Pooling bedeutet, eine begrenzte Anzahl bereits geöffneter Datenbankverbindungen wiederzuverwenden, statt für jede Anfrage eine neue Verbindung aufzubauen und wieder abzubauen. In Sprachen mit Long-Running-Prozessen wie Java oder Node.js ist das trivial: Der Prozess lebt über viele Anfragen hinweg, ein Pool im Prozessspeicher verwaltet die Verbindungen zentral. Klassisches PHP unter PHP-FPM folgt jedoch dem shared-nothing Modell: Jeder Request startet in einem frischen oder wiederverwendeten Worker-Prozess, der am Ende des Requests seinen Zustand verliert.
Das bedeutet: Ein Connection Pool im klassischen Sinne, verwaltet im Speicher eines einzelnen Anwendungsprozesses über mehrere Requests hinweg, existiert in Standard-PHP nicht von Natur aus. Wer Connection Pooling in PHP umsetzen will, hat drei grundsätzlich verschiedene Wege: persistente Verbindungen auf PHP-FPM-Worker-Ebene, einen externen Pooler zwischen Anwendung und Datenbank, oder ein Long-Running-Prozessmodell wie Swoole, das PHP-Prozesse über Requests hinweg am Leben hält und damit einen echten In-Process-Pool ermöglicht.
2. Den Overhead einer Datenbankverbindung messen
Bevor man in Connection Pooling investiert, lohnt sich die Frage, wie teuer eine neue Verbindung tatsächlich ist. Der Aufbau einer TCP-Verbindung, gefolgt vom Datenbank-Handshake, Authentifizierung und gegebenenfalls TLS-Verschlüsselung, kostet bei MySQL typischerweise ein bis mehrere Millisekunden, abhängig von Netzwerklatenz und Serverauslastung. Bei einer Anwendung mit tausend Requests pro Sekunde, von denen jeder eine neue Verbindung öffnet, summiert sich das schnell zu spürbarer CPU- und Netzwerklast, sowohl auf Anwendungs- als auch auf Datenbankseite.
Ein zweiter, oft unterschätzter Faktor: Jede offene Verbindung verbraucht auf dem Datenbankserver Speicher für Thread-Stack, Puffer und Sitzungsvariablen, bei MySQL häufig mehrere Megabyte pro Verbindung. Bei hoher Parallelität ohne Connection Pooling steigt die Anzahl gleichzeitiger Verbindungen schnell an die konfigurierte max_connections-Grenze, was zu abgelehnten Verbindungen führt, lange bevor die eigentliche CPU- oder I/O-Kapazität des Servers ausgeschöpft ist.
3. Persistent Connections mit PDO: Chancen und Risiken
PDO bietet mit PDO::ATTR_PERSISTENT die einfachste Form von Connection Pooling in klassischem PHP. Wird dieses Attribut gesetzt, versucht PHP, die Verbindung am Ende des Requests offen zu halten und beim nächsten Request im selben PHP-FPM-Worker-Prozess wiederzuverwenden, statt eine neue TCP-Verbindung aufzubauen. Das reduziert den Verbindungsaufbau-Overhead spürbar, besonders bei Datenbanken mit teurem Handshake wie PostgreSQL mit SSL.
Der entscheidende Nachteil: Persistente Verbindungen werden pro PHP-FPM-Worker-Prozess gehalten, nicht projektweit geteilt. Bei hundert Worker-Prozessen entstehen potenziell hundert offene Verbindungen zur Datenbank, selbst wenn nur wenige davon gerade aktiv genutzt werden. Zudem überlebt der Verbindungszustand zwischen Requests, was bei nicht zurückgesetzten Transaktionen oder Sitzungsvariablen zu schwer nachvollziehbaren Fehlern im nächsten Request führen kann, der zufällig denselben Worker-Prozess trifft.
<?php
declare(strict_types=1);
final class PersistentConnectionFactory
{
public function __construct(
private readonly string $dsn,
private readonly string $username,
private readonly string $password,
) {
}
public function create(): PDO
{
return new PDO($this->dsn, $this->username, $this->password, [
// Reuse the connection across requests within the same FPM worker
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
}
// Defensive reset at the start of every request that reuses a persistent connection
final class ConnectionResetter
{
public function resetSessionState(PDO $pdo): void
{
if ($pdo->inTransaction()) {
// A leftover open transaction from a crashed previous request
// must never leak into the next request silently
$pdo->rollBack();
}
$pdo->exec('SET SESSION sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_DATE"');
}
}
4. Externe Connection Pooler: ProxySQL und PgBouncer
Der robustere Weg für Connection Pooling in PHP-Umgebungen ist ein dedizierter Pooling-Proxy zwischen Anwendung und Datenbank. ProxySQL für MySQL und PgBouncer für PostgreSQL laufen als eigener Prozess, meist auf demselben Host wie die PHP-Anwendung oder als zentraler Dienst im Netzwerk. Die PHP-Anwendung verbindet sich wie gewohnt über PDO, allerdings gegen den Proxy statt direkt gegen den Datenbankserver, der Proxy hält im Hintergrund einen echten, projektweiten Pool an Verbindungen zur eigentlichen Datenbank offen.
Der Vorteil gegenüber ATTR_PERSISTENT: Der Pool wird zentral über alle PHP-FPM-Worker-Prozesse und sogar über mehrere Anwendungsserver hinweg geteilt, statt pro Worker isoliert zu existieren. PgBouncer unterstützt zusätzlich verschiedene Pooling-Modi, wobei der transaction-Modus die Verbindung nur für die Dauer einer einzelnen Transaktion reserviert und danach sofort für andere Clients freigibt, was die effektive Anzahl benötigter Datenbankverbindungen drastisch reduziert.
# pgbouncer.ini — transaction pooling mode releases the connection
# back to the pool immediately after each transaction commits
[databases]
shop_production = host=127.0.0.1 port=5432 dbname=shop
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 40
reserve_pool_size = 10
server_idle_timeout = 300
5. Ein echter Connection Pool in Long-Running-Prozessen
In Long-Running-PHP-Umgebungen wie Swoole oder RoadRunner läuft der PHP-Prozess über tausende Requests hinweg weiter, ohne zwischendurch beendet zu werden. Das erlaubt einen echten, in-Prozess implementierten Connection Pool, wie er in Java oder Node.js üblich ist: Ein Pool-Objekt hält beim Prozessstart eine feste Anzahl an Verbindungen offen, jede Coroutine oder jeder Request leiht sich eine Verbindung aus dem Pool aus und gibt sie danach zurück, statt eine neue zu öffnen.
Diese Variante bietet die beste Performance, weil kein zusätzlicher Netzwerk-Hop über einen Proxy nötig ist, verlangt aber, dass die Anwendung als Long-Running-Prozess betrieben wird, was Anpassungen an Speicherverwaltung, globalem Zustand und Fehlerbehandlung erfordert. Insbesondere darf keine globale Variable ungewollt zwischen Requests überleben, ein Fehler, der in klassischem PHP-FPM durch den Prozessneustart pro Request automatisch verhindert wird.
<?php
declare(strict_types=1);
use Swoole\Coroutine\Channel;
final class SwooleConnectionPool
{
private Channel $channel;
public function __construct(
private readonly Closure $connectionFactory,
int $poolSize = 20,
) {
$this->channel = new Channel($poolSize);
for ($i = 0; $i < $poolSize; $i++) {
$this->channel->push(($this->connectionFactory)());
}
}
// Borrow a connection, run the callback, always return it to the pool
public function withConnection(Closure $callback): mixed
{
/** @var PDO $connection */
$connection = $this->channel->pop();
try {
return $callback($connection);
} finally {
$this->channel->push($connection);
}
}
}
$pool = new SwooleConnectionPool(
fn (): PDO => new PDO('mysql:host=127.0.0.1;dbname=shop', 'app', 'secret'),
);
$result = $pool->withConnection(
fn (PDO $pdo) => $pdo->query('SELECT COUNT(*) FROM orders')->fetchColumn()
);
6. Verbindungslimits und Timeouts richtig konfigurieren
Unabhängig von der gewählten Connection Pooling-Strategie muss die maximale Anzahl an Verbindungen bewusst konfiguriert werden. MySQLs max_connections, standardmäßig oft 151, muss auf die tatsächliche Anzahl der PHP-FPM-Worker plus Sicherheitsreserve abgestimmt sein. Wird dieser Wert zu niedrig gewählt, scheitern Verbindungsversuche mit Too many connections, wird er zu hoch gewählt, kann der Server bei tatsächlicher Vollauslastung durch Speicherdruck ins Swapping geraten.
Ebenso wichtig sind Timeouts: wait_timeout und interactive_timeout bei MySQL bestimmen, wie lange eine inaktive Verbindung gehalten wird, bevor der Server sie serverseitig schließt. Ohne aufeinander abgestimmte Werte zwischen Anwendung und Datenbank entstehen Situationen, in denen die Anwendung eine bereits vom Server geschlossene Verbindung wiederverwenden will, was zu MySQL server has gone away-Fehlern führt, einem der häufigsten Symptome fehlerhaft konfigurierten Connection Poolings.
7. Health Checks und Reconnect-Strategien
Jede Form von Connection Pooling, ob persistente PDO-Verbindungen, externer Proxy oder In-Process-Pool, braucht eine Strategie für den Fall, dass eine Verbindung im Pool inzwischen ungültig geworden ist, etwa durch einen Netzwerk-Timeout oder einen Datenbank-Failover. Ein einfacher Health Check vor der Wiederverwendung prüft mit einer minimalen Abfrage wie SELECT 1, ob die Verbindung noch funktioniert, bevor sie für die eigentliche Arbeit genutzt wird.
Bei einer ungültigen Verbindung sollte die Pool-Implementierung automatisch eine neue Verbindung aufbauen und die defekte verwerfen, statt den Fehler unbehandelt an den Aufrufer weiterzureichen. Externe Pooler wie ProxySQL bringen solche Health Checks bereits eingebaut mit und markieren fehlerhafte Backend-Verbindungen automatisch, während bei eigenen In-Process-Pools diese Logik selbst implementiert werden muss.
8. Monitoring von Connection Pools
Ohne Monitoring bleibt Connection Pooling eine Blackbox. Die wichtigsten Metriken sind die Anzahl aktiver Verbindungen, die Anzahl wartender Anfragen auf eine freie Verbindung, und die durchschnittliche Wartezeit, bis eine Verbindung verfügbar wird. Ein wachsender Wartequeue-Wert ist ein frühes Warnsignal dafür, dass der Pool zu klein für die aktuelle Last dimensioniert ist, lange bevor Requests tatsächlich mit Timeout fehlschlagen.
MySQL bietet mit SHOW STATUS LIKE 'Threads_connected' und SHOW STATUS LIKE 'Max_used_connections' grundlegende Kennzahlen auf Datenbankseite. ProxySQL stellt über seine Admin-Schnittstelle deutlich detailliertere Statistiken pro Connection-Pool und Backend-Server bereit, die sich in Prometheus oder Grafana einbinden lassen, um Trends über Zeit sichtbar zu machen, statt nur Momentaufnahmen zu betrachten.
9. Pooling-Strategien im Vergleich
Die drei vorgestellten Ansätze unterscheiden sich erheblich in Implementierungsaufwand, Effektivität und Betriebsanforderungen. Die folgende Tabelle fasst die wichtigsten Unterschiede zusammen.
| Kriterium | PDO Persistent | ProxySQL / PgBouncer | Swoole In-Process-Pool |
|---|---|---|---|
| Implementierungsaufwand | Minimal | Mittel, eigener Dienst | Hoch, Architekturwechsel |
| Pool wirklich geteilt | Nein, pro Worker | Ja, projektweit | Ja, pro Prozess |
| Zusätzlicher Netzwerk-Hop | Nein | Ja, über Proxy | Nein |
| Kompatibel mit PHP-FPM | Ja | Ja | Nein |
| Risiko von Zustandslecks | Mittel bis hoch | Gering | Hoch ohne Disziplin |
In der Praxis ist ein externer Pooler wie ProxySQL für die meisten klassischen PHP-FPM-Projekte der beste Kompromiss aus Effektivität und überschaubarem Migrationsaufwand. Long-Running-Prozessmodelle bringen die beste Performance, erfordern aber eine bewusste Architekturentscheidung, die nicht nebenbei in einem bestehenden Projekt nachgerüstet werden sollte.
Mironsoft
PHP-Performance und Datenbank-Infrastruktur
Zu viele Datenbankverbindungen unter Last?
Wir analysieren Verbindungsmuster in PHP-FPM-Umgebungen, konfigurieren ProxySQL oder PgBouncer und entwerfen Pooling-Strategien, die auch bei Lastspitzen stabil bleiben.
Analyse
Verbindungsverhalten und max_connections-Limits unter realer Last messen
Pooler-Setup
ProxySQL oder PgBouncer konfigurieren und in bestehende Infrastruktur integrieren
Monitoring
Pool-Metriken in Prometheus und Grafana sichtbar machen
10. Zusammenfassung
Connection Pooling in PHP ist keine Selbstverständlichkeit wie in Java oder Node.js, sondern erfordert eine bewusste Entscheidung zwischen drei Strategien: persistente PDO-Verbindungen als einfachster, aber pro Worker isolierter Ansatz, externe Pooler wie ProxySQL oder PgBouncer als robuste Middleware-Lösung, oder ein In-Process-Pool in Long-Running-Umgebungen wie Swoole für maximale Performance bei entsprechendem Architekturaufwand.
Unabhängig von der gewählten Strategie sind korrekt konfigurierte Verbindungslimits, Timeouts, Health Checks und Monitoring keine optionalen Details, sondern die Voraussetzung dafür, dass Connection Pooling unter realer Last tatsächlich hilft statt neue, schwerer diagnostizierbare Probleme zu erzeugen.
Connection Pooling in PHP Anwendungen — Das Wichtigste auf einen Blick
Grundproblem
PHP-FPM folgt dem shared-nothing Modell, ein In-Process-Pool über Requests hinweg existiert von Natur aus nicht.
Einfachster Ansatz
PDO::ATTR_PERSISTENT reduziert Overhead pro Worker, teilt den Pool aber nicht projektweit.
Robusteste Lösung
ProxySQL oder PgBouncer als zentraler Pooler zwischen Anwendung und Datenbank, projektweit geteilt.
Betrieb
max_connections, Timeouts, Health Checks und Monitoring sind für jede Pooling-Strategie Pflicht.