PRODPiattaforma BaaS europea sovranaApri Dashboard →

Prestazioni · 10 lettura minima

PgBouncer spiegazione del pooling delle transazioni

Affane Daylami · Fondateur · 12 giugno 2026

Torniamo al blog

La modalità di transazione di PgBouncer rilascia la connessione PostgreSQL alla fine di ogni transazione, non quando il client si disconnette. Questo è ciò che rende possibile servire migliaia di client HTTP con poche dozzine di connessioni server effettive ed è la modalità consigliata per qualsiasi API REST con query brevi.

Questo testo inglese è stato generato automaticamente dall'originale francese e non è stato ancora rivisto.
Questa pagina è stata tradotta automaticamente. Fa fede la versione inglese.

Questo guadagno ha un costo specifico: la modalità di transazione interrompe silenziosamente tutto ciò che presuppone una connessione Postgres stabile da una richiesta a quella successiva. Sessione SET, ASCOLTA/NOTIFICA, blocchi consultivi, cursori che sopravvivono alla transazione, istruzioni preparate denominate. Questo articolo dettaglia il meccanismo, elenca questi limiti con i loro esatti sintomi, poi mostra come un backend in produzione (il nostro, verificato direttamente nel suo repository) lo configura senza esserne intrappolato. Per il metodo di misurazione alla base dei dati sulle prestazioni qui citati, consultare la nostra metodologia di benchmark .

L'essenziale

  • Modalità transazione: la connessione al server viene rilasciata al termine di ogni transazione, non quando il client si disconnette. Questa è la modalità più efficiente per condividere connessioni brevi di tipo API REST.
  • Incompatibile per costruzione con: sessione SET/RESET, LISTEN/NOTIFY, blocchi consultivi di sessione, cursori WITH HOLD, tabelle temporanee riutilizzate da una richiesta all'altra.
  • La trappola più comune nella pratica: le istruzioni preparate con nome, che diversi driver (sqlx, asyncpg, il driver JDBC pgjdbc) attivano per impostazione predefinita, possono essere riprodotte su un'altra connessione al server e innescare un errore come prepared statement does not exist sotto carico.
  • Dalla versione 1.21, PgBouncer può seguire le istruzioni preparate dal protocollo in modalità transazione (cache LRU tramite connessione al server). Ciò non impedisce di disabilitare la cache lato client se l'applicazione cambia search_path a ogni richiesta.
  • Verificato nel codice Aurabase: i pool tenant vengono eseguiti con statement_cache_capacity(0) e PgBouncer in pool_mode=transaction, mentre PostgREST rimane volontariamente in connessione diretta per il ricaricamento dello schema tramite LISTEN/NOTIFY.
#
Concetti

Le 3 modalità di pooling di PgBouncer

PgBouncer offre tre modalità, che differiscono solo nel momento in cui la connessione al server Postgres ritorna al pool comune. La documentazione ufficiale li nomina session, transaction e statement (pgbouncer.org/features.html, sezione "Modalità pooling", accesso il 24 agosto 2026).

ModaConnessione al server allentataCompatibilità della sessione
sessione (impostazione predefinita)Alla disconnessione del clientTotale: IMPOSTA, ASCOLTA, cursori, tutto funziona come dal vivo
transazioneAl termine di ogni transazione (COMMIT/ROLLBACK)Parziale: solo ciò che rimane locale alla transazione
dichiarazioneDopo ogni singola richiestaMinimo: transazioni multi-query esplicite vietate

La modalità Session è la più permissiva ma la meno efficace in termini di scalabilità: una connessione Postgres rimane riservata per un client finché rimane connesso, anche se non fa nulla tra due richieste. La modalità Statement è riservata a casi molto specifici (proxy di sola lettura, controlli di integrità) e interrompe anche le classiche transazioni esplicite. La modalità di transazione è il compromesso che domina nella pratica per un'API REST: ogni richiesta HTTP corrisponde generalmente a una singola breve transazione Postgres.

#
Meccanismo

Come funziona la modalità transazione, connessione per connessione

In modalità transazione, PgBouncer collega una connessione server a un client solo quando quest'ultimo apre una transazione e la restituisce al pool dopo COMMIT o ROLLBACK. Tra due transazioni, lo stesso client potrebbe trovarsi riassegnato a una connessione server completamente diversa.

