Connection Pooling in PHP Anwendungen: Verbindungen effizient wiederverwenden
AI generated
<?php
8.4
PHP · Connection Pooling · Performance · Datenbanken
Connection Pooling in PHP Anwendungen
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.

17 Min. Lesezeit Connection Pooling · ProxySQL · PgBouncer · Swoole PHP 8.4 · PDO · PHP-FPM

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.

11. FAQ: Connection Pooling in PHP Anwendungen

1Kein echtes Pooling in klassischem PHP?
Shared-nothing Modell in PHP-FPM: jeder Worker verliert seinen Zustand nach dem Request.
2Was macht ATTR_PERSISTENT?
Hält die Verbindung im selben Worker offen und vermeidet erneuten Verbindungsaufbau.
3Größter Nachteil persistenter Verbindungen?
Pool ist pro Worker isoliert, Zustand kann unbeabsichtigt zwischen Requests überleben.
4Was ist ProxySQL?
Eigenständiger Proxy vor MySQL, verwaltet einen projektweit geteilten Connection Pool.
5Was ist transaction pooling mode?
Verbindung wird nur pro Transaktion reserviert, danach sofort für andere Clients frei.
6Warum ermöglicht Swoole echtes Pooling?
Prozess lebt über viele Requests, ein Pool-Objekt kann Verbindungen tatsächlich teilen.
7Wie erkenne ich zu kleinen Pool?
Wachsende Warteschlange und steigende Wartezeiten sind frühe Warnsignale.
8Was verursacht MySQL server has gone away?
Diskrepanz zwischen wait_timeout und tatsächlicher Nutzungsdauer der Verbindung.
9Braucht es Health Checks vor Wiederverwendung?
Ja, ein SELECT 1 vor der eigentlichen Nutzung verhindert Fehler durch tote Verbindungen.
10Welche Metrik zeigt Überlastung am besten?
Wartende Anfragen kombiniert mit durchschnittlicher Wartezeit auf eine freie Verbindung.