Top 7 des goulots d'étranglement de performance PostgreSQL courants et leurs solutions
Diagnostiquez sept goulots d'étranglement de performance PostgreSQL courants, des plans lents et des index inefficaces à l'autovacuum, la mémoire, le pooling et les verrous.
Top 7 des goulots d'étranglement de performance PostgreSQL courants et leurs solutions
Le travail de performance PostgreSQL tourne mal lorsque chaque requête lente reçoit la même réponse : "ajoutez un index" ou "augmentez la mémoire". Parfois, c'est juste. Parfois, la base de données attend un verrou, vide un tri sur le disque, se noie dans des connexions inactives, ou lit dix fois plus de pages de table qu'elle ne le devrait parce que l'autovacuum a pris du retard.
L'habitude utile est d'identifier le goulot d'étranglement avant de changer quoi que ce soit. Un point de terminaison API lent n'est qu'un symptôme. La base de données peut généralement vous dire si le temps a été consacré à l'analyse, aux jointures, au tri, à la lecture depuis le disque, à l'attente d'une autre transaction, ou à l'ouverture de trop de sessions.
1. Plans d'exécution de requêtes inefficaces
L'une des causes les plus fréquentes de performances lentes est des requêtes SQL mal optimisées. Le planificateur de requêtes de PostgreSQL est sophistiqué, mais il peut parfois générer des plans d'exécution inefficaces, en particulier avec des requêtes complexes ou des statistiques obsolètes.
Identifier le goulot d'étranglement
Utilisez EXPLAIN et EXPLAIN ANALYZE pour comprendre comment PostgreSQL exécute vos requêtes. EXPLAIN montre l'exécution planifiée, tandis que EXPLAIN ANALYZE exécute réellement la requête et fournit le timing réel et les comptages de lignes.
-- Pour voir le plan d'exécution :
EXPLAIN SELECT * FROM users WHERE email LIKE 'john.doe%';
-- Pour voir le plan et les détails d'exécution réels :
EXPLAIN ANALYZE SELECT * FROM users WHERE email LIKE 'john.doe%';
Recherchez :
- Les analyses séquentielles sur les grandes tables où un index serait bénéfique.
- Les grandes erreurs d'estimation de lignes par rapport aux comptages de lignes réels.
- Les jointures de boucle imbriquée lorsqu'une jointure de hachage ou une jointure de fusion pourrait être plus appropriée.
Solutions
- Ajoutez des index appropriés : Assurez-vous que des index existent pour les colonnes utilisées dans les clauses
WHERE,JOIN,ORDER BYetGROUP BY. Pour les clausesLIKEavec des caractères génériques en tête (%), les index B-tree sont souvent inefficaces ; envisagez la recherche en texte intégral ou les index trigrammes. - Réécrivez la requête : Parfois, une requête plus simple ou structurée différemment peut conduire à un meilleur plan.
- Mettez à jour les statistiques : PostgreSQL utilise les statistiques pour estimer la sélectivité des prédicats. Des statistiques obsolètes peuvent égarer le planificateur.
ANALYZE table_name; -- Ou pour toutes les tables : ANALYZE; - Ajustez les paramètres du planificateur de requêtes :
work_memetrandom_page_costpeuvent influencer les choix du planificateur, mais ceux-ci doivent être ajustés avec prudence.
2. Index manquants ou inefficaces
Les index sont cruciaux pour une récupération rapide des données. Sans eux, PostgreSQL doit effectuer des analyses séquentielles, lisant chaque ligne d'une table pour trouver les données correspondantes, ce qui est extrêmement lent pour les grandes tables.
Identifier le goulot d'étranglement
- Sortie
EXPLAIN ANALYZE: RecherchezSeq Scansur les grandes tables dans le plan de requête. - Outils de surveillance de base de données : Des outils comme
pg_stat_user_tablespeuvent montrer les comptages d'analyse de table.
Solutions
- Créez des index B-tree : Ce sont les plus courants et conviennent aux opérations d'égalité (
=), de plage (<,>,<=,>=) etLIKE(sans caractère générique en tête).CREATE INDEX idx_users_email ON users (email); - Utilisez d'autres types d'index :
- GIN/GiST : Pour la recherche en texte intégral, les opérations JSONB et les types de données géométriques.
- Index de hachage : Pour les vérifications d'égalité (moins courants dans les versions récentes de PostgreSQL en raison des améliorations du B-tree).
- BRIN (Block Range Index) : Pour les très grandes tables avec des données physiquement corrélées.
- Index partiels : Indexez uniquement un sous-ensemble de lignes, utile lorsque les requêtes ciblent fréquemment des conditions spécifiques.
CREATE INDEX idx_orders_pending ON orders (order_date) WHERE status = 'pending'; - Index d'expression : Indexez le résultat d'une fonction ou d'une expression.
CREATE INDEX idx_users_lower_email ON users (lower(email)); - Évitez les index redondants : Avoir trop d'index peut ralentir les opérations d'écriture (
INSERT,UPDATE,DELETE) et consommer de l'espace disque.
3. Activité d'autovacuum excessive ou famine
PostgreSQL utilise un système de contrôle de concurrence multi-version (MVCC), ce qui signifie que les opérations UPDATE et DELETE ne suppriment pas les lignes immédiatement. Au lieu de cela, elles les marquent comme obsolètes. VACUUM récupère cet espace et empêche le bouclage des identifiants de transaction. L'autovacuum automatise ce processus.
Identifier le goulot d'étranglement
- Charge CPU/IO élevée : L'autovacuum peut être gourmand en ressources.
- Gonflement de table : Visible comme des écarts importants entre
pg_class.relpagesetpg_class.reltuplesavec la taille réelle des données ou les comptages de lignes attendus. pg_stat_activity: Recherchez les processusautovacuum workerde longue durée.pg_stat_user_tables: Surveillezn_dead_tup(nombre de tuples morts) et les heureslast_autovacuum/last_autoanalyze.
Solutions
Réglez les paramètres d'autovacuum : Ajustez les paramètres dans
postgresql.confou les paramètres par table.autovacuum_vacuum_threshold: Nombre minimum de tuples morts pour déclencher un vacuum.autovacuum_vacuum_scale_factor: Fraction de la taille de la table à prendre en compte pour le vacuum.autovacuum_analyze_thresholdetautovacuum_analyze_scale_factor: Paramètres similaires pourANALYZE.autovacuum_max_workers: Nombre de workers d'autovacuum parallèles.autovacuum_work_mem: Mémoire disponible pour chaque worker.
Exemple de paramètres par table :
ALTER TABLE large_table SET (autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.02);VACUUMmanuel : Pour une récupération immédiate d'espace ou lorsque l'autovacuum ne suit pas.VACUUM (VERBOSE, ANALYZE) table_name;Utilisez
VACUUM FULLuniquement en cas d'absolue nécessité, car il verrouille la table et réécrit la table entière, ce qui peut être très perturbant.Surveillez les anciennes transactions : Les transactions de longue durée peuvent conserver d'anciennes versions de lignes ouvertes et empêcher le nettoyage.
Surveillez l'âge des identifiants de transaction : Comprendre
vacuum_freeze_min_age,autovacuum_freeze_max_ageetage(datfrozenxid)de la base de données est crucial pour éviter les urgences de bouclage.
4. Ressources matérielles insuffisantes (CPU, RAM, IOPS)
Les performances de PostgreSQL sont directement liées au matériel sous-jacent. Un CPU, une RAM insuffisants ou des E/S disque lentes peuvent créer des goulots d'étranglement importants.
Identifier le goulot d'étranglement
- Outils de surveillance système :
top,htop,iostat,vmstatsur Linux ; Moniteur de performances sur Windows. pg_stat_activity: Recherchez les requêtes en attente de verrous (wait_event_type = 'IO','LWLock', etc.).- Utilisation élevée du CPU : Constamment proche de 100%.
- Temps d'attente E/S disque élevés : Systèmes passant beaucoup de temps à attendre les opérations disque.
- Faible mémoire disponible / Utilisation élevée du swap : Indique que la RAM est insuffisante.
Solutions
- CPU : Assurez-vous d'avoir suffisamment de cœurs, en particulier pour les charges de travail concurrentes. PostgreSQL utilise efficacement plusieurs cœurs pour l'exécution parallèle des requêtes (dans les versions plus récentes) et les processus d'arrière-plan.
- RAM (
shared_buffers,work_mem) :shared_buffers: Cache pour les blocs de données. Une recommandation courante est de 25% de la RAM système, mais ajustez en fonction de la charge de travail.work_mem: Utilisé pour le tri, le hachage et d'autres opérations intermédiaires. Unwork_meminsuffisant force les vidages sur disque.
- E/S disque :
- Utilisez des SSD : Significativement plus rapides que les HDD pour les charges de travail de base de données.
- Configuration RAID : Optimisez pour les performances de lecture/écriture (par exemple, RAID 10).
- Lecteur WAL séparé : Placer le journal d'écriture anticipée (WAL) sur un lecteur rapide séparé peut améliorer les performances d'écriture.
- Réseau : Assurez une bande passante suffisante et une faible latence pour la communication client-serveur, en particulier dans les environnements distribués.
Les symptômes matériels ont besoin de preuves. Si le CPU est élevé et l'attente disque faible, recherchez des plans coûteux, des requêtes lourdes en expressions, du traitement JSON ou trop de workers actifs. Si l'attente E/S est élevée, examinez les lectures de tampon dans EXPLAIN (ANALYZE, BUFFERS), le comportement des points de contrôle et si les tables chaudes tiennent en mémoire. Si le swap est actif, réduisez la pression de connexion ou les paramètres de mémoire avant d'ajouter plus de concurrence de requêtes.
5. Fichier postgresql.conf mal configuré
Le fichier postgresql.conf de PostgreSQL contient des centaines de paramètres qui contrôlent son comportement. Les paramètres par défaut sont souvent conservateurs et non optimisés pour des charges de travail ou du matériel spécifiques.
Identifier le goulot d'étranglement
- Lenteur générale : Temps de requête lents dans l'ensemble.
- E/S disque excessives : Par rapport à la RAM disponible.
- Utilisation de la mémoire : Système montrant des signes de pression mémoire.
- Consultation de guides d'optimisation des performances : Comprendre les valeurs optimales courantes.
Solutions
Paramètres clés à considérer :
shared_buffers: (Comme mentionné ci-dessus) Cache pour les blocs de données. Commencez avec ~25% de la RAM système.work_mem: Mémoire pour les tris/hachages. Ajustez en fonction de la sortieEXPLAIN ANALYZEmontrant des vidages sur disque.maintenance_work_mem: Mémoire pourVACUUM,CREATE INDEX,ALTER TABLE ADD FOREIGN KEY. Des valeurs plus grandes accélèrent ces opérations.effective_cache_size: Aide le planificateur à estimer la quantité de mémoire disponible pour la mise en cache par le système d'exploitation et PostgreSQL lui-même.wal_buffers: Tampons pour les écritures WAL. Augmentez si vous avez des charges d'écriture élevées.checkpoint_completion_target: Étale les écritures de point de contrôle dans le temps, réduisant les pics d'E/S.max_connections: Définissez de manière appropriée ; trop élevé peut épuiser les ressources.log_statement: Utile pour le débogage, mais la journalisation de toutes les instructionsALLpeut avoir un impact sur les performances.
Astuce : Utilisez des outils comme pgtune pour obtenir des recommandations de départ basées sur votre matériel. Testez toujours les modifications dans un environnement de staging avant de les appliquer à la production.
Un piège avec la configuration PostgreSQL est de traiter chaque paramètre comme un bouton d'accélération. work_mem en est un bon exemple. Il est alloué par opération, pas une fois pour tout le serveur. Une seule requête peut l'utiliser plusieurs fois, et de nombreuses requêtes concurrentes peuvent le multiplier rapidement. L'augmenter de 4MB à 128MB globalement pourrait aider une requête de rapport et nuire à l'ensemble du serveur pendant le trafic. Pour tester une requête connue, utilisez d'abord une modification au niveau de la session :
SET work_mem = '128MB';
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...
Si le plan cesse de vider sur le disque et que la latence s'améliore, vous avez appris quelque chose d'utile. Vous devez toujours décider s'il faut réécrire la requête, ajouter un index, définir la mémoire pour un rôle de reporting ou modifier la valeur globale.
6. Problèmes de pooling de connexions
L'établissement d'une nouvelle connexion à la base de données est une opération coûteuse. Dans les applications avec des interactions fréquentes et de courte durée avec la base de données, l'ouverture et la fermeture répétées de connexions peuvent devenir un goulot d'étranglement de performance important.
Identifier le goulot d'étranglement
- Nombre élevé de connexions :
pg_stat_activitymontre un très grand nombre de connexions, dont beaucoup sont inactives. - Temps de démarrage/réponse de l'application lents : Lorsque les connexions à la base de données sont fréquemment établies.
- Épuisement des ressources du serveur : Utilisation élevée du CPU ou de la mémoire attribuée à la gestion des connexions.
Solutions
- Implémentez le pooling de connexions : Utilisez un pooler de connexions comme PgBouncer ou Odyssey. Ces outils maintiennent un pool de connexions de base de données ouvertes et les réutilisent pour les demandes client entrantes.
- PgBouncer : Un pooler de connexions léger et très performant. Il peut fonctionner en modes de pooling transaction, session ou instruction.
- Odyssey : Un pooler de connexions plus moderne et riche en fonctionnalités avec prise en charge de protocoles comme SCRAM-SHA-256.
- Configurez le pooler de manière appropriée : Ajustez la taille du pool, les délais d'attente et le mode de pooling en fonction des besoins de l'application et de la capacité de la base de données.
- Pooling côté application : Certains frameworks d'application fournissent des capacités de pooling de connexions intégrées. Assurez-vous qu'elles sont correctement configurées.
Les problèmes de pooling de connexions apparaissent souvent après une mise à l'échelle du déploiement. Une instance d'application avec un pool de 20 connexions peut être correcte. Trente instances avec le même paramètre de pool peuvent créer 600 sessions de base de données possibles avant même l'arrivée du trafic réel. PostgreSQL utilise un processus par connexion, donc les sessions inactives ne sont pas gratuites. Gardez les pools d'application petits, mettez PgBouncer devant lorsque de nombreuses demandes de courte durée sont attendues, et surveillez pg_stat_activity par nom d'application afin de savoir qui possède les sessions.
7. Contention de verrouillage
Lorsque plusieurs transactions tentent d'accéder et de modifier les mêmes données simultanément, elles peuvent devoir s'attendre mutuellement si elles acquièrent des verrous conflictuels. Une contention de verrouillage excessive peut ralentir les applications.
Identifier le goulot d'étranglement
pg_stat_activity: Recherchez les lignes oùwait_event_typeestLock.- Dégradation des performances de l'application : Des opérations spécifiques deviennent extrêmement lentes.
- Impasses : Transactions qui s'attendent indéfiniment les unes les autres.
- Transactions de longue durée : Maintiennent les verrous pendant des périodes prolongées.
Solutions
- Optimisez les transactions : Gardez les transactions courtes et concises. Validez ou annulez dès que possible.
- Révisez la logique de l'application : Identifiez les conditions de concurrence potentielles ou les modèles de verrouillage inefficaces.
- Utilisez des niveaux de verrouillage appropriés : PostgreSQL offre différents niveaux de verrouillage (par exemple,
ACCESS EXCLUSIVE,ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE). Comprenez et utilisez le verrou le moins restrictif nécessaire. SELECT ... FOR UPDATE/SELECT ... FOR NO KEY UPDATE: Utilisez-les judicieusement lorsque vous devez verrouiller des lignes pour modification afin d'empêcher d'autres transactions de les modifier avant la fin de votre transaction.VACUUMrégulièrement : Comme mentionné précédemment,VACUUMaide à nettoyer les tuples morts, ce qui peut parfois réduire indirectement la contention de verrouillage en empêchant les opérationsVACUUMlongues.- Vérifiez
pg_locks: Interrogezpg_lockspour voir quels processus en bloquent d'autres.SELECT blocked_locks.pid AS blocked_pid, blocked_activity.usename AS blocked_user, blocking_locks.pid AS blocking_pid, blocking_activity.usename AS blocking_user, blocked_activity.query AS blocked_statement, blocking_activity.query AS current_statement_in_blocking_process FROM pg_catalog.pg_locks blocked_locks JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocking_locks.pid != blocked_locks.pid JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid WHERE NOT blocked_locks.granted;
Lorsque PostgreSQL ralentit, rassemblez les preuves avant de modifier le système : pg_stat_statements pour la forme de la charge de travail, EXPLAIN (ANALYZE, BUFFERS) pour le chemin de la requête, pg_stat_activity pour les attentes et les connexions, et les métriques de l'hôte pour le CPU, la mémoire et les E/S. La correction est beaucoup plus claire lorsque vous savez où le temps est réellement passé.