Journal

Der Startparameter, den PgBouncer abwies: statement_timeout und pgx

PgBouncer weist ein Startpaket mit einer Einstellung ab, die es nicht verfolgt, etwa statement_timeout. Ein SET nach dem Verbinden hat es behoben.

Das Backend setzte bei jeder Connection drei PostgreSQL-Einstellungen als Startparameter: die Zeitzone, statement_timeout und idle_in_transaction_session_timeout. Gegen PostgreSQL funktioniert das. Über PgBouncer, der auf unserem Cluster vor der Datenbank sitzt, wird die Connection vor der ersten Query geschlossen, mit:

unsupported startup parameter: statement_timeout

Die Lösung ist klein: application_name bleibt ein Startparameter, der Rest ist ein SET direkt nach dem Verbinden. Warum das beim Session Pooling richtig ist und beim Transaction Pooling falsch wäre, ist der längere Teil.

Was der Code tat

Mit pgx landen die RuntimeParams der Connection-Konfiguration in der Startnachricht:

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 behandelt jeden Parameter in der Startnachricht, der nicht zum Protokoll selbst gehört, als Laufzeiteinstellung, die beim Start des Backends angewendet und als Standard der Session behalten wird. Kein Roundtrip, und die Einstellungen gelten vor der ersten Query. Das Muster ist sauber, deshalb ist es verbreitet.

Keines der beiden Timeouts ist Dekoration. 15 Sekunden sind das Maximum, das ein Statement dieser Anwendung dauern darf; ohne diese Grenze hält eine langsame Query einen Request und eine Pool-Connection, so lange sie will. 30 Sekunden darf eine Connection untätig in einer offenen Transaktion verharren, bevor der Server sie beendet, damit eine vergessene Transaktion ihre Locks nicht für immer hält.

Was PgBouncer mit einem Startpaket macht

PgBouncer liest das Startpaket Parameter für Parameter. database, user und options behandelt es selbst, ebenso application_name. Ein Parameter, den es verfolgt, kommt in seinen Cache der Client-Einstellungen. Ein Parameter, der in ignore_startup_parameters steht, wird verworfen. Alles andere beendet die Connection mit der Meldung oben.

Was es standardmässig verfolgt, ist laut seiner Dokumentation eine kurze Liste: application_name, client_encoding, DateStyle, default_transaction_read_only, IntervalStyle, scram_iterations (ab PostgreSQL 16), search_path (ab PostgreSQL 18), session_authorization, standard_conforming_strings und TimeZone. Das sind Parameter, die PostgreSQL dem Client meldet, wenn sie sich ändern. So kann PgBouncer ihnen folgen und sie auf der Server-Connection wiederherstellen, die der Client als Nächstes bekommt. statement_timeout wird nicht gemeldet, also kann PgBouncer ihm nicht folgen und weist es ab, statt so zu tun als ob.

TimeZone steht auf der Liste, das wäre also durchgegangen. Welches der beiden Timeouts der Fehler nennt, hängt von der Reihenfolge der Parameter im Paket ab, und pgx füllt das Paket aus einer Go-Map, es kann also jedes der beiden sein.

Zwei Einstellungen, die wie Lösungen aussehen

PgBouncer hat zwei Optionen, die den Fehler verschwinden lassen. Keine davon wollte ich.

ignore_startup_parameters = statement_timeout lässt die Connection durch und verwirft den Parameter. Jedes Statement läuft dann ohne Timeout, und nichts sagt es Ihnen. Das ist schlimmer als der Fehler, der wenigstens den Start stoppt: Unser Open pingt die Datenbank, bevor irgendetwas anderes läuft, also scheitert der Prozess, statt Anfragen zu bedienen.

track_extra_parameters bittet PgBouncer, einen Wert in seinem Cache zu halten und auf dem Server wiederherzustellen. Die Dokumentation sagt klar, dass sich die meisten Parameter so nicht vollständig verfolgen lassen, denn PgBouncer erfährt von einem SET nur bei Parametern, die PostgreSQL meldet. Ausserdem gehört PgBouncer zum Cluster, den sich alle Anwendungen darauf teilen. Eine Anwendung, die nur mit besonderen Pooler-Einstellungen funktioniert, ist eine Sache mehr, an die man bei der nächsten denken muss.

Die Lösung

