Verbindungsoptimierung

Abgeschlossen

Datenbankverbindungen sind teure Ressourcen. Jede Verbindung verbraucht Serverspeicher, erfordert Authentifizierungsaufwand und zählt zu Servergrenzwerten. Für KI-Anwendungen, die häufige Vektorabfragen erstellen, ist eine effiziente Verbindungsverwaltung unerlässlich, um einen hohen Durchsatz zu erzielen, ohne Ressourcen zu erschöpfen.

Hinweis

Codebeispiele in dieser Lektion veranschaulichen Verbindungsverwaltungsmuster für Python (psycopg) und .NET (Npgsql). Diese Bibliotheken werden häufig aktualisiert. Besuchen Sie die Psycopg-Dokumentation und die Npgsql-Dokumentation , um aktuelle API-Details zu erhalten.

Verbindungsaufwand

Das Erstellen einer neuen PostgreSQL-Verbindung umfasst mehrere Schritte, wobei jede Latenz hinzugefügt wird:

  1. TCP-Handshake: Einrichten der Netzwerkverbindung (in der Regel 1-3 Roundtrips)
  2. TLS-Aushandlung: Verschlüsseln der Verbindung (erforderlich für Azure Database for PostgreSQL)
  3. Authentifizierung: Überprüfen von Anmeldeinformationen (Kennwort- oder Tokenaustausch)
  4. Serverprozesszuordnung: PostgreSQL spawns einen Back-End-Prozess für jede Verbindung
  5. Sitzungsinitialisierung: Festlegen von Sitzungsparametern und Ladekonfigurationen

Diese Sequenz benötigt je nach Netzwerklatenz und Serverlast 50-200 Millisekunden. Für ein Empfehlungsmodul, das Tausende von Anforderungen pro Sekunde verarbeitet, würde das Erstellen neuer Verbindungen pro Anforderung mehr Zeit im Verbindungssetup verbrauchen als bei der tatsächlichen Abfrageausführung.

Die Azure-Datenbank für PostgreSQL beschränkt gleichzeitige Verbindungen basierend auf der Computeebene. Burstable B1ms ermöglicht 50 Verbindungen, General Purpose 2 vCores ermöglicht 859 Verbindungen, General Purpose 4 vCores ermöglicht 1.718 Verbindungen, Memory Optimized 4 vCores ermöglicht 3.437 Verbindungen, und Memory Optimized 16 vCores ermöglicht 5.000 Verbindungen. Das Überschreiten dieser Grenzwerte führt zu Verbindungsfehlern. Anwendungen, die Verbindungen pro Anforderung erstellen, können diese Grenzwerte bei Datenverkehrsspitzen schnell erreichen.

Verbindungspooling mit PgBouncer

PgBouncer ist ein einfacher Verbindungspooler, der zwischen Ihrer Anwendung und PostgreSQL liegt. Es verwaltet einen Pool von Datenbankverbindungen und multiplexiert die Clientverbindungen darüber, wodurch die Anzahl der tatsächlich benötigten Datenbankverbindungen erheblich reduziert wird.

Die Azure-Datenbank für PostgreSQL bietet integrierte PgBouncer-Unterstützung in den allgemein zweckmäßigen und speicheroptimierten Rechenleistungsebenen. Diese Funktion wird von der Burstable-Stufe nicht unterstützt. Aktivieren Sie PgBouncer über das Azure-Portal oder die CLI. Nach der Aktivierung verbinden Sie sich über Port 6432 (der PgBouncer-Port) anstelle von 5432 (der direkte PostgreSQL-Port). Die PgBouncer-Verbindungszeichenfolge verwendet postgresql://user:password@myserver.postgres.database.azure.com:6432/mydb.

az postgres flexible-server parameter set \
    --resource-group myResourceGroup \
    --server-name myserver \
    --name pgbouncer.enabled \
    --value true

PgBouncer unterstützt drei Pooling-Modi, von denen jeder unterschiedliche Kompromisse mit sich bringt. Der Sitzungsmodus bedeutet, dass ein Client eine Serververbindung für die gesamte Sitzung (bis zur Trennung) aufrecht erhält. Dieser Modus unterstützt alle PostgreSQL-Features, bietet jedoch minimale Verbindungsreduzierung. Der Transaktionsmodus bedeutet, dass ein Client nur während einer Transaktion eine Serververbindung enthält. Zwischen Transaktionen kehrt die Verbindung zum Pool zurück. Dieser Modus eignet sich gut für die meisten Anwendungen und reduziert die Verbindungsanforderungen erheblich. Der Abfragemodus bedeutet, dass ein Client die Verbindung nur für einzelne Abfragen erhält. Dieser Modus bietet die geringste Verbindungsauslastung, unterstützt jedoch keine Mehrfachanweisungs-Transaktionen. Bei Vektorsuchworkloads ist der Transaktionsmodus in der Regel die beste Wahl.