Concretamente, con un default_pool_size di 20, PgBouncer può assorbire diverse centinaia di clienti simultanei che, in ogni momento, hanno solo poche transazioni effettivamente in corso. È questo rapporto che giustifica la modalità di transazione per un'API REST con traffico elevato ma transazioni brevi: la risorsa rara (una connessione Postgres, costosa in memoria lato server) viene occupata solo per il tempo strettamente necessario.

pgbouncer.iniini
[pgbouncer]
listen_port = 5432

; La connexion serveur est libérée dès la fin de chaque transaction
pool_mode = transaction

default_pool_size = 80
max_client_conn = 1000

; Ne pas utiliser avec des sessions stateful (SET, advisory locks, LISTEN/NOTIFY)

Quest'ultimo commento riassume l'essenza: la modalità transazione funziona perché interrompe deliberatamente il collegamento tra "la mia sessione dell'applicazione" e "la mia connessione Postgres". Tutto basato su questo collegamento si interrompe. La sezione successiva elenca esattamente cosa.

#
Limiti

Cosa si interrompe nella modalità di pooling delle transazioni

La documentazione ufficiale di PgBouncer elenca esplicitamente le funzionalità di PostgreSQL che perdono il loro significato non appena una connessione al server può essere riciclata tra due richieste dello stesso client.

Funzionalità interessataPerché si rompeSintomo tipico
IMPOSTA/IMPOSTA SESSIONEL'impostazione si applica a una connessione che può essere riciclata immediatamente dopoUn parametro sembra essere dimenticato casualmente tra due richieste
ASCOLTA/INFORMAPresuppone una connessione permanente per ricevere notificheIl client non viene mai informato o solo in modo intermittente
Blocchi consultivi sulla sessioneIl blocco è mantenuto dalla connessione al server, non dal client logicoUn blocco viene rilasciato prima del completamento previsto oppure non viene mai rilasciato
CON cursori ATTESADeve sopravvivere oltre la transazione che lo ha apertoErrore "il cursore non esiste" alla successiva iterazione
Tabelle temporaneeRelativo alla sessione Postgres, non alla transazioneLa tabella "scompare" nella query successiva
Dichiarazioni preparate denominatePreparato su una connessione server specifica, riprodotto su un'altra"istruzione preparata... non esiste" sotto carico
La trappola non è sempre immediata

La maggior parte di queste limitazioni non si manifestano nello sviluppo locale, dove in genere una singola connessione serve tutto il traffico. Appaiono in condizioni di carico reale, quando diversi client condividono effettivamente il pool e una connessione al server passa effettivamente di mano tra due richieste dallo stesso client logico. Un test del fumo non li rivela quasi mai.

#
Trappola comune

Dichiarazioni preparate: il limite più frainteso

La maggior parte dei moderni driver Postgres preparano richieste denominate sul lato protocollo per impostazione predefinita, senza che il codice dell'applicazione lo richieda esplicitamente. Questo è proprio ciò che rende difficile anticipare questa trappola.

Un'istruzione preparata dal protocollo viene denominata e memorizzata nella cache su una connessione server specifica, al momento di Parse. In modalità transazione, questa connessione può essere riassegnata a un altro client tra due richieste provenienti dallo stesso client logico. Se il driver riproduce quindi lo stesso nome dell'istruzione su una connessione in cui non è mai stato preparato, Postgres risponde con un errore esplicito, in genere prepared statement "sqlx_s_N" does not exist per un client sqlx. Il comportamento è intermittente: dipende da come si comportano le connessioni sotto carico, non da un bug deterministico riproducibile ad ogni chiamata.

La correzione lato client è la stessa indipendentemente dalla lingua: disabilitare la cache delle istruzioni preparate denominate o forzare query senza nome per qualsiasi pool che attraversa un pooler in modalità transazione. In Rust con sqlx, passa attraverso statement_cache_capacity(0) nelle opzioni di connessione.

pool.rsrust
let connect_options = url
    .parse::<PgConnectOptions>()?
    .statement_cache_capacity(0);

// Equivalenti: asyncpg -> Statement_cache_size=0, pgjdbc -> prepareThreshold=0

