PRODSoeverein Europees BaaS-platformOpen Dashboard →

Prestaties · 10 min gelezen

PgBouncer transactiepooling uitgelegd

Affane Daylami · Fondateur · 12 juni 2026

Terug naar blog

De transactiemodus van PgBouncer geeft de PostgreSQL-verbinding vrij aan het einde van elke transactie, niet wanneer de client de verbinding verbreekt. Dit maakt het mogelijk om duizenden HTTP-clients te bedienen met enkele tientallen daadwerkelijke serververbindingen, en het is de aanbevolen modus voor elke REST API met korte zoekopdrachten.

Deze Engelse tekst is automatisch gegenereerd op basis van het Franse origineel en is nog niet beoordeeld.
Deze pagina is automatisch vertaald. De Engelse versie is gezaghebbend.

Deze winst brengt specifieke kosten met zich mee: de transactiemodus verbreekt stilletjes alles wat een stabiele Postgres-verbinding veronderstelt, van het ene verzoek naar het volgende. Sessie SET, LISTEN/NOTIFY, adviesvergrendelingen, cursors die de transactie overleven, benoemde voorbereide verklaringen. Dit artikel beschrijft het mechanisme, somt deze limieten op met hun exacte symptomen, en laat vervolgens zien hoe een backend in productie (de onze, rechtstreeks geverifieerd in de repository) deze configureert zonder erdoor gevangen te worden. Voor de meetmethode achter de hier genoemde prestatiecijfers, zie onze benchmarkmethodologie.

De essentie

  • Transactiemodus: de serververbinding wordt aan het einde van elke transactie vrijgegeven, niet wanneer de client de verbinding verbreekt. Dit is de meest efficiënte modus voor het delen van korte verbindingen van het REST API-type.
  • Incompatibel door constructie met: sessie SET/RESET, LISTEN/NOTIFY, sessie-adviesvergrendelingen, WITH HOLD-cursors, tijdelijke tabellen die van het ene verzoek naar het andere worden hergebruikt.
  • De meest voorkomende valkuil in de praktijk: benoemde voorbereide instructies, die standaard door verschillende stuurprogramma's (sqlx, asyncpg, het JDBC pgjdbc-stuurprogramma) worden geactiveerd, kunnen op een andere serververbinding worden afgespeeld en onder belasting een fout als prepared statement does not exist activeren.
  • Sinds versie 1.21 kan PgBouncer protocol-opgestelde instructies volgen in transactiemodus (LRU-cache via serververbinding). Dit verhindert niet dat u de cache aan de clientzijde uitschakelt als uw toepassing bij elk verzoek search_path wijzigt.
  • Geverifieerd in de Aurabase-code: de tenantpools draaien met statement_cache_capacity(0) en PgBouncer in pool_mode=transaction, terwijl PostgREST vrijwillig in directe verbinding blijft voor het herladen van het schema via LISTEN/NOTIFY.
#
Concepten

De 3 poolmodi van PgBouncer

PgBouncer biedt drie modi, die alleen verschillen wanneer de Postgres-serververbinding terugkeert naar de gemeenschappelijke pool. De officiële documentatie noemt ze session, transaction en statement (pgbouncer.org/features.html, sectie "Poolingmodi", geraadpleegd op 24 augustus 2026).

ModeSerververbinding losCompatibiliteit van sessies
sessie (standaard)Bij het verbreken van de clientverbindingTotaal: SET, LISTEN, cursors, alles werkt als live
transactieAan het einde van elke transactie (COMMIT/ROLLBACK)Gedeeltelijk: alleen wat lokaal blijft voor de transactie
verklaringNa elk individueel verzoekMinimaal: expliciete transacties met meerdere zoekopdrachten zijn verboden

De sessiemodus is het meest tolerant, maar het minst effectief wat betreft schaalbaarheid: een Postgres-verbinding blijft gereserveerd voor een client zolang deze verbonden blijft, zelfs als deze tussen twee verzoeken niets doet. De verklaringsmodus is gereserveerd voor zeer specifieke gevallen (alleen-lezen proxy, gezondheidscontroles) en verbreekt zelfs klassieke expliciete transacties. De transactiemodus is het compromis dat in de praktijk domineert voor een REST API: elk HTTP-verzoek komt doorgaans overeen met een enkele korte Postgres-transactie.

#
Mechanisme

Hoe de transactiemodus werkt, verbinding voor verbinding

