Journal

Il parametro di avvio che PgBouncer rifiutava: statement_timeout e pgx

PgBouncer rifiuta un pacchetto di avvio con un'impostazione che non traccia, come statement_timeout. Lo ha risolto un SET subito dopo la connessione.

Il backend impostava tre parametri di PostgreSQL su ogni connessione come parametri di avvio: il fuso orario, statement_timeout e idle_in_transaction_session_timeout. Con PostgreSQL funziona. Passando da PgBouncer, che sul nostro cluster sta davanti al database, la connessione viene chiusa prima della prima query con:

unsupported startup parameter: statement_timeout

La correzione è piccola: application_name resta un parametro di avvio, il resto diventa un SET subito dopo la connessione. La parte lunga è spiegare perché questo è corretto nel session pooling e sarebbe sbagliato nel transaction pooling.

Cosa faceva il codice

Con pgx, i RuntimeParams della configurazione della connessione finiscono nel messaggio di avvio:

pcfg.ConnConfig.RuntimeParams["timezone"] = "UTC"
pcfg.ConnConfig.RuntimeParams["statement_timeout"] = "15000"
pcfg.ConnConfig.RuntimeParams["idle_in_transaction_session_timeout"] = "30000"
pcfg.ConnConfig.RuntimeParams["application_name"] = "elchi-web"

PostgreSQL tratta ogni parametro del messaggio di avvio che non fa parte del protocollo stesso come un'impostazione di runtime, applicata all'avvio del backend e mantenuta come default della sessione. Nessun round trip, e le impostazioni valgono già prima della prima query. È uno schema pulito, ed è per questo che è diffuso.

Nessuno dei due timeout è decorativo. 15 secondi sono il massimo che può durare un'istruzione di questa applicazione; senza, una query lenta tiene occupate una richiesta e una connessione del pool per tutto il tempo che vuole. 30 secondi sono il tempo per cui una connessione può restare inattiva dentro una transazione aperta prima che il server la chiuda, così una transazione dimenticata non può tenere i suoi lock per sempre.

Cosa fa PgBouncer con un pacchetto di avvio

PgBouncer legge il pacchetto di avvio un parametro alla volta. database, user e options li gestisce da sé, e così anche application_name. Un parametro che traccia finisce nella sua cache delle impostazioni del client. Un parametro elencato in ignore_startup_parameters viene scartato. Qualsiasi altra cosa chiude la connessione con il messaggio riportato sopra.

Ciò che traccia di default è un elenco breve, secondo la sua documentazione: application_name, client_encoding, DateStyle, default_transaction_read_only, IntervalStyle, scram_iterations (PostgreSQL 16 e successivi), search_path (PostgreSQL 18 e successivi), session_authorization, standard_conforming_strings e TimeZone. Sono parametri che PostgreSQL comunica al client quando cambiano, ed è così che PgBouncer riesce a seguirli e a ripristinarli su qualunque connessione al server il client riceva dopo. statement_timeout non viene comunicato, quindi PgBouncer non può seguirlo, e lo rifiuta piuttosto che fingere.

TimeZone è nell'elenco, quindi quello sarebbe passato. Quale dei due timeout compaia nell'errore dipende dall'ordine dei parametri nel pacchetto, e pgx riempie il pacchetto partendo da una map di Go, quindi può essere l'uno o l'altro.

Due impostazioni che sembrano correzioni

PgBouncer ha due opzioni che fanno sparire l'errore. Nessuna delle due era quella che volevo.

ignore_startup_parameters = statement_timeout lascia passare la connessione e scarta il parametro. Da lì in poi ogni istruzione gira senza timeout, e niente Glielo segnala. È peggio dell'errore, che almeno ferma l'avvio: il nostro Open fa un ping al database prima che giri qualsiasi altra cosa, quindi il processo fallisce invece di servire richieste.

track_extra_parameters chiede a PgBouncer di tenere un valore nella sua cache e di ripristinarlo sul server. La sua documentazione dice chiaramente che la maggior parte dei parametri non può essere tracciata completamente in questo modo, perché PgBouncer viene a sapere di un SET solo per i parametri che PostgreSQL comunica. C'è dell'altro: PgBouncer appartiene al cluster, e lo condividono tutte le applicazioni che ci girano. Un'applicazione che funziona solo con impostazioni speciali del pooler è una cosa in più da ricordare per quella successiva.

La correzione

Le impostazioni sono passate nell'AfterConnect di pgxpool, che gira su ogni nuova connessione prima che venga aggiunta al pool:

const sessionSettings = `SET TIME ZONE 'UTC'; SET statement_timeout = 15000; SET idle_in_transaction_session_timeout = 30000`