PgBouncer macht mehrere Parameter verfügbar, die das Verhalten des Pools, Verbindungslimits und die Timeoutbehandlung steuern. Bei Vektorsucharbeitslasten mit platzigen Datenverkehrsmustern hilft die Optimierung dieser Parameter beim Ausgleich der Verbindungsverfügbarkeit mit dem Ressourcenverbrauch. Konfigurieren pgbouncer.default_pool_size (20-50 abhängig von Parallelitätsanforderungen), pgbouncer.max_client_conn (5000+ für Anwendungen mit hohem Datenverkehr), pgbouncer.pool_mode (Transaktion) und pgbouncer.query_wait_timeout (30-120 Sekunden).

Im Transaktionsmodus werden die Verbindungen nach jedem Commit oder Rollback einer Transaktion wieder an den Pool zurückgegeben. Dies wirkt sich auf mehrere PostgreSQL-Features aus. Sitzungsvariablen werden zwischen Transaktionen zurückgesetzt, sodass mit SET angewendete Einstellungen nicht über Transaktionen hinweg beibehalten werden. Verwenden Sie SET LOCAL innerhalb von Transaktionen oder konfigurieren Sie serverseitige Serverstandards. Vorbereitete Anweisungen funktionieren möglicherweise nicht, da benannte vorbereitete Anweisungen an Verbindungen gebunden sind. Im Transaktionsmodus ist eine vorbereitete Anweisung, die in einer Transaktion erstellt wurde, möglicherweise nicht in der nächsten Transaktion verfügbar, wenn eine andere Verbindung zugewiesen wird. LISTEN/NOTIFY funktioniert nicht, da diese Features dauerhafte Verbindungen erfordern und nicht mit transaktionspooling kompatibel sind. Bei Vektorsuchanwendungen sind diese Einschränkungen selten problematisch, da Abfragen in der Regel einfache Auswahlen ohne sitzungsspezifischen Zustand sind.

Verbindungspooling auf Anwendungsebene

Zusätzlich zu (oder statt) PgBouncer kann Ihre Anwendung Verbindungspools direkt verwalten. Dies bietet eine bessere Kontrolle über den Verbindungslebenszyklus und lässt sich in Anwendungsframeworks integrieren.

Das psycopg_pool Paket stellt verbindungspooling für psycopg bereit. Pools auf Anwendungsebene ermöglichen Ihnen die Kontrolle über den Verbindungslebenszyklus, das Leerlauftimeoutverhalten und die Integritätsprüfung. Sie integrieren sich auch natürlich in die Fehlerbehandlung und Protokollierung Ihrer Anwendung. In Kombination mit PgBouncer behandeln Anwendungspools die lokale Verbindungsverwaltung, während PgBouncer serverseitiges Multiplexing verarbeitet. Der with pool.connection() Kontext-Manager gibt die Verbindung zum Pool automatisch zurück, wenn der Block beendet wird, auch wenn eine Ausnahme auftritt.

from psycopg_pool import ConnectionPool

# Create a connection pool
pool = ConnectionPool(
    conninfo="postgresql://user:password@myserver.postgres.database.azure.com:6432/mydb",
    min_size=5,      # Minimum connections to maintain
    max_size=20,     # Maximum connections allowed
    max_idle=300,    # Close idle connections after 5 minutes
    max_lifetime=3600  # Recycle connections after 1 hour
)

# Use connections from the pool
def search_similar_products(query_embedding, limit=10):
    with pool.connection() as conn:
        with conn.cursor() as cur:
            cur.execute("""
                SELECT id, name, embedding <=> %s AS distance
                FROM products
                ORDER BY embedding <=> %s
                LIMIT %s
            """, (query_embedding, query_embedding, limit))
            return cur.fetchall()

