Ce document détaille les choix de performance et de concurrence du projet : le pool de
connexions, les requêtes préparées (protocole étendu de PostgreSQL), la propagation du
context.Context, les timeouts, la gestion des goroutines, le modèle de concurrence MVCC, et les
conseils d'optimisation SQL (index, EXPLAIN, VACUUM). Chaque réglage est justifié : le but est
de comprendre pourquoi il compte.
- 1. Le pool de connexions
- 2. Requêtes préparées côté serveur
- 3.
context.Context: propagation et annulation - 4. Timeouts en cascade
- 5. Concurrence et goroutines
- 6. Optimisation SQL : index et
EXPLAIN - 7. Autres bonnes pratiques de performance
- Récapitulatif des réglages
Le point clé à comprendre. En Go, *sql.DB n'est pas une connexion : c'est un pool de
connexions géré automatiquement. On l'ouvre une seule fois au démarrage et on le partage
(par injection) dans toute l'application. Ouvrir/fermer une connexion à chaque requête serait
catastrophique — d'autant qu'en PostgreSQL, chaque connexion correspond à un processus serveur
dédié (modèle un processus par connexion), ce qui rend le sur-approvisionnement encore plus coûteux.
sql.Open("pgx", …)n'établit pas réellement de connexion : il prépare le pool et s'appuie sur
le pilote pgx (exposé comme un pilotedatabase/sqlclassique via son sous-paquetstdlib).
C'estPingContextqui force une vraie connexion et permet de détecter tôt un problème (mauvais mot
de passe, base injoignable). Voirinternal/database/database.go.
Les réglages du pool (internal/database/database.go, valeurs par internal/config/config.go) :
db.SetMaxOpenConns(cfg.MaxConnexionsOuvertes) // BDD_MAX_CONNEXIONS_OUVERTES, défaut 25
db.SetMaxIdleConns(cfg.MaxConnexionsInactives) // BDD_MAX_CONNEXIONS_INACTIVES, défaut 25
db.SetConnMaxLifetime(cfg.DureeVieMaxConnexion) // BDD_DUREE_VIE_CONNEXION, défaut 5m
db.SetConnMaxIdleTime(cfg.DureeVieMaxConnexion) // même durée pour l'inactivité| Réglage | Rôle et justification |
|---|---|
SetMaxOpenConns |
Plafonne le nombre de connexions simultanées vers PostgreSQL. Trop haut → on sature le serveur (max_connections, défaut 100). Trop bas → goulet d'étranglement (les requêtes attendent). On l'aligne sur la capacité de la base. |
SetMaxIdleConns |
Nombre de connexions gardées ouvertes au repos pour éviter le coût de réouverture. Le mettre égal à MaxOpenConns donne un pool réactif (une connexion libérée reste disponible). |
SetConnMaxLifetime |
Recycle les connexions périodiquement. Indispensable derrière un équilibreur de charge ou un pare-feu qui coupe les connexions longues, et pour éviter des connexions « zombies ». |
SetConnMaxIdleTime |
Recycle une connexion restée inactive trop longtemps (libère des ressources côté base). |
Pourquoi MaxIdleConns == MaxOpenConns ? Si MaxIdleConns était plus petit, une connexion
tout juste libérée après une requête serait fermée faute de place au repos, puis rouverte à
la requête suivante — un gâchis. En les égalisant, une connexion active reste disponible pour la
prochaine requête.
Dimensionner le pool face à max_connections. Côté serveur, PostgreSQL borne le nombre total
de connexions par le paramètre max_connections (100 par défaut), dont quelques-unes sont
réservées à l'administration (superuser_reserved_connections). Le pool applicatif doit rester
sous cette limite. Si l'on déploie N instances de l'API, chacune avec MaxOpenConns = 25, le
total N × 25 doit tenir sous max_connections : avec 100 connexions disponibles, on fait tourner
~3 instances (3 × 25 = 75) tout en gardant une marge pour l'administration et les tâches pg_cron.
Au-delà, on augmente max_connections (au prix de plus de mémoire par connexion) ou l'on interpose
un pooler externe comme PgBouncer.
Surveillance en production. Un ordonnanceur journalise périodiquement l'état du pool (voir
cmd/api/main.go), ce qui permet de détecter une saturation (WaitCount qui grimpe = requêtes en
attente d'une connexion) :
stats := db.Stats()
logger.Info("état du pool de connexions",
slog.Int("connexions_ouvertes", stats.OpenConnections),
slog.Int("en_utilisation", stats.InUse),
slog.Int("au_repos", stats.Idle),
slog.Int64("en_attente", stats.WaitCount),
)Si en_attente augmente durablement, il faut soit augmenter MaxOpenConns (si PostgreSQL tient
sous max_connections), soit optimiser les requêtes lentes qui monopolisent les connexions.
Ce que fait le pilote. pgx dialogue avec PostgreSQL via le protocole étendu (extended query
protocol), en trois temps : Parse (le serveur analyse et planifie la requête), Bind (il y
attache les valeurs des paramètres) puis Execute (il exécute). Les valeurs ne transitent donc
jamais mêlées au texte SQL : elles sont envoyées séparément, en binaire. Autre spécificité,
les emplacements de paramètres sont numérotés — $1, $2, $3… — et non anonymes.
-- Ce que les repositories envoient (voir internal/repository/) :
SELECT ... FROM vue_livres_details WHERE uuid = $1;
INSERT INTO livres (...) VALUES ($1, $2, ..., $11) RETURNING id;Un cache de statements côté connexion. pgx mémorise les requêtes préparées par connexion :
lorsqu'une même requête (même texte SQL) revient sur la même connexion du pool, son plan analysé est
réutilisé, sans nouvelle phase Parse. Sur des requêtes répétées (lectures de liste, INSERT
en boucle…), cela évite un reparse systématique côté serveur.
Double bénéfice :
- Sécurité : les valeurs ne peuvent jamais altérer la structure de la requête → neutralise l'injection SQL (voir SECURITE.md).
- Performance : sur une requête répétée avec des valeurs différentes, le serveur réutilise le plan d'exécution, et le passage des paramètres en binaire est efficace.
Construction du DSN. La chaîne de connexion est assemblée avec net/url (qui échappe
correctement l'utilisateur et le mot de passe), sous forme d'URL postgres://… :
// internal/database/database.go — extrait de construireDSN
u := url.URL{
Scheme: "postgres",
User: url.UserPassword(cfg.Utilisateur, cfg.MotDePasse), // échappement sûr
Host: fmt.Sprintf("%s:%d", cfg.Hote, cfg.Port), // ex. 127.0.0.1:5432
Path: cfg.Nom,
}
parametres.Set("sslmode", "disable") // pas de TLS (réseau Docker privé ; « require » en prod)
parametres.Set("connect_timeout", "…") // délai d'établissement de la connexion, en secondes
parametres.Set("timezone", "UTC") // horodatages cohérents en UTC (serveur ET session)Appels de procédures à paramètres INOUT. La procédure d'emprunt renvoie ses valeurs de sortie
via des paramètres INOUT. En PostgreSQL, CALL pr_emprunter_livre($1, $2, $3, NULL, NULL, NULL)
renvoie directement une ligne contenant ces valeurs (on passe NULL en entrée pour les INOUT) :
un simple QueryRowContext(...).Scan(...) sur le pool suffit, sans réserver de connexion dédiée
ni passer par des variables de session liées à une seule connexion. C'est le point à retenir pour les
procédures INOUT (voir internal/repository/emprunt_repository.go).
Le principe. Chaque requête HTTP porte un context.Context. On le propage jusqu'aux appels
base de données (...Context). Si la requête est annulée (client déconnecté, délai dépassé), le
contexte est annulé et les opérations qui l'observent s'interrompent d'elles-mêmes, libérant
aussitôt les ressources (connexion, mémoire).
En pratique, partout dans le code :
// Handlers -> services -> repositories : r.Context() est transmis de bout en bout.
r.db.QueryRowContext(ctx, requete, uuid) // et non QueryRow(...) sans contexte
r.db.ExecContext(ctx, requete, args...)
database.EnTransaction(ctx, r.db, ...) // même une transaction est liée au contextePourquoi c'est crucial ? Sans propagation du contexte, une requête SQL lente continuerait de
tourner côté base même si le client a abandonné ou si le délai a expiré : la connexion resterait
mobilisée pour rien, réduisant la capacité disponible. Concrètement, si le contexte est annulé
pendant une requête, pgx émet une demande d'annulation (cancel request) vers PostgreSQL pour
interrompre la requête en cours côté serveur, et rend la connexion au pool.
Règle d'or : toujours utiliser les variantes
...Contextet propagerr.Context(). Sans
cela, le middleware de timeout (§4) serait sans effet sur les requêtes SQL (voir le commentaire
deinternal/middleware/timeout.go).
Sondes bornées. La sonde /ready elle-même borne son ping à 2 s
(context.WithTimeout(r.Context(), 2*time.Second) dans internal/handler/sante_handler.go, qui
appelle database.Verifier → PingContext) : la sonde de disponibilité ne doit jamais rester
bloquée.
Plusieurs délais se complètent, chacun protégeant une phase différente :
| Délai | Où | Protège… |
|---|---|---|
ReadHeaderTimeout / ReadTimeout |
http.Server (cmd/api/main.go) |
La lecture de la requête (anti-Slowloris) |
WriteTimeout |
http.Server |
L'écriture de la réponse |
IdleTimeout |
http.Server |
Les connexions keep-alive inactives |
DelaiTraitement (middleware) |
middleware/timeout.go |
La durée de traitement applicatif (contexte) |
DelaiConnexion (pool) |
database/database.go |
L'établissement d'une connexion (connect_timeout) |
Ping /ready (2 s) |
handler/sante_handler.go |
La sonde de disponibilité |
Cohérence importante. Le délai de traitement doit rester inférieur au WriteTimeout du
serveur, pour que le contexte expire AVANT que le serveur ne coupe l'écriture de la réponse (on
préfère renvoyer une erreur propre plutôt qu'une connexion coupée). C'est documenté dans
internal/config/config.go (défaut : traitement 10 s < écriture 15 s).
Go rend la concurrence naturelle. Le projet en illustre les fondamentaux proprement, avec un
arrêt maîtrisé (pas de goroutine « fuyarde »).
net/http traite chaque requête dans sa propre goroutine. C'est pourquoi le code partagé doit
être sans état mutable global : ici, tout est injecté (pool, config, logger…), et les seules
variables au niveau package sont des constantes immuables (ex. les regexp compilés une fois
dans internal/validation/validation.go, sûrs pour un usage concurrent).
internal/middleware/rate_limiter.go maintient une map[string]*clientLimite (un seau par IP),
protégée par un sync.Mutex car des requêtes concurrentes y accèdent :
l.mu.Lock()
defer l.mu.Unlock()
// … accès à la map partagée …Une goroutine de nettoyage (lancée au démarrage) purge les IP inactives toutes les minutes et
s'arrête proprement quand le context.Context global est annulé (arrêt du serveur) :
select {
case <-ctx.Done():
return // arrêt propre, pas de fuite de goroutine
case <-ticker.C:
// … purge des clients inactifs …
}internal/scheduler/scheduler.go illustre le motif complet : une goroutine par tâche, un
time.Ticker pour la périodicité, un context.Context pour l'arrêt, et un sync.WaitGroup pour
attendre la fin de toutes les goroutines lors de l'arrêt gracieux :
func (o *Ordonnanceur) Demarrer(ctx context.Context) {
for _, tache := range o.taches {
o.wg.Add(1)
go o.boucleTache(ctx, tache) // une goroutine par tâche
}
}
func (o *Ordonnanceur) Attendre() { o.wg.Wait() } // arrêt propreÀ l'arrêt (cmd/api/main.go), le contexte est annulé, puis ordonnanceur.Attendre() bloque jusqu'à
ce que toutes les tâches soient sorties — aucune goroutine n'est laissée en suspens.
Les tests se lancent avec le détecteur de data races (go test -race, cible make tester) : il
signale tout accès concurrent non synchronisé à une même donnée. C'est le filet de sécurité
indispensable de tout code concurrent.
La concurrence ne s'arrête pas au code Go : PostgreSQL gère lui aussi des accès simultanés, via
le MVCC (Multi-Version Concurrency Control). Le principe : plutôt que de verrouiller une ligne
pour la lire, PostgreSQL conserve plusieurs versions d'une même ligne ; chaque transaction voit
un instantané cohérent des données à son instant de départ.
Conséquence directe et fondamentale : les lecteurs ne bloquent pas les écrivains, et les écrivains
ne bloquent pas les lecteurs. Un SELECT n'attend jamais qu'un UPDATE concurrent se termine — il
lit simplement la version précédente de la ligne. Seuls deux écrivains visant la même ligne
s'attendent l'un l'autre.
C'est pourquoi la transaction de retour de livre (Rendre, dans emprunt_repository.go) prend un
verrou explicite là où c'est nécessaire :
// SELECT ... FOR UPDATE : verrouille la ligne d'emprunt le temps de la transaction, pour
// empêcher deux retours simultanés du même emprunt (voir emprunt_repository.go).
`SELECT id, livre_id, statut::text, date_retour_prevue FROM emprunts WHERE uuid = $1 FOR UPDATE`FOR UPDATE sérialise uniquement les transactions qui veulent modifier cette ligne précise ; les
lectures simples de l'emprunt, elles, continuent sans attendre.
La contrepartie du MVCC. Comme un UPDATE ou un DELETE ne réécrit pas la ligne sur place mais
crée une nouvelle version et marque l'ancienne comme obsolète, la table accumule des « lignes
mortes » (dead tuples) : des versions devenues invisibles mais encore présentes sur le disque. Il
faut les recycler — c'est le rôle de VACUUM (voir §6).
Deux mécanismes de planification, chacun à sa place (voir aussi DATABASE.md) :
- Tâches
pg_cron: maintenance des données, au plus près des tables, sans dépendre de l'application — marquer les retards, purger les jetons expirés, archiver, calculer les statistiques, et la maintenanceVACUUM ANALYZE(voirsql/cron/09_cron.sql). - Ordonnanceur Go : tâches applicatives (ici, journaliser l'état du pool de connexions).
Un index accélère les lectures, ralentit un peu les écritures. Il évite de parcourir toute la
table pour un WHERE, un JOIN ou un ORDER BY, mais il faut le maintenir à chaque écriture et
il occupe de l'espace. On indexe donc les colonnes réellement filtrées ou triées — et l'on
choisit la bonne méthode : là où beaucoup de moteurs se limitent au B-tree, PostgreSQL offre une
palette de méthodes, chacune adaptée à un usage (voir sql/schema/03_tables.sql et
sql/schema/04_index.sql).
| Méthode | Quand l'utiliser | Exemple réel dans le projet |
|---|---|---|
| B-tree (défaut) | égalité, plages, tri (=, <, >, ORDER BY) |
idx_livres_categorie (categorie_id), idx_jetons_expire (expire_le) |
GIN + pg_trgm |
recherche ILIKE '%mot%' (joker en tête, floue) |
idx_livres_titre_trgm (titre), idx_auteurs_nom_trgm (nom) |
| GIN sur JSONB | recherche dans un document JSON (opérateur @>) |
idx_audit_nouvelles_gin (journal_audit.nouvelles_valeurs) |
| BRIN | grosse table append-only, colonne corrélée à l'ordre physique | idx_audit_cree_le_brin (journal_audit.cree_le) |
Partiel (WHERE …) |
n'indexer qu'un sous-ensemble de lignes | idx_utilisateurs_actifs … WHERE supprime_le IS NULL |
Couvrant (INCLUDE) |
index-only scan (répondre sans lire la table) | idx_livres_categorie_couvrant … INCLUDE (titre, prix) |
| Multicolonne | filtrer/trier sur plusieurs colonnes à la fois | idx_emprunts_util_statut (utilisateur_id, statut) |
Quelques points saillants :
- GIN +
pg_trgm— la star du projet. Elle rend rapidetitre ILIKE '%terme%', la recherche exposée par l'API.pg_trgmdécompose le texte en trigrammes (groupes de 3 caractères) et sait donc répondre à une recherche « au milieu » du texte — ce qu'un B-tree ne peut pas faire à cause du joker en tête. - GIN sur JSONB.
idx_audit_nouvelles_ginpermet des recherches dans le document d'audit, par ex.WHERE nouvelles_valeurs @> '{"role":"admin"}'. - BRIN — l'index minuscule.
journal_auditne fait que grandir dans l'ordre du temps :cree_leest donc fortement corrélé à l'ordre physique des blocs. BRIN résume chaque plage de blocs et occupe une place dérisoire — idéal pour une très grosse table append-only, là où un B-tree serait énorme. - Partiel + couvrant.
idx_livres_categorie_couvrantcombine les deux : il n'indexe que les livres actifs (WHERE supprime_le IS NULL) et incluttitreetprix(INCLUDE), de sorte que « lister les livres d'une catégorie avec leur titre et leur prix » se résout par un index-only scan, sans toucher la table.
Ordre des colonnes d'un index multicolonne.
(a, b)sert les requêtes filtrant suraseul,
ou suraetb, mais pas surbseul. On place en tête la colonne la plus souvent
filtrée. Ainsiidx_emprunts_util_statut (utilisateur_id, statut)sert « les emprunts d'un membre »
et « les emprunts d'un membre par statut ».
Le compromis lecture/écriture. Chaque index supplémentaire doit être mis à jour à chaque
INSERT/UPDATE/DELETE et consomme de l'espace disque. On n'indexe donc que ce qui sert
réellement les requêtes du projet — jamais « au cas où ».
EXPLAIN affiche le plan d'exécution estimé par le planificateur, sans exécuter la requête.
EXPLAIN (ANALYZE, BUFFERS) l'exécute réellement et rapporte les temps et le nombre de lignes
mesurés (attention : un EXPLAIN ANALYZE sur un UPDATE/INSERT/DELETE modifie bel et bien
les données !).
-- Plan estimé (n'exécute pas la requête) :
EXPLAIN SELECT * FROM vue_livres_details WHERE titre ILIKE '%tolstoï%';
-- Plan réel, avec temps mesurés et accès aux blocs (buffers) :
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM vue_livres_details WHERE titre ILIKE '%tolstoï%';Lire un plan PostgreSQL. Les nœuds d'accès à connaître, du moins bon au meilleur pour un filtre sélectif :
| Nœud | Signification |
|---|---|
| Seq Scan | parcours séquentiel de toute la table — aucun index utilisé (acceptable sur une petite table, à éviter sur une grosse) |
| Bitmap Heap Scan | l'index désigne d'abord les blocs à lire, puis la table est lue par paquets (filtre moyennement sélectif) |
| Index Scan | parcours de l'index puis accès ciblé à la table (filtre sélectif) |
| Index Only Scan | tout est lu dans l'index (index couvrant), sans toucher la table — le plus rapide |
Chaque nœud affiche des chiffres à interpréter :
cost=0.00..12.34: coût estimé (unité arbitraire du planificateur), sous la forme démarrage..total ;rows=…: nombre de lignes estimé ;- avec
ANALYZE,actual time=…etrows=…réels, ainsi que le nombre deloops(boucles).
L'écart estimé/réel est le signal clé. Si le planificateur estime rows=1 mais en trouve
rows=50000, ses statistiques sont périmées → un ANALYZE s'impose (voir plus bas).
Exemple concret sur ce projet. La recherche titre ILIKE '%…%' (endpoint de liste des livres) ne
peut pas utiliser un index B-tree, à cause du joker en tête. Sans index adapté, on obtient un
Seq Scan. Avec l'index GIN trigramme idx_livres_titre_trgm, le plan bascule sur un
Bitmap Index Scan suivi d'un Bitmap Heap Scan :
Bitmap Heap Scan on livres (cost=… rows=…) (actual time=… rows=…)
Recheck Cond: (titre ~~* '%tolstoï%'::text)
-> Bitmap Index Scan on idx_livres_titre_trgm (cost=…) (actual time=… rows=…)
Index Cond: (titre ~~* '%tolstoï%'::text)
Le passage de Seq Scan à Bitmap Index Scan on idx_livres_titre_trgm est la preuve que l'index
trigramme est utilisé (~~* est l'opérateur d'ILIKE dans les plans) — exactement ce que promettait
sql/schema/04_index.sql, et sans changer une ligne de la requête applicative.
On l'a vu (§5, MVCC) : les UPDATE/DELETE laissent des lignes mortes. Deux opérations de
maintenance les prennent en charge — et PostgreSQL les déclenche automatiquement.
VACUUMrécupère l'espace occupé par les lignes mortes pour le réutiliser (il ne rend pas forcément le disque au système d'exploitation ;VACUUM FULL, verrouillant et coûteux, le fait mais n'est pas nécessaire ici).ANALYZEmet à jour les statistiques dont le planificateur se sert pour choisir ses plans (taille des tables, distribution des valeurs). Des statistiques fraîches = de meilleurs arbitragesSeq ScanvsIndex Scan.- L'autovacuum est un processus d'arrière-plan qui déclenche
VACUUMetANALYZEtout seul, dès qu'une table a suffisamment changé. Dans la grande majorité des cas, on le laisse travailler : il suffit à maintenir la base en bonne santé.
Un filet de sécurité hebdomadaire. En complément de l'autovacuum, une tâche pg_cron
bib_maintenance lance un VACUUM ANALYZE global chaque dimanche à 05h00
(sql/cron/09_cron.sql) :
SELECT cron.schedule('bib_maintenance', '0 5 * * 0', $$
VACUUM ANALYZE
$$);Surveiller le bloat. Si une table accumule trop de lignes mortes (le bloat), ses parcours ralentissent. Pour inspecter en détail ce que fait le nettoyage sur une table :
VACUUM (VERBOSE, ANALYZE) livres;La sortie indique le nombre de lignes mortes retirées et les blocs récupérés. En production, on
surveille le ratio de lignes mortes (via pg_stat_user_tables.n_dead_tup) pour repérer une table qui
« gonfle » et, au besoin, régler l'autovacuum plus agressivement sur celle-ci. Pour approfondir
EXPLAIN, les méthodes d'index et VACUUM, voir DATABASE.md et
POSTGRESQL.md.
- Vue pré-jointe :
vue_livres_detailsjoint auteurs/catégories et calcule la disponibilité une fois, ce qui évite au client des allers-retours et centralise la logique de lecture. - Vue matérialisée :
vue_statistiques_livrespré-calcule la popularité des livres (agrégat stocké sur disque), rafraîchie hors ligne parpg_cron— une lecture instantanée là où l'agrégat serait coûteux à recalculer à chaque appel (voirsql/views/06_vues.sql). RETURNING:INSERT INTO livres (…) VALUES (…) RETURNING idrécupère l'identifiant généré dans la même requête, sans second aller-retour vers la base (voirCreer).COUNT+SELECTpaginé : les listes exécutent unCOUNT(*)(pour la pagination) puis unSELECT … LIMIT $N OFFSET $M, en court-circuitant leSELECTsi le total est 0 (voir les repositories).- Pagination bornée :
tailleest plafonnée à 100 (models.TailleMax), pour éviter qu'un client ne demande des millions de lignes d'un coup (protection mémoire et performance).
- Pré-allocation des tranches : les repositories créent les slices résultats avec une capacité
initiale (
make([]models.X, 0, params.Taille)) pour limiter les réallocations pendant le parcours des lignes. - Fermeture systématique des curseurs :
defer lignes.Close()après chaqueQueryContext, et vérification delignes.Err()après la boucle (une erreur d'itération ne se voit pas dansNext()). - Choisir la bonne méthode d'index (voir §6) : B-tree par défaut, GIN +
pg_trgmpourILIKE '%…%', BRIN pour une grosse table append-only. Un mauvais type d'index laisse unSeq Scanpersister malgré l'index. - Mesurer avant/après avec
EXPLAIN (ANALYZE): on ne devine pas qu'un index sert — on le prouve en comparant le plan avant et après sa création. - Laisser l'autovacuum travailler : le désactiver conduit au bloat et à des statistiques périmées (donc à de mauvais plans). On l'ajuste plutôt qu'on ne le coupe.
- Rafraîchir la vue matérialisée sans blocage :
pg_cronexécuteREFRESH MATERIALIZED VIEW CONCURRENTLY vue_statistiques_livres— les lectures ne sont jamais bloquées pendant le rafraîchissement (au prix d'un index unique requis, voirsql/views/06_vues.sql). - Image Docker minimale : binaire statique compilé avec
-ldflags "-s -w"(table des symboles retirée), image finale ~20 Mo → démarrage rapide et déploiements légers (voirDockerfile). - Métriques de latence : l'histogramme
bibliotheque_http_duree_requete_secondes(voirinternal/observabilite/metriques.go) permet de suivre les quantiles (p50, p95, p99) et de repérer les endpoints lents. Le labelrouteutilise le patron (/livres/{id}), pas le chemin réel, pour éviter l'explosion de cardinalité (une série par identifiant). ON DELETE CASCADE/RESTRICTciblés : l'intégrité est gérée par la base (pas de multiples requêtes applicatives pour nettoyer les dépendances).
| Réglage | Variable / lieu | Défaut | Effet |
|---|---|---|---|
| Connexions max du pool | BDD_MAX_CONNEXIONS_OUVERTES |
25 |
Plafond de connexions simultanées à PostgreSQL (à garder sous max_connections, défaut 100) |
| Connexions au repos | BDD_MAX_CONNEXIONS_INACTIVES |
25 |
Connexions gardées prêtes |
| Durée de vie d'une connexion | BDD_DUREE_VIE_CONNEXION |
5m |
Recyclage périodique |
| Délai de connexion | BDD_DELAI_CONNEXION |
5s |
Timeout d'établissement (connect_timeout) |
| Délai de traitement | SERVEUR_DELAI_TRAITEMENT |
10s |
Annulation du contexte au-delà |
| Lecture / écriture serveur | SERVEUR_DELAI_LECTURE / _ECRITURE |
10s/15s |
Anti-Slowloris / borne l'écriture |
| Keep-alive | SERVEUR_DELAI_INACTIF |
60s |
Ferme les connexions inactives |
| Débit par IP | RATE_LIMIT_PAR_SECONDE / _RAFALE |
10/20 |
Limite le volume par client |
| Taille max du corps | REQUETE_TAILLE_MAX_OCTETS |
1 Mio |
Protège la mémoire |
| Taille de page max | models.TailleMax |
100 |
Borne le volume renvoyé par liste |
Ces valeurs sont des points de départ raisonnables. En production, on les ajuste à partir des
mesures (métriques Prometheus, db.Stats(), EXPLAIN (ANALYZE) et pg_stat_statements sur les
requêtes lentes) : on optimise ce que l'on mesure, jamais à l'aveugle.