Die Einstellungen sind in das AfterConnect von pgxpool gewandert, das bei jeder neuen Connection läuft, bevor sie in den Pool kommt:

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 schickt eine Query ohne Argumente über das Simple Protocol, das mehrere Statements in einem String erlaubt. Das kostet also einen Roundtrip pro neuer Connection, nicht pro Query. Scheitert es, erreicht die Connection den Pool nie.

application_name bleibt ein Startparameter. PgBouncer akzeptiert ihn, und er ist vom ersten Moment an nützlich, in pg_stat_activity und in den Logs des Servers. Die Zeitzone ist mit den Timeouts umgezogen, obwohl PgBouncer sie akzeptiert hätte, damit jede Einstellung, auf die sich der Code verlässt, in einem Statement an einer Stelle steht.

Warum das beim Session Pooling sicher ist

Beim Session Pooling weist PgBouncer einem Client beim Verbinden eine Server-Connection zu und lässt sie zugewiesen, bis der Client die Verbindung trennt. Ein SET direkt nach dem Verbinden gilt auf dieser Server-Connection, solange der Client sie hat, also während der Lebensdauer der Pool-Connection. Geht der Client, führt PgBouncer seine server_reset_query aus, standardmässig DISCARD ALL, bevor die Server-Connection an jemand anderen geht. Unsere 15 Sekunden sickern nicht in die Queries einer anderen Anwendung.

Warum es beim Transaction Pooling nicht sicher wäre

Beim Transaction Pooling hat ein Client eine Server-Connection nur für eine Transaktion. Das SET aus AfterConnect würde auf der Server-Connection landen, die dieses eine Statement ausgeführt hat. Diese Connection geht dann zurück in den Pool und weiter an einen anderen Client, mit unseren Timeouts darauf, während unsere nächste Transaktion auf einer Server-Connection ohne sie läuft. Die Reset-Query läuft im Transaction-Modus nicht, ausser server_reset_query_always ist eingeschaltet. Die eigene Feature-Tabelle von PgBouncer führt SET und RESET als Dinge auf, die beim Transaction Pooling nie funktionieren, und Advisory Locks auf Session-Ebene gleich mit.

Nutzen Sie Transaction Pooling, legen Sie die Einstellungen dorthin, wo der Server sie selbst anwendet. ALTER ROLE ... SET statement_timeout = '15s' macht den Wert zum Standard für jede neue Server-Session dieser Rolle, egal was dazwischen sitzt, und SET LOCAL innerhalb einer Transaktion gilt nur für diese Transaktion. pgx bereitet Statements standardmässig vor, was in diesem Modus ausserdem max_prepared_statements von PgBouncer braucht; im Session-Modus braucht es nichts.

Wir nutzen Session Pooling, mit vier Connections pro Instanz, also ist eine Server-Connection pro Pool-Connection ohnehin das, was wir wollen.

Wie es getestet wurde

TestSessionSettingsThroughPgBouncer führt dieselben Prüfungen zweimal aus: einmal direkt gegen die Testdatenbank, einmal über einen echten PgBouncer im Session-Modus davor. Jedes Mal öffnet der Test einen Pool von zwei, nimmt beide Connections gleichzeitig, damit er zwei getrennte Connections prüft und nicht zweimal die erste, und liest auf jeder die Einstellungen zurück:

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

Er erwartet UTC, 15s, 30s und elchi-web. Dann führt er SELECT pg_sleep(16) aus und erwartet, dass PostgreSQL es nach 15 Sekunden mit einem Statement Timeout abbricht. Eine Einstellung, die korrekt zurückgelesen wird, aber nichts bewirkt, würde dort scheitern.

Gegen die vorige Version von Open scheitert die PgBouncer-Hälfte vor all dem, bei der ersten Connection, mit genau dem Fehler oben. Die direkte Hälfte besteht so oder so, und darin liegt das ganze Problem in einem Test: Eine Testsuite, die nur direkt mit PostgreSQL spricht, kann es nicht sehen.

Quellen

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

Geschrieben von Samuel Krauss, Founder. Abgelegt unter postgresql, pgbouncer, go, pgx, connection-pool, statement-timeout.

Übersetzt aus dem Englischen. Zum englischen Original

Alle Beiträge Dieser Beitrag als Markdown Atom-Feed