Dalla versione 1.21, PgBouncer allevia parte del problema lato server: può seguire le istruzioni preparate dal protocollo in modalità transazione e prepararle al volo sulla connessione assegnata, con una cache LRU per connessione la cui dimensione viene regolata tramite max_prepared_statements. Ciò riduce il numero di errori, ma non esonera dal disabilitare la cache del client in un pool multi-tenant in cui search_path cambia da una richiesta all'altra: un piano memorizzato nella cache blocca l'identificatore interno (OID) della tabella risolta al momento di Parsee la sua riproduzione con un altro schema potrebbe restituire dati dal tenant sbagliato anziché un semplice errore.

#
Codice registrato

Come Aurabase configura PgBouncer in modalità transazione

Il repository Aurabase distribuisce PgBouncer come pool_mode=transaction davanti al piano dati condiviso (deploy/helm/aurabase/templates/infra/pgbouncer.yaml) e un CNPG Pooler configurato in modo identico davanti a ciascuna istanza Postgres dedicata di un tenant (deploy/cnpg/tenant-pooler.yaml). Entrambi i percorsi applicano la stessa disciplina sopra descritta.

Il codice sorgente documenta una specifica ragione di sicurezza per questa scelta, non solo una ragione di stabilità. I pool Postgres condivisi tra tenant posizionano un search_path diverso per progetto sulle connessioni riutilizzate. Un'istruzione preparata memorizzata nella cache blocca l'OID della tabella risolta al momento di Parse; riprodurlo per un altro tenant sulla stessa connessione eseguirebbe la query sullo schema del primo tenant, un bypass di isolamento, non solo un errore dell'applicazione. statement_cache_capacity(0) viene quindi applicato senza eccezioni, anche su istanze dedicate che passano anche attraverso un CNPG Pooler in modalità transazione.

Seconda misura di igiene della sessione: quando ogni connessione ritorna al pool, un hook esegue DISCARD ALL (reimpostazione delle impostazioni, deallocazione delle istruzioni preparate sul lato server, rilascio dei blocchi consultivi, eliminazione dei cursori e delle tabelle temporanee). Senza questo hook, un residuo di sessione posto da una richiesta precedente potrebbe fuoriuscire nella richiesta successiva da un tenant diverso che riutilizza la stessa connessione riciclata.