In de transactiemodus maakt PgBouncer alleen een serververbinding met een client wanneer deze een transactie opent, en stuurt deze terug naar de pool bij COMMIT of ROLLBACK. Tussen twee transacties kan het voorkomen dat dezelfde client opnieuw wordt toegewezen aan een geheel andere serververbinding.

Concreet kan PgBouncer met een default_pool_size van 20 honderden gelijktijdige klanten absorberen die op een gegeven moment slechts een paar lopende transacties hebben. Het is deze verhouding die de transactiemodus rechtvaardigt voor een REST API met veel verkeer maar korte transacties: de zeldzame bron (een Postgres-verbinding, duur in geheugen aan de serverzijde) wordt alleen bezet voor de tijd die strikt noodzakelijk is.

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)

Deze laatste opmerking vat de essentie samen: de transactiemodus werkt omdat het opzettelijk de link tussen "mijn applicatiesessie" en "mijn Postgres-verbinding" verbreekt. Alles gebaseerd op deze link gaat kapot. In het volgende gedeelte wordt precies vermeld wat.

#
Grenzen

Wat breekt er in de modus voor transactiepooling

De officiële PgBouncer-documentatie vermeldt expliciet PostgreSQL-functies die hun betekenis verliezen zodra een serververbinding kan worden gerecycled tussen twee verzoeken van dezelfde client.

Beïnvloede functionaliteitWaarom breekt hetTypisch symptoom
INSTELLEN / SESSIE INSTELLENDe instelling geldt voor een verbinding die direct daarna kan worden gerecycledHet lijkt erop dat een parameter tussen twee verzoeken willekeurig wordt vergeten
LUISTEREN / MELDENEr wordt uitgegaan van een permanente verbinding om meldingen te ontvangenDe klant wordt nooit of slechts sporadisch op de hoogte gebracht
Sessie-adviesslotenDe vergrendeling wordt vastgehouden door de serververbinding, niet door de logische clientEen vergrendeling wordt vrijgegeven vóór de verwachte voltooiing, of wordt nooit vrijgegeven
MET HOLD-schuifregelaarsMoet overleven na de transactie waarmee het werd geopendFout 'cursor bestaat niet' bij de volgende iteratie
Tijdelijke tafelsGerelateerd aan de Postgres-sessie, niet aan de transactieDe tabel "verdwijnt" bij de volgende query
Opgestelde verklaringen genoemdVoorbereid op een specifieke serververbinding, afgespeeld op een andere"voorbereide verklaring... bestaat niet" onder belasting
De valstrik is niet altijd onmiddellijk

De meeste van deze beperkingen komen niet tot uiting in de lokale ontwikkeling, waar doorgaans één enkele verbinding al het verkeer bedient. Ze verschijnen onder echte belasting, wanneer verschillende clients de pool daadwerkelijk delen en een serververbinding daadwerkelijk van eigenaar wisselt tussen twee verzoeken van dezelfde logische client. Een rooktest brengt ze vrijwel nooit aan het licht.

#
Gemeenschappelijke val

Opgestelde verklaringen: de meest onbegrepen limiet

De meeste moderne Postgres-stuurprogramma's bereiden standaard benoemde verzoeken aan de protocolkant voor, zonder dat de applicatiecode hier expliciet om vraagt. Dit is precies wat het moeilijk maakt om op deze val te anticiperen.

Een op protocol voorbereide instructie wordt benoemd en in de cache opgeslagen op een specifieke serververbinding, op het moment van Parse. In de transactiemodus kan deze verbinding opnieuw worden toegewezen aan een andere client tussen twee verzoeken van dezelfde logische client. Als het stuurprogramma vervolgens dezelfde instructienaam opnieuw afspeelt op een verbinding waar deze nooit is voorbereid, reageert Postgres met een expliciete fout, meestal prepared statement "sqlx_s_N" does not exist voor een sqlx-client. Het gedrag is intermitterend: het hangt af van hoe de verbindingen presteren onder belasting, en niet van een deterministische bug die bij elke oproep reproduceerbaar is.

De correctie aan de clientzijde is hetzelfde, ongeacht de taal: schakel de cache van benoemde voorbereide instructies uit, of forceer niet-benoemde zoekopdrachten, voor elke pool die een pooler kruist in de transactiemodus. In Rust met sqlx gaat het via statement_cache_capacity(0) over de verbindingsopties.

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

// Equivalenten: asyncpg -> statement_cache_size=0, pgjdbc -> prepareThreshold=0