Npgsql enthält standardmäßig integrierte Verbindungspooling, sodass Sie kein separates Paket benötigen. Der Pool verwaltet automatisch die Erstellung, Wiederverwendung und Entsorgung der Verbindung basierend auf Parametern, die Sie in der Verbindungszeichenfolge angeben. Jede eindeutige Verbindungszeichenfolge verwaltet einen eigenen Pool, sodass konsistente Verbindungszeichenfolgen in Ihrer Anwendung eine effiziente Poolnutzung gewährleisten. Wenn Sie conn.Close() aufrufen oder die Verbindung freigegeben wird, wird sie an den Pool zurückgegeben, anstatt zerstört zu werden. Konfigurieren Sie die Poolerstellung über Verbindungszeichenfolgenparameter wie Minimum Pool Size=5;Maximum Pool Size=20;Connection Idle Lifetime=300;Connection Lifetime=3600.

Die Poolgröße wirkt sich sowohl auf die Leistung als auch auf den Ressourcenverbrauch aus. Das Festlegen des Pools zu klein führt dazu, dass Anforderungen auf verfügbare Verbindungen warten, wodurch die Latenz erhöht wird. Wenn Sie sie zu groß festlegen, wird Arbeitsspeicher verschwendet und der Datenbankserver kann überlastet werden. Die richtige Größe hängt von Ihren Datenverkehrsmustern, der Abfragedauer und der Anzahl der Anwendungsinstanzen ab, die die Datenbank freigeben. Halten Sie die Mindestgröße groß genug, um den Basis-Datenverkehr zu verarbeiten, ohne zu warten. Begrenzen Sie die maximale Größe auf das, was die Datenbank verkraften kann, geteilt durch die Anzahl der Anwendungsinstanzen. Wenn Sie über 10 Anwendungsinstanzen verfügen und Ihre Datenbank 1.000 Verbindungen unterstützt, sollten Sie maximal 100 Verbindungen pro Instanz festlegen (um Puffer zu lassen). Recyceln Sie Verbindungen in regelmäßigen Abständen (alle 30 bis 60 Minuten), um die Systemintegrität zu wahren, da dauerhafte Verbindungen Speicherlecks verursachen oder veraltete zwischengespeicherte Pläne speichern können.

Sitzungsverwaltung für KI-Workloads

Einige Vektorabfragen profitieren von Einstellungen auf Sitzungsebene, die der Abfrage mehr Ressourcen zuweisen, als die serverweiten Standardwerte zulassen.

Vektorähnlichkeitsabfragen, die große Ergebnismengen sortieren, profitieren von einer erhöhten work_mem. Legen Sie es für bestimmte Sitzungen oder Transaktionen mithilfe von SET LOCAL work_mem = '256MB' fest. SET LOCAL gilt nur innerhalb der aktuellen Transaktion. Wenn die Transaktion endet, wird die Einstellung auf die Standardeinstellung zurückgesetzt, die für poolierte Verbindungen sicher ist.

Passen Sie hnsw.ef_search oder ivfflat.probes für Abfragen mit unterschiedlichen Genauigkeitsanforderungen an. Verwenden Sie SET LOCAL hnsw.ef_search = 200 bei Abfragen, in denen eine hohe Genauigkeit entscheidend ist, oder SET LOCAL hnsw.ef_search = 20 für schnellere Abfragen, bei denen ungefähre Ergebnisse akzeptabel sind. Mit diesem Muster können Sie Genauigkeit und Geschwindigkeit basierend auf dem jeweiligen Anwendungsfall ausgleichen, ohne dass sich dies auf andere Abfragen auswirkt.

Effiziente SDK-Verwendungsmuster

Über die Verbindungsverwaltung hinaus wirkt sich die Struktur von Datenbankinteraktionen auf die Leistung aus.

Netzwerk-Roundtrips fügen jeder Datenbankoperation Latenz hinzu. Wenn Sie mehrere Datenstücke benötigen, wird durch das Abrufen mit einer einzigen Abfrage der Overhead, der durch Netzwerkübertragung, Abfrageanalyse und Ergebnisserialisierung entsteht, beseitigt. Bei KI-Anwendungen, die Einbettungen für mehrere Elemente abrufen, kann die Batchverarbeitung die Gesamtlatenz von Hunderten von Millisekunden auf einzelne Ziffern reduzieren. Verwenden Sie anstelle mehrerer Roundtrips mit einzelnen Abfragen eine einzige Abfrage mit WHERE id = ANY(%s) und übergeben Sie eine Liste von IDs.