Eccezione accettata: le istanze PostgREST dedicate rimangono in connessione diretta al primario, senza passare attraverso il pooler. Il ricaricamento dello schema PostgREST si basa su LISTEN/NOTIFY, che presuppone una connessione persistente, esattamente la funzionalità interrotta dalla modalità di transazione (dettagli già documentati nel nostro articolo sulla compatibilità PostgREST su Aurabase). Le impostazioni RLS per richiesta vengono passate a SET LOCAL all'interno di una transazione esplicita, l'unico modo per rimanere compatibile con un pool che può modificare le connessioni del server in qualsiasi COMMIT (vedere il nostro articolo sull'isolamento RLSmulti-tenant).

#
Guida pratica

Abilita la modalità transazione senza interrompere l'applicazione

Una breve checklist, applicabile a qualsiasi backend che passa da una connessione Postgres diretta a un PgBouncer in modalità transazione.

  1. Controlla il codice dell'applicazione. Cerca SETdi non transazione, LISTEN/NOTIFY, blocchi consultivi di sessione, cursori WITH HOLD e tabelle temporanee riutilizzate tra le query.
  2. Sostituisci i SET di sessione con i SET LOCALI all'interno di una transazione esplicita. Questa è l'unica impostazione che sopravvive correttamente al riciclo della connessione, perché viene ripulita al COMMIT/ROLLBACK invece che perdere alla connessione successiva.
  3. Disabilita la cache delle istruzioni preparate lato driver se il pool attraversa il pooler e lo schema o il ruolo cambia da una richiesta all'altra. Il costo delle prestazioni è reale ma misurabile e molto inferiore al rischio di perdite tra inquilini.
  4. Isola le connessioni che necessitano realmente della modalità sessione (migrazioni, script di amministrazione, tutto ciò che dipende da LISTEN/NOTIFY) su una connessione diretta non pooler, anziché rinunciare alla modalità transazione per tutto il resto del traffico.
  5. Dimensioni default_pool_size e max_client_conn relative all'effettivo max_connectionsdi Postgres, non da una cifra arbitraria copiata da un altro progetto.
  6. Test sotto carico reale, non solo test del fumo. Gli errori delle istruzioni preparate e le perdite di impostazioni della sessione non vengono quasi mai visualizzati su una singola connessione locale.
  7. Monitora SHOW POOLS e SHOW STATS dalla console di amministrazione PgBouncer una volta in produzione, per individuare la saturazione del pool prima che diventi visibile sul lato client.
#
Decisione

Dovresti sempre scegliere la modalità di transazione anziché la sessione?

No, ma è la scelta predefinita corretta per la stragrande maggioranza delle API REST. La modalità sessione rimane preferibile per un'applicazione legacy fortemente dipendente dalle funzionalità di sessione di cui non è possibile eseguire il refactoring rapidamente o per un traffico ridotto in cui il guadagno di pooling non compensa lo sforzo di migrazione.

PgBouncer non è nemmeno l'unica implementazione di questo modello di pooling: Supavisor (Supabase) e PgCat sono due alternative recenti, con diversi compromessi sulla distribuzione del carico e sul clustering. Guarda il nostro confronto dettagliato, PgBouncer vs Supavisor vs PgCat, per scegliere tra i tre a seconda della tua topologia.

#
Domande frequenti

Domande frequenti

Le domande che sorgono più spesso una volta attivata la modalità transazione in produzione.

Cos'è la modalità di pooling delle transazioni di PgBouncer?+
Questa è una delle 3 modalità di PgBouncer (con sessione e istruzione) in cui la connessione Postgres viene riassegnata a un altro client alla fine di ogni transazione, anziché quando il client si disconnette. Ciò rende possibile servire molti più clienti concorrenti di quanti siano effettivamente le connessioni Postgres aperte.
Perché i miei estratti conto preparati si bloccano in modalità transazione?+
Un'istruzione preparata denominata viene preparata su una connessione server specifica. In modalità transazione, questa connessione può essere riassegnata a un altro client tra due richieste. Se il tuo driver riproduce il nome dell'istruzione su una connessione in cui non è mai stata preparata, Postgres restituisce un errore come l'istruzione preparata non esiste. La correzione consiste nel disabilitare la cache delle istruzioni preparate sul lato driver (statement_cache_capacity(0) con sqlx, Statement_cache_size=0 con asyncpg).
Possiamo usare ASCOLTA/NOTIFICA dietro un PgBouncer in modalità transazione?+
No, non in modo affidabile. ASCOLTA/NOTIFICA presuppone una connessione persistente per ricevere notifiche, modalità di transazione che non garantisce. La pratica standard consiste nel passare i componenti che dipendono da LISTEN/NOTIFY (PostgREST, ad esempio) attraverso una connessione diretta a Postgres, all'esterno del pooler.
Dovremmo usare SET LOCAL anziché SET in modalità transazione?+
Sì, sistematicamente per qualsiasi impostazione che debba applicarsi a una determinata richiesta. SET LOCAL viene ripulito automaticamente su COMMIT o ROLLBACK, rendendolo sicuro con una connessione al server che può cambiare tra due transazioni. Un SET classico può passare al client successivo che ripristina la stessa connessione al server riciclata.
La modalità transazione funziona con la sicurezza a livello di riga (RLS)?+
Sì, a condizione che le attestazioni JWT o le variabili di sessione utilizzate dalle policy RLS siano impostate in LOCAL SET all'interno della transazione, non nella sessione SET. Questo è il modello descritto nel nostro articolo sull'isolamento della sicurezza a livello di riga multi-tenant.
PgBouncer, Supavisor, PgCat: quale scegliere?+
I tre implementano un modello di pooling simile, con differenze nel clustering, nella distribuzione del carico e nell'ecosistema (Supavisor è sviluppato da Supabase, PgCat è scritto in Rust). La scelta dipende soprattutto dalla topologia di implementazione e dai vincoli operativi esistenti: consulta il nostro confronto dedicato per i dettagli.

PRONTO PER L'IMPLEMENTAZIONE?

Il tuo backend in cinque minuti.

Nessuna carta di credito richiesta · 500 MB gratuiti · 50.000 MAU