Sinds versie 1.21 verhelpt PgBouncer een deel van het probleem aan de serverzijde: het kan in het protocol voorbereide instructies volgen in de transactiemodus en deze direct voorbereiden op de toegewezen verbinding, met een LRU-cache per verbinding waarvan de grootte wordt aangepast via max_prepared_statements. Dit vermindert het aantal missers, maar ontslaat u niet van het uitschakelen van de clientcache in een pool met meerdere tenants waar de search_path van het ene verzoek naar het andere verandert: een in de cache opgeslagen plan bevriest de interne identificatie (OID) van de opgeloste tabel op het moment van Parse, en het opnieuw afspelen ervan onder een ander schema kan gegevens van de verkeerde tenant retourneren in plaats van een eenvoudige fout.

#
Code ingecheckt

Hoe Aurabase PgBouncer configureert in transactiemodus

De Aurabase-repository implementeert PgBouncer als pool_mode=transaction vóór het gedeelde datavlak (deploy/helm/aurabase/templates/infra/pgbouncer.yaml), en een identiek geconfigureerde Pooler CNPG vóór elke speciale Postgres-instantie van een tenant (deploy/cnpg/tenant-pooler.yaml). Bij beide trajecten wordt dezelfde discipline toegepast als hierboven beschreven.

De broncode documenteert een specifieke veiligheidsreden voor deze keuze, niet alleen een stabiliteitsreden. Postgres-pools die tussen tenants worden gedeeld, positioneren een andere search_path per project op hergebruikte verbindingen. Een in de cache opgeslagen instructie bevriest de OID van de opgeloste tabel op het moment van Parse; Als u de query opnieuw afspeelt voor een andere tenant op dezelfde verbinding, wordt de query uitgevoerd op basis van het schema van de eerste tenant, een isolatie-bypass en niet alleen een toepassingsfout. statement_cache_capacity(0) wordt daarom zonder uitzondering toegepast, ook op speciale instanties die ook in transactiemodus via een CNPG Pooler gaan.

Hygiënemaatregel voor de tweede sessie: wanneer elke verbinding terugkeert naar de pool, voert een hook DISCARD ALL uit (instellingen resetten, de toewijzing van voorbereide instructies aan de serverzijde ongedaan maken, adviesvergrendelingen vrijgeven, cursors en tijdelijke tabellen opschonen). Zonder deze hook zou een sessieresidu van een eerder verzoek kunnen lekken bij het volgende verzoek van een andere tenant die dezelfde gerecyclede verbinding hergebruikt.

Uitzondering geaccepteerd: speciale PostgREST-instanties blijven in directe verbinding met de primaire instantie, zonder via de pooler te gaan. Het herladen van het PostgREST-schema is afhankelijk van LISTEN/NOTIFY, dat uitgaat van een persistente verbinding, precies de functionaliteit die de transactiemodus verbreekt (details zijn al gedocumenteerd in ons artikel over PostgREST-compatibiliteit bij Aurabase). RLS-instellingen per verzoek worden binnen een expliciete transactie doorgegeven aan SET LOCAL, de enige manier om compatibel te blijven met een pool die serververbindingen bij elke COMMIT kan wijzigen (zie ons artikel overmulti-tenant RLS-isolatie).

#
Praktische gids

Schakel de transactiemodus in zonder uw applicatie te verbreken

Een korte checklist, toepasbaar op elke backend die overstapt van een directe Postgres-verbinding naar een PgBouncer in transactiemodus.

  1. Controleer de applicatiecode. Zoek naar niet-transactie SET, LISTEN/NOTIFY, sessie-adviesblokkeringen, WITH HOLD cursors en tijdelijke tabellen die tussen query's worden hergebruikt.
  2. Vervang sessie-SETs door LOCAL SETs binnen een expliciete transactie. Dit is de enige instelling die het recyclen van verbindingen goed overleeft, omdat deze wordt opgeschoond bij COMMIT/ROLLBACK in plaats van te lekken bij de volgende verbinding.
  3. Schakel de cache voor voorbereide instructies aan de bestuurderszijde uit als uw pool de pooler doorloopt en het schema of de rol van het ene verzoek naar het andere verandert. De prestatiekosten zijn reëel maar meetbaar, en veel lager dan het risico op lekkage tussen huurders.
  4. Isoleer verbindingen die echt de sessiemodus nodig hebben (migraties, beheerdersscripts, alles wat afhankelijk is van LISTEN/NOTIFY) naar een directe niet-poolerverbinding, in plaats van af te zien van de transactiemodus voor al het andere verkeer.
  5. Grootte default_pool_size en max_client_conn relatief ten opzichte van Postgres' werkelijke max_connections, niet door een willekeurig getal gekopieerd uit een ander project.
  6. Test onder echte belasting, niet alleen een rooktest. Opgestelde instructiefouten en lekken van sessie-instellingen verschijnen bijna nooit op een enkele lokale verbinding.
  7. Monitor SHOW POOLS en SHOW STATS vanaf de PgBouncer-beheerconsole zodra deze in productie is, om poolverzadiging te detecteren voordat deze zichtbaar wordt aan de clientzijde.
