À passer avant la mise en production, puis à re-passer le jour où la charge double. Chaque point est vérifiable en moins de cinq minutes.
Pool de connexions#
-
max_connectionsdu pool entre 10 et 20 par instance, pas plus. Le calcul qui compte :instances × pool ≤ max_connections Postgres − 10(les 10 réservées aux migrations, aux sauvegardes et à ta sessionpsqlde secours). -
acquire_timeoutréglé à 3 secondes. Sans lui, une saturation du pool se transforme en requêtes qui pendent indéfiniment au lieu d’échouer vite. -
idle_timeoutà 10 minutes, pour que les connexions mortes côté réseau soient renouvelées. -
min_connectionsà 1 ou 2, pas plus : garder 20 connexions chaudes pour rien coûte de la mémoire à Postgres. - La profondeur de file du pool est exposée en métrique. C’est le premier signal d’une saturation, bien avant la latence.
Schéma et index#
-
Chaque requête de liste a son index composite, dans l’ordre exact
WHEREpuisORDER BY. UnWHERE user_id = $1 ORDER BY created_at DESCveut un index(user_id, created_at DESC), pas deux index séparés. -
EXPLAIN (ANALYZE, BUFFERS)passé sur les cinq requêtes les plus fréquentes, avec un volume de données réaliste. Sur 100 lignes, tout est rapide. -
Aucun
Seq Scansur une table de plus de 10 000 lignes dans un chemin utilisateur. -
Les clés étrangères sont indexées côté enfant. Postgres ne le fait pas automatiquement, et un
DELETEsur le parent scanne alors toute la table. -
CREATE INDEX CONCURRENTLYen production : la version sansCONCURRENTLYpose un verrou qui bloque les écritures pendant toute la construction. -
Les colonnes de dates sont en
timestamptz, jamais entimestamp. Le jour du changement d’heure arrive toujours. -
Les identifiants exposés publiquement sont des UUID v4 ou v7, pas des
serialqui révèlent ton volume d’affaires.
Requêtes#
-
Aucun N+1 : une liste de 50 éléments ne fait pas 51 requêtes. Un
JOINou unWHERE id = ANY($1)règle le problème. -
Pagination par curseur sur les listes qui grandissent.
OFFSET 10000fait lire 10 000 lignes à Postgres pour les jeter. - Les listes ont une limite maximale côté serveur. Un client ne décide pas de la charge du serveur.
-
statement_timeoutpositionné (5 à 10 secondes) au niveau du rôle applicatif. Une requête pathologique ne doit pas occuper une connexion pendant dix minutes.
Transactions#
- Les transactions ne contiennent aucun appel réseau externe. Une transaction ouverte pendant un appel HTTP de 2 secondes, c’est 2 secondes de verrous.
- Les opérations multi-tables qui doivent être cohérentes sont bien dans une transaction. La rotation d’un refresh token en est l’exemple type : révoquer l’ancien et insérer le nouveau, ou rien.
- Les verrous sont pris toujours dans le même ordre, sinon interblocage garanti sous charge.
-
Les jobs de fond qui tournent sur plusieurs instances utilisent
pg_advisory_lockpour ne pas se marcher dessus.
Migrations#
- Les migrations sont versionnées et jouées au démarrage, jamais à la main.
- Chaque migration est rétrocompatible avec la version précédente du code. Pendant un déploiement progressif, les deux versions tournent en même temps.
- Supprimer une colonne se fait en deux déploiements : d’abord le code arrête de la lire, ensuite seulement la colonne part.
-
Une migration sur une grosse table a été chronométrée sur une copie de la production. Un
ALTER TABLEqui réécrit 50 millions de lignes bloque tout.
Exploitation#
- Les sauvegardes existent et une restauration a été testée. Une sauvegarde jamais restaurée n’est pas une sauvegarde.
-
pg_stat_statementsest activé. Sans lui, tu débugues une lenteur à l’aveugle. - Alerte sur l’âge de la plus vieille transaction ouverte : c’est ce qui empêche l’autovacuum de faire son travail.
-
La chaîne de connexion est en
sslmode=requireau minimum, et le mot de passe vient d’un secret, pas du dépôt.
Le template Axum applique la plupart de ces points, notamment le pool borné, l’index composite et la rotation de token en transaction.