Für das Laden einer großen Anzahl von Vektoren ist der PostgreSQL-Befehl COPY erheblich schneller als einzelne INSERT Anweisungen. COPY Daten werden direkt in die Tabelle in einem Binär- oder Textformat gestreamt, wobei der Aufwand der Analyse einzelner SQL-Anweisungen umgangen wird. Beim Laden von Einbettungsdaten aus Batchverarbeitungspipelines oder anfänglichen Datenmigrationen kann COPY die Ladezeiten von Stunden auf Minuten reduzieren. COPY kann Hunderte von Tausend Zeilen pro Sekunde laden, während einzelne Einfügungen auf Tausende pro Sekunde beschränkt sind.

Wenn Ihre Anwendung Arbeit parallelisieren kann, verbessern asynchrone Datenbankvorgänge den Durchsatz, indem mehrere Abfragen gleichzeitig ausgeführt werden, ohne Threads zu blockieren. Dieses Muster ist für KI-Workloads nützlich, die mehrere Vektorsammlungen gleichzeitig durchsuchen oder die Vektorsuche mit anderen Datenabrufen kombinieren müssen. Asynchrone Pools verwalten Verbindungen effizient über gleichzeitige Vorgänge hinweg und respektieren dabei die Poolgrößenbeschränkungen. Verwenden Sie AsyncConnectionPool aus psycopg_pool und asyncio.gather, um mehrere Suchvorgänge gleichzeitig auszuführen.

Verbindungsresilienz

Netzwerkprobleme, Serverneustarts und Failover können Datenbankverbindungen unterbrechen. Robuste Anwendungen behandeln diese geschickt.

Vorübergehende Fehler wie Netzwerkstörungen, Verbindungszurücksetzungen und kurzzeitige Serverunverfügbarkeit während der Wartung sind in Cloud-Umgebungen unvermeidlich. Das Implementieren von Wiederholungslogik mit exponentiellem Backoff hilft Ihrer Anwendung, sich von diesen temporären Problemen zu erholen, ohne den Server mit unmittelbaren Wiederholungsversuchen zu überlasten. Fügen Sie zufälligen Jitter hinzu, um zu verhindern, dass mehrere Anwendungsinstanzen gleichzeitig erneut versuchen. Fangen Sie OperationalError Ausnahmen ab, berechnen Sie die Wartezeit als (2 ** attempt) + random.uniform(0, 1) und wiederholen Sie den Vorgang bis zur maximalen Anzahl von Versuchen.

Timeouts verhindern, dass Ihre Anwendung unbegrenzt wartet, wenn die Datenbank langsam oder nicht erreichbar ist. Verbindungstimeouts begrenzen die Wartezeit beim Herstellen neuer Verbindungen, während Abfrage-Timeouts die Ausführungszeit der Abfrage begrenzen. Wählen Sie für Vektorsuchanwendungen Timeouts aus, die Ihre langsamsten legitimen Abfragen berücksichtigen und bei Abfragen, die die zulässige Latenz überschreiten, schnell abbrechen. Konfigurieren Sie Timeouts in der Verbindungszeichenfolge mithilfe von Parametern wie connect_timeout=10 und options=-c statement_timeout=30000. Legen Sie für Vektorabfragen Abfrage-Timeouts fest, die die langsamsten akzeptablen Anfragen berücksichtigen. Ein Timeout von 30 Sekunden ist für komplexe Vektorsuchen angemessen; Interaktive Anwendungen verwenden möglicherweise niedrigere Werte.

Wenn alle Poolverbindungen verwendet werden und neue Anforderungen eingehen, muss der Pool entweder Anforderungen in die Warteschlange stellen (was die Latenz erhöht) oder sie sofort ablehnen. Keine der beiden Optionen ist ideal, sodass die Überwachung der Poolnutzung Ihnen hilft, zu skalieren, bevor die Erschöpfung häufig wird. Im Falle einer Erschöpfung hilft die Rückgabe einer eindeutigen Fehlermeldung den Clients dabei, ihre eigene Wiederholungslogik zu implementieren, anstatt unvorhersehbar eine Zeitüberschreitung zu erleiden. Behandeln Sie PoolTimeout Ausnahmen, indem Sie eine passende Fehlermeldung wie {"error": "Service temporarily busy, please retry"} zurückgeben. Überwachen sie die Poolauslastung und skalieren Sie, wenn die Erschöpfung häufig auftritt.

Weitere Ressourcen