#
Besluit

Moet u altijd de transactiemodus kiezen in plaats van de sessie?

Nee, maar het is de juiste standaardkeuze voor de overgrote meerderheid van REST API's. De sessiemodus blijft de voorkeur genieten voor een oudere applicatie die sterk afhankelijk is van sessiefunctionaliteiten die u niet snel kunt herstructureren, of voor weinig verkeer waarbij de poolingwinst de migratie-inspanning niet compenseert.

PgBouncer is ook niet de enige implementatie van dit poolingmodel: Supavisor (Supabase) en PgCat zijn twee recente alternatieven, met verschillende afwegingen op het gebied van belastingverdeling en clustering. Bekijk onze gedetailleerde vergelijking, PgBouncer vs Supavisor vs PgCat, om tussen de drie te kiezen, afhankelijk van uw topologie.

#
Veelgestelde vragen

Veelgestelde vragen

De vragen die het vaakst naar voren komen zodra de transactiemodus in productie is geactiveerd.

Wat is de PgBouncer-transactiepoolingmodus?+
Dit is een van de drie modi van PgBouncer (met sessie en statement) waarbij de Postgres-verbinding aan het einde van elke transactie opnieuw wordt toegewezen aan een andere client, in plaats van wanneer de client de verbinding verbreekt. Dit maakt het mogelijk om veel meer concurrerende klanten te bedienen dan er daadwerkelijk open Postgres-verbindingen zijn.
Waarom crashen mijn voorbereide afschriften in de transactiemodus?+
Een benoemde voorbereide instructie wordt voorbereid op een specifieke serververbinding. In de transactiemodus kan deze verbinding tussen twee verzoeken opnieuw worden toegewezen aan een andere client. Als uw stuurprogramma de naam van de instructie opnieuw afspeelt op een verbinding waar deze nog nooit is voorbereid, retourneert Postgres een fout alsof de voorbereide instructie niet bestaat. De correctie bestaat uit het uitschakelen van de cache voor voorbereide instructies aan de stuurprogrammazijde (statement_cache_capacity(0) met sqlx, statement_cache_size=0 met asyncpg).
Kunnen we LISTEN/NOTIFY gebruiken achter een PgBouncer in transactiemodus?+
Nee, niet betrouwbaar. LISTEN/NOTIFY gaat uit van een permanente verbinding om meldingen te ontvangen, welke transactiemodus niet kan worden gegarandeerd. Standaardpraktijk is om componenten die afhankelijk zijn van LISTEN/NOTIFY (bijvoorbeeld PostgREST) ​​door te geven via een directe verbinding met Postgres, buiten de pooler.
Moeten we SET LOCAL gebruiken in plaats van SET in de transactiemodus?+
Ja, systematisch voor elke instelling die van toepassing moet zijn op een bepaald verzoek. SET LOCAL wordt automatisch opgeschoond bij COMMIT of ROLLBACK, waardoor het veilig is met een serververbinding die tussen twee transacties kan wisselen. Een klassieke SET kan lekken naar de volgende client, die dezelfde gerecyclede serververbinding herstelt.
Werkt de transactiemodus met Row Level Security (RLS)?+
Ja, op voorwaarde dat de JWT-claims of sessievariabelen die door uw RLS-beleid worden gebruikt, binnen de transactie zijn ingesteld in LOCAL SET, en niet in sessie-SET. Dit is het patroon dat wordt beschreven in ons artikel over RLS-isolatie met meerdere tenants.
PgBouncer, Supavisor, PgCat: welke moet je kiezen?+
De drie implementeren een soortgelijk poolingmodel, met verschillen in clustering, belastingverdeling en het ecosysteem (Supavisor is ontwikkeld door Supabase, PgCat is geschreven in Rust). De keuze hangt vooral af van uw implementatietopologie en uw bestaande operationele beperkingen: zie onze speciale vergelijking voor details.

KLAAR VOOR IMPLEMENTATIE?

Uw backend in vijf minuten.

Geen creditcard vereist · 500 MB gratis · 50.000 MAU