Paramètre de démarrage refusé par PgBouncer : statement_timeout et pgx
PgBouncer refuse un paquet de démarrage qui contient un réglage qu'il ne suit pas, comme statement_timeout. Un SET juste après la connexion a réglé le problème.
Le backend définissait trois réglages PostgreSQL sur chaque connexion sous forme de paramètres de démarrage : le fuseau horaire, statement_timeout et idle_in_transaction_session_timeout. Face à PostgreSQL, cela fonctionne. À travers PgBouncer, placé devant la base de données sur notre cluster, la connexion est fermée avant la première requête avec :
unsupported startup parameter: statement_timeout
La correction est petite : application_name reste un paramètre de démarrage, le reste devient un SET juste après la connexion. Pourquoi c'est correct en session pooling, et serait faux en transaction pooling, c'est la partie la plus longue.
Ce que faisait le code
Avec pgx, les RuntimeParams de la configuration de connexion vont dans le message de démarrage :
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 traite chaque paramètre du message de démarrage qui ne fait pas partie du protocole lui-même comme un réglage d'exécution, appliqué au lancement du backend et conservé comme valeur par défaut de la session. Pas d'aller-retour, et les réglages s'appliquent avant la première requête. C'est un schéma propre, et c'est pour cela qu'il est courant.
Aucun des deux timeouts n'est décoratif. 15 secondes, c'est le maximum qu'une instruction de cette application peut durer ; sans cette limite, une requête lente retient une requête HTTP et une connexion du pool aussi longtemps qu'elle veut. 30 secondes, c'est le temps qu'une connexion peut rester inactive dans une transaction ouverte avant que le serveur n'y mette fin, pour qu'une transaction oubliée ne puisse pas garder ses verrous indéfiniment.
Ce que fait PgBouncer d'un paquet de démarrage
PgBouncer lit le paquet de démarrage paramètre par paramètre. Il traite lui-même database, user et options, ainsi que application_name. Un paramètre qu'il suit va dans son cache des réglages du client. Un paramètre listé dans ignore_startup_parameters est écarté. Tout le reste met fin à la connexion avec le message ci-dessus.
Ce qu'il suit par défaut tient en une courte liste, tirée de sa documentation : application_name, client_encoding, DateStyle, default_transaction_read_only, IntervalStyle, scram_iterations (PostgreSQL 16 et suivants), search_path (PostgreSQL 18 et suivants), session_authorization, standard_conforming_strings et TimeZone. Ce sont des paramètres que PostgreSQL signale au client quand ils changent, et c'est ce qui permet à PgBouncer de les suivre et de les rétablir sur la connexion serveur que le client obtiendra ensuite, quelle qu'elle soit. statement_timeout n'est pas signalé : PgBouncer ne peut pas le suivre, et le refuse plutôt que de faire semblant.
TimeZone figure sur la liste, celui-là serait donc passé. Lequel des deux timeouts l'erreur nomme dépend de l'ordre des paramètres dans le paquet, et pgx remplit le paquet à partir d'une map Go : cela peut être l'un ou l'autre.
Deux réglages qui ressemblent à des corrections
PgBouncer a deux options qui font disparaître l'erreur. Je ne voulais ni l'une ni l'autre.
ignore_startup_parameters = statement_timeout laisse passer la connexion et écarte le paramètre. Chaque instruction tourne alors sans timeout, et rien ne vous le dit. C'est pire que l'erreur, qui au moins arrête le démarrage : notre Open interroge la base de données avant que quoi que ce soit d'autre ne tourne, si bien que le processus échoue au lieu de servir.
track_extra_parameters demande à PgBouncer de garder une valeur dans son cache et de la rétablir sur le serveur. Sa documentation dit clairement que la plupart des paramètres ne peuvent pas être entièrement suivis de cette façon, puisqu'il n'apprend l'existence d'un SET que pour les paramètres que PostgreSQL signale. Au-delà de cela, PgBouncer appartient au cluster, que partagent toutes les applications qui y tournent. Une application qui ne fonctionne qu'avec des réglages spéciaux du pooler, c'est une chose de plus à ne pas oublier pour la suivante.
La correction
Les réglages sont passés dans le AfterConnect de pgxpool, qui s'exécute sur chaque nouvelle connexion avant qu'elle ne soit ajoutée au 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 envoie une requête sans arguments par le protocole simple, qui accepte plusieurs instructions dans une même chaîne : cela coûte un aller-retour par nouvelle connexion, pas par requête. Si cela échoue, la connexion n'atteint jamais le pool.
application_name reste un paramètre de démarrage. PgBouncer l'accepte, et il est utile dès le tout premier instant, dans pg_stat_activity et dans les logs du serveur. Le fuseau horaire a suivi les timeouts, bien que PgBouncer l'aurait accepté, pour que chaque réglage dont dépend le code vive dans une seule instruction, à un seul endroit.
Pourquoi c'est sûr en session pooling
En session pooling, PgBouncer attribue une connexion serveur à un client quand il se connecte et la lui laisse jusqu'à ce qu'il se déconnecte. Un SET juste après la connexion tient sur cette connexion serveur aussi longtemps que le client la détient, c'est-à-dire pendant toute la vie de la connexion du pool. Quand le client s'en va, PgBouncer exécute sa server_reset_query, DISCARD ALL par défaut, avant que la connexion serveur ne passe à quelqu'un d'autre. Nos 15 secondes ne fuient pas dans les requêtes d'une autre application.
Pourquoi ce ne serait pas sûr en transaction pooling
En transaction pooling, un client ne dispose d'une connexion serveur que le temps d'une transaction. Le SET d'AfterConnect atterrirait sur la connexion serveur qui a exécuté cette instruction-là. Cette connexion retourne ensuite au pool et passe à un autre client, avec nos timeouts, pendant que notre transaction suivante tourne sur une connexion serveur qui ne les a pas. La requête de réinitialisation ne s'exécute pas en mode transaction, sauf si server_reset_query_always est activé. Le tableau des fonctionnalités de PgBouncer lui-même indique que SET et RESET ne fonctionnent jamais en transaction pooling, pas plus que les advisory locks au niveau de la session.
Si vous êtes en transaction pooling, placez les réglages là où le serveur les applique lui-même. ALTER ROLE ... SET statement_timeout = '15s' en fait la valeur par défaut de chaque nouvelle session serveur de ce rôle, quoi qu'il y ait entre les deux, et SET LOCAL dans une transaction ne vaut que pour cette transaction. pgx prépare les instructions par défaut, ce qui, dans ce mode, exige aussi le max_prepared_statements de PgBouncer ; en mode session, cela n'exige rien.
Nous sommes en session pooling, avec quatre connexions par instance : une connexion serveur par connexion du pool, c'est de toute façon ce que nous voulons.
Comment cela a été testé
TestSessionSettingsThroughPgBouncer exécute deux fois les mêmes vérifications : une fois directement contre la base de données de test, une fois à travers un vrai PgBouncer en mode session placé devant elle. À chaque fois, il ouvre un pool de deux, prend les deux connexions en même temps, pour vérifier deux connexions distinctes plutôt que deux fois la première, et relit les réglages sur chacune :
SELECT current_setting('TimeZone'), current_setting('statement_timeout'),
current_setting('idle_in_transaction_session_timeout'), current_setting('application_name')
Il attend UTC, 15s, 30s et elchi-web. Puis il exécute SELECT pg_sleep(16) et attend de PostgreSQL qu'il l'annule à 15 secondes pour statement timeout. Un réglage qui se relit correctement mais ne fait rien échouerait là.
Contre la version précédente d'Open, la moitié PgBouncer échoue avant tout cela, dès la première connexion, avec exactement l'erreur citée plus haut. La moitié directe passe dans les deux cas, et c'est tout le problème résumé en un test : une suite qui ne parle qu'à PostgreSQL en direct ne peut pas le voir.
Sources
- PgBouncer configuration: ignore_startup_parameters, track_extra_parameters, server_reset_query, pool_mode, PgBouncer, consulté le
- PgBouncer features: pooling modes and what they support, PgBouncer, consulté le
- PgBouncer source: client.c, startup parameter handling, PgBouncer project, consulté le
- PostgreSQL documentation: Client Connection Defaults (statement_timeout, idle_in_transaction_session_timeout), PostgreSQL Global Development Group, consulté le
- PostgreSQL documentation: Message Formats, StartupMessage, PostgreSQL Global Development Group, consulté le
- PostgreSQL documentation: ALTER ROLE, PostgreSQL Global Development Group, consulté le
- pgxpool package documentation (Config.AfterConnect), pkg.go.dev, consulté le
Écrit par Samuel Krauss, Founder. Classé sous postgresql, pgbouncer, go, pgx, connection-pool, statement-timeout.
Traduit de l’anglais. Lire l’original anglais