← Journal technique

backend

pgBouncer, et le bug search_path qui a corrompu des données

pgBouncer, et le bug search_path qui a corrompu des données

Chaque connexion Postgres coûte de la vraie mémoire, et Postgres a une limite dure sur le nombre qu'il accepte. Un back-end Node sous vraie charge simultanée — surtout plusieurs instances tenant chacune leur propre pool — essaiera joyeusement d'en ouvrir plus. pgBouncer se place devant, garde un pool plus petit de vraies connexions, et multiplexe beaucoup de connexions client dessus. L'application croit avoir sa propre connexion ; elle ne l'a pas.

Je n'ai pas mis ça d'entrée dès le premier jour. Je l'ai ajouté une fois que l'échelle réelle attendue de la plateforme était concrète, après avoir commencé avec juste un back-end, un front-end et une base. Provisionner pour une échelle que tu n'as pas confirmée est sa propre erreur — j'ajoute la pièce quand le besoin est réel.

DATABASE_URL=postgresql://...@pgbouncer:6432/db   # exécution, via le pooler
DIRECT_URL=postgresql://...@postgres:5432/db      # migrations, droit vers Postgres

Le transaction pooling — le mode qui donne réellement le bénéfice du multiplexage — ne prend pas en charge les prepared statements ni l'état au niveau session, et les migrations DDL ont besoin des deux. Alors les migrations contournent le pooler entièrement via DIRECT_URL.

La partie qui m'a mordu

Le transaction pooling réutilise une vraie connexion entre différentes sessions client logiques, parfois en milieu de séquence. Si une session exécute SET search_path et que le pooling passe ensuite cette même connexion à un autre client, le réglage ne se réinitialise pas. Le client suivant hérite d'un contexte de schéma inattendu et lit ou écrit silencieusement le mauvais schéma.

Ce n'était pas théorique. Ça a corrompu des données de test et de seed pendant le développement — aucune erreur, aucun crash, juste des données qui atterrissent au mauvais endroit. C'est assez alarmant pour que je garde maintenant un garde-fou CI permanent : un test smoke qui fait tourner toute la stack via Docker Compose et martèle spécifiquement les requêtes à travers pgBouncer, pas contre une connexion directe.

Le correctif dans le code : ne te repose pas du tout sur un SET search_path au niveau session derrière un proxy en transaction pooling. Intègre le schéma dans la chaîne de connexion, ou qualifie complètement chaque référence de table. Traite search_path comme quelque chose qui ne doit jamais dépendre du fait que la réutilisation de connexion soit propre, parce qu'elle ne le sera pas.

Teste à travers le pooler dès le début. Une installation locale qui parle droit à Postgres ne fera jamais surgir cette classe de bug — elle n'apparaît qu'avec de vraies connexions poolées sous charge, ce qui veut généralement dire qu'elle apparaît d'abord quelque part de partagé ou de type production à moins que tu ne l'aies cherchée exprès.