Ottimizzazione connessione

Completato

Le connessioni di database sono risorse costose. Ogni connessione utilizza la memoria del server, richiede un sovraccarico relativo all'autenticazione e viene conteggiata rispetto ai limiti del server. Per le applicazioni di intelligenza artificiale che eseguono frequenti query vettoriali, la gestione efficiente delle connessioni è essenziale per ottenere una velocità effettiva elevata senza esaurire le risorse.

Annotazioni

Esempi di codice in questa unità illustrano i modelli di gestione delle connessioni per Python (psycopg) e .NET (Npgsql). Queste librerie vengono aggiornate di frequente. Per informazioni dettagliate sull'API corrente, vedere la documentazione di psycopg e la documentazione di Npgsql .

Overhead della connessione

La creazione di una nuova connessione PostgreSQL prevede più passaggi, ognuno dei quali aggiunge la latenza:

  1. Handshake TCP: Stabilizzazione della connessione di rete (in genere 1-3 viaggi di andata e ritorno)
  2. Negoziazione TLS: Crittografia della connessione (necessaria per Database di Azure per PostgreSQL)
  3. Autenticazione: Verifica delle credenziali (scambio di password o token)
  4. Allocazione dei processi del server: PostgreSQL genera un processo back-end per ogni connessione
  5. Inizializzazione sessione: Impostazione dei parametri di sessione e caricamento delle configurazioni

Questa sequenza richiede 50-200 millisecondi a seconda della latenza di rete e del carico del server. Per un motore di raccomandazione che gestisce migliaia di richieste al secondo, la creazione di nuove connessioni per richiesta richiederebbe più tempo nell'installazione della connessione rispetto all'esecuzione effettiva delle query.

Database di Azure per PostgreSQL limita le connessioni simultanee in base al livello di calcolo. Il Burstable B1ms consente 50 connessioni, il General Purpose 2 vCore consente 859 connessioni, il General Purpose 4 vCore consente 1.718 connessioni, il Memory Optimized 4 vCore consente 3.437 connessioni e il Memory Optimized 16 vCore consente 5.000 connessioni. Il superamento di questi limiti causa errori di connessione. Le applicazioni che creano connessioni per richiesta possono raggiungere rapidamente questi limiti durante i picchi di traffico.

Pool di connessioni con PgBouncer

PgBouncer è un pooler di connessioni leggero che si trova tra l'applicazione e PostgreSQL. Gestisce un pool di connessioni di database e connessioni client multiplexes tra loro, riducendo notevolmente il numero di connessioni di database effettive necessarie.

Il database di Azure per PostgreSQL include il supporto predefinito di PgBouncer nei livelli di calcolo Generale e Ottimizzato per la Memoria. Il livello burstable non supporta questa funzionalità. Abilitare PgBouncer tramite il portale di Azure o l'interfaccia della riga di comando. Dopo l'abilitazione, connettersi tramite la porta 6432 (porta PgBouncer) anziché 5432 (porta PostgreSQL diretta). La stringa di connessione PgBouncer usa 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 supporta tre modalità di pooling, ognuna con compromessi diversi. La modalità sessione indica che un client contiene una connessione server per l'intera sessione (fino alla disconnessione). Questa modalità supporta tutte le funzionalità di PostgreSQL, ma offre una riduzione minima della connessione. La modalità transazione indica che un client mantiene una connessione al server solo durante una transazione. Tra le transazioni, la connessione torna al pool. Questa modalità funziona bene per la maggior parte delle applicazioni e riduce significativamente i requisiti di connessione. La modalità istruzione indica che un client ottiene una connessione solo per le singole istruzioni. Questa modalità offre la riduzione massima della connessione, ma non supporta transazioni con più istruzioni. Per i carichi di lavoro di ricerca vettoriale, la modalità transazione è in genere la scelta migliore.

PgBouncer espone diversi parametri che controllano il comportamento del pool, i limiti di connessione e la gestione del timeout. Per i carichi di lavoro di ricerca vettoriale con modelli di traffico bursty, l'ottimizzazione di questi parametri consente di bilanciare la disponibilità della connessione rispetto all'utilizzo delle risorse. Configurare pgbouncer.default_pool_size (20-50 a seconda delle esigenze di concorrenza), pgbouncer.max_client_conn (5000+ per applicazioni con traffico elevato), (transazione) pgbouncer.pool_mode e pgbouncer.query_wait_timeout (30-120 secondi).