pcfg.ConnConfig.RuntimeParams["application_name"] = "elchi-web"
pcfg.AfterConnect = func(ctx context.Context, c *pgx.Conn) error {
	_, err := c.Exec(ctx, sessionSettings)
	return err
}

pgx invia una query senza argomenti con il protocollo semplice, che ammette più istruzioni in una stringa, quindi questo costa un round trip per ogni nuova connessione, non per ogni query. Se fallisce, la connessione non arriva mai nel pool.

application_name resta un parametro di avvio. PgBouncer lo accetta, ed è utile fin dal primo istante, in pg_stat_activity e nei log del server. Il fuso orario si è spostato insieme ai timeout, anche se PgBouncer l'avrebbe accettato, così ogni impostazione su cui il codice fa affidamento sta in un'unica istruzione, in un unico punto.

Perché è sicuro nel session pooling

Nel session pooling, PgBouncer assegna una connessione al server a un client quando questo si connette e gliela lascia finché non si disconnette. Un SET subito dopo la connessione vale su quella connessione al server per tutto il tempo in cui il client la tiene, cioè per tutta la vita della connessione del pool. Quando il client se ne va, PgBouncer esegue la sua server_reset_query, DISCARD ALL di default, prima che la connessione al server passi a qualcun altro. I nostri 15 secondi non finiscono nelle query di un'altra applicazione.

Perché non sarebbe sicuro nel transaction pooling

Nel transaction pooling, un client ha una connessione al server solo per una transazione. Il SET di AfterConnect finirebbe su quella connessione al server che ha eseguito quell'unica istruzione, qualunque sia. Quella connessione torna poi nel pool e passa a un altro client, con i nostri timeout addosso, mentre la nostra transazione successiva gira su una connessione al server che non li ha. In modalità transazione la query di reset non viene eseguita, a meno che server_reset_query_always non sia attivo. La tabella delle funzionalità di PgBouncer stesso indica che SET e RESET non funzionano mai nel transaction pooling, e con loro gli advisory lock a livello di sessione.

Se Lei usa il transaction pooling, metta le impostazioni dove il server le applica da sé. ALTER ROLE ... SET statement_timeout = '15s' ne fa il default per ogni nuova sessione server di quel ruolo, qualunque cosa ci sia in mezzo, e SET LOCAL dentro una transazione vale solo per quella transazione. pgx prepara le istruzioni di default, e in quella modalità questo richiede anche max_prepared_statements di PgBouncer; in modalità sessione non richiede nulla.

Noi usiamo il session pooling, con quattro connessioni per istanza, quindi una connessione al server per ogni connessione del pool è comunque ciò che vogliamo.

Come lo abbiamo testato

TestSessionSettingsThroughPgBouncer esegue gli stessi controlli due volte: una direttamente sul database di test, una attraverso un vero PgBouncer in modalità sessione messo davanti. Ogni volta apre un pool di due, prende entrambe le connessioni insieme, così controlla due connessioni distinte e non due volte la prima, e su ognuna rilegge le impostazioni:

SELECT current_setting('TimeZone'), current_setting('statement_timeout'),
       current_setting('idle_in_transaction_session_timeout'), current_setting('application_name')

Si aspetta UTC, 15s, 30s e elchi-web. Poi esegue SELECT pg_sleep(16) e si aspetta che PostgreSQL la annulli a 15 secondi con uno statement timeout. Un'impostazione che si rilegge correttamente ma non fa nulla fallirebbe lì.

Con la versione precedente di Open, la metà con PgBouncer fallisce prima di tutto questo, alla prima connessione, esattamente con l'errore riportato sopra. La metà diretta passa in ogni caso, ed è tutto il problema in un solo test: una suite che parla con PostgreSQL solo direttamente non può vederlo.

Fonti

  1. PgBouncer configuration: ignore_startup_parameters, track_extra_parameters, server_reset_query, pool_mode, PgBouncer, consultato il
  2. PgBouncer features: pooling modes and what they support, PgBouncer, consultato il
  3. PgBouncer source: client.c, startup parameter handling, PgBouncer project, consultato il
  4. PostgreSQL documentation: Client Connection Defaults (statement_timeout, idle_in_transaction_session_timeout), PostgreSQL Global Development Group, consultato il
  5. PostgreSQL documentation: Message Formats, StartupMessage, PostgreSQL Global Development Group, consultato il
  6. PostgreSQL documentation: ALTER ROLE, PostgreSQL Global Development Group, consultato il
  7. pgxpool package documentation (Config.AfterConnect), pkg.go.dev, consultato il

Scritto da Samuel Krauss, Founder. Archiviato in postgresql, pgbouncer, go, pgx, connection-pool, statement-timeout.

Tradotto dall’inglese. Leggi l’originale inglese

Tutti gli articoli Questo articolo in Markdown Feed Atom