La modalità transazione restituisce le connessioni al pool dopo il commit o il rollback di ogni transazione. Ciò influisce su diverse funzionalità di PostgreSQL. Le variabili di sessione vengono reimpostate tra le transazioni, quindi le impostazioni applicate con SET non vengono mantenute tra le transazioni. Usare SET LOCAL all'interno delle transazioni o configurare le impostazioni predefinite sul lato server. Le istruzioni preparate potrebbero non funzionare perché le istruzioni preparate denominate sono associate alle connessioni. In modalità transazione, una dichiarazione preparata creata in una transazione potrebbe non essere disponibile nella successiva transazione se viene assegnata una connessione diversa. LISTEN/NOTIFY non funziona perché queste funzionalità richiedono connessioni persistenti e non sono compatibili con il pool di transazioni. Per le applicazioni di ricerca vettoriale, queste limitazioni sono raramente problematiche poiché le query sono in genere selezioni semplici senza stato specifico della sessione.

Pool di connessioni a livello di applicazione

Oltre a (o invece) PgBouncer, l'applicazione può gestire direttamente i pool di connessioni. Questo offre un controllo più corretto sul ciclo di vita della connessione e si integra con i framework dell'applicazione.

Il psycopg_pool pacchetto fornisce il pool di connessioni per psycopg. I pool dell'applicazione consentono di controllare il ciclo di vita della connessione, il comportamento di timeout per inattività e la verifica dello stato di integrità. Si integrano naturalmente anche con la gestione e la registrazione degli errori dell'applicazione. In combinazione con PgBouncer, i pool di applicazioni gestiscono la gestione delle connessioni locali mentre PgBouncer gestisce il multiplexing lato server. Il with pool.connection() gestore del contesto restituisce automaticamente la connessione al pool quando il blocco viene terminato, anche se si verifica un'eccezione.

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 include un pool di connessioni integrato e abilitato per impostazione predefinita, quindi non è necessario un pacchetto separato. Il pool gestisce automaticamente la creazione, il riutilizzo e l'eliminazione della connessione in base ai parametri specificati nella stringa di connessione. Ogni stringa di connessione univoca mantiene il proprio pool, in modo che le stringhe di connessione coerenti nell'applicazione garantiscano un utilizzo efficiente del pool. Quando si chiama conn.Close() o la connessione viene eliminata, viene restituita al pool anziché essere eliminata definitivamente. Configurare il pooling tramite parametri della stringa di connessione come Minimum Pool Size=5;Maximum Pool Size=20;Connection Idle Lifetime=300;Connection Lifetime=3600.

Le dimensioni del pool influiscono sia sulle prestazioni che sul consumo delle risorse. Impostare un pool troppo piccolo causa alle richieste di attendere le connessioni disponibili, aumentando la latenza. Impostandolo troppo grande spreca memoria e può sovraccaricare il server del database. Le dimensioni corrette dipendono dai modelli di traffico, dalla durata delle query e dal numero di istanze dell'applicazione che condividono il database. Mantenere le dimensioni minime sufficienti per gestire il traffico di base senza attendere. Limitare le dimensioni massime in base a ciò che il database può gestire diviso per il numero di istanze dell'applicazione. Se si hanno 10 istanze dell'applicazione e il database supporta 1.000 connessioni, impostare il massimo su 100 per istanza (lasciando un margine di sicurezza). Riciclare periodicamente le connessioni (ogni 30-60 minuti) per mantenere la stabilità perché le connessioni di lunga durata possono accumulare perdite di memoria o contenere piani cache obsoleti.

Gestione delle sessioni per carichi di lavoro di intelligenza artificiale

Alcune query vettoriali traggono vantaggio dalle impostazioni a livello di sessione che allocano più risorse alla query rispetto alle impostazioni predefinite a livello di server.

Le interrogazioni di somiglianza vettoriale che ordinano set di risultati di grandi dimensioni traggono vantaggio dall'aumento di work_mem. Impostarlo per sessioni o transazioni specifiche usando SET LOCAL work_mem = '256MB'. SET LOCAL si applica solo all'interno della transazione corrente. Al termine della transazione, l'impostazione ripristina l'impostazione predefinita, che è sicura per le connessioni in pool.

Modificare hnsw.ef_search o ivfflat.probes per le query con requisiti di accuratezza diversi. Usare SET LOCAL hnsw.ef_search = 200 per un richiamo più elevato nelle query in cui l'accuratezza è critica o SET LOCAL hnsw.ef_search = 20 per query più veloci in cui i risultati approssimativi sono accettabili. Questo modello consente di bilanciare l'accuratezza e la velocità in base al caso d'uso specifico senza influire sulle altre query.

Modelli di utilizzo efficienti dell'SDK

Oltre alla gestione delle connessioni, la struttura delle interazioni del database influisce sulle prestazioni.

I round trip di rete aggiungono latenza a ogni operazione del database. Quando sono necessari più elementi di dati, recuperarli in una singola query elimina il sovraccarico di ciascuna query per la trasmissione di rete, l'analisi delle query e la serializzazione dei risultati. Per le applicazioni di intelligenza artificiale che recuperano incorporamenti per più elementi, l'invio in batch può ridurre la latenza totale da centinaia di millisecondi a singole cifre. Anziché effettuare più round trip con singole query, utilizza una singola query con WHERE id = ANY(%s) e passa un elenco di ID.

Per caricare un numero elevato di vettori, il comando PostgreSQL COPY è notevolmente più veloce rispetto alle singole INSERT istruzioni. COPY trasmette i dati direttamente nella tabella in un formato binario o di testo, ignorando l'overhead dell'analisi di singole istruzioni SQL. Quando si caricano dati di incorporamento da pipeline di elaborazione batch o migrazioni iniziali dei dati, COPY è possibile ridurre i tempi di caricamento da ore a minuti. COPY può caricare centinaia di migliaia di righe al secondo, mentre i singoli inserimenti sono limitati a migliaia al secondo.

Quando l'applicazione può parallelizzare il lavoro, le operazioni asincrone del database migliorano la velocità effettiva eseguendo più query contemporaneamente senza bloccare i thread. Questo modello è utile per i carichi di lavoro di intelligenza artificiale che devono cercare più raccolte vettoriali contemporaneamente o combinare la ricerca vettoriale con altri dati di recupero. I pool asincroni gestiscono le connessioni in modo efficiente tra le operazioni simultanee rispettando i limiti delle dimensioni del pool. Usare AsyncConnectionPool da psycopg_pool e asyncio.gather per eseguire più ricerche contemporaneamente.

Resilienza della connessione

Problemi di rete, riavvii del server e failover possono interrompere le connessioni di database. Le applicazioni affidabili gestiscono queste applicazioni normalmente.

Gli errori temporanei, ad esempio i blip di rete, le reimpostazioni delle connessioni e la breve indisponibilità del server durante la manutenzione sono inevitabili negli ambienti cloud. L'implementazione della logica di ritentare con backoff esponenziale consente all'applicazione di riprendersi da questi problemi temporanei senza sovraccaricare il server con tentativi di ripetizione immediati. Aggiungere un jitter casuale per impedire che più istanze dell'applicazione riprovino contemporaneamente. Intercettare OperationalError le eccezioni, calcolare il tempo di attesa come (2 ** attempt) + random.uniform(0, 1)e riprovare fino a un numero massimo di tentativi.

I timeout impediscono all'applicazione di attendere a tempo indeterminato quando il database è lento o non raggiungibile. I timeout di connessione limitano la durata dell'attesa quando si stabiliscono nuove connessioni, mentre i timeout delle istruzioni limitano il tempo di esecuzione della query. Per le applicazioni di ricerca vettoriale, scelga timeout che accomodano le query legittime più lente, mentre falliscono rapidamente per le query che superano la latenza accettabile. Configurare i timeout nella stringa di connessione usando parametri come connect_timeout=10 e options=-c statement_timeout=30000. Per le query vettoriali, configurare i timeout per le istruzioni in modo che supportino il tempo di esecuzione delle query più lente ma accettabili da parte vostra. Un timeout di 30 secondi è ragionevole per ricerche vettoriali complesse; le applicazioni interattive potrebbero usare valori inferiori.

Quando tutte le connessioni alla piscina sono in uso e arrivano nuove richieste, la piscina può accodare le richieste (che comporta un'aggiunta di latenza) o rifiutarle immediatamente. Nessuna delle due opzioni è ideale, quindi il monitoraggio dell'utilizzo del pool consente di ridimensionare prima che l'esaurimento diventi frequente. Quando si verifica l'esaurimento, la restituzione di un messaggio di errore chiaro consente ai client di implementare la propria logica di ripetizione dei tentativi anziché il timeout imprevedibile. Gestire PoolTimeout le eccezioni restituendo un errore normale, ad esempio {"error": "Service temporarily busy, please retry"}. Monitorare l'utilizzo e la scalabilità del pool se l'esaurimento si verifica frequentemente.

Risorse aggiuntive