Comment Linux 7.0 a cassé PostgreSQL
(read.thecoder.cafe)- Avec la suppression du mode de préemption PREEMPT_NONE, qui était la valeur par défaut traditionnelle des serveurs, dans Linux 7.0, une grave régression de performances est apparue : sur un matériel identique, le débit de PostgreSQL a été divisé par deux
- Lorsqu’un ingénieur AWS a exécuté pgbench sur une machine Graviton4 à 96 vCPU, les transactions par seconde sont passées de 98 565 à 50 751 entre Linux 6.x et Linux 7.0, tandis que 55 % du CPU était consommé par une seule fonction de spinlock
- Le spinlock qui protège l’accès au shared buffer pool de PostgreSQL, combiné à des fautes de page mineures sur des pages mémoire de 4 KB, fait que si une préemption par l’ordonnanceur survient pendant que le verrou est détenu, tous les backends en attente tournent à vide et gaspillent du CPU
- L’activation des Huge Pages (2 MB ou 1 GB) réduit le nombre potentiel de fautes de page de 31 millions à quelques dizaines de milliers, voire quelques centaines, ce qui élimine la régression
- Côté noyau, l’adoption de Restartable Sequences (rseq) a été proposée, mais la communauté PostgreSQL estime qu’une baisse de performances causée par une mise à niveau du noyau viole le principe selon lequel cela ne doit pas « casser l’espace utilisateur »
Le problème observé
- L’ingénieur AWS Salvatore Dipietro a exécuté pgbench sur un processeur Graviton4 à 96 vCPU, avec un facteur d’échelle de 8 470 (table d’environ 847 millions de lignes), 1 024 clients et 96 threads, dans un test de charge hautement parallèle
- Le débit est passé de 98 565 TPS sous Linux 6.x à 50 751 TPS sous Linux 7.0, soit presque une division par deux
- D’après le profilage
perf, 55,60 % du temps CPU était consommé dans la fonctions_lock- Chemin d’appel :
StartReadBuffer→GetVictimBuffer→StrategyGetBuffer→s_lock
- Chemin d’appel :
Qu’est-ce que la préemption ?
- La préemption correspond à la décision de l’ordonnanceur de l’OS d’interrompre un thread en cours d’exécution pour donner le CPU à un autre thread
- Avant Linux 7.0, trois options existaient
- PREEMPT_NONE : le thread est très rarement interrompu avant de céder volontairement le CPU (syscall, blocage d’E/S, sleep). C’était la valeur par défaut traditionnelle des serveurs, avec peu de changements de contexte et un débit élevé
- PREEMPT_FULL : le thread en cours peut être interrompu à presque tous les points sûrs. Le temps de réponse baisse, mais le coût des changements de contexte augmente. C’était la valeur par défaut traditionnelle des postes de travail
- PREEMPT_LAZY : compromis introduit dans Linux 6.12, qui attend des frontières naturelles tout en autorisant la préemption si nécessaire. Il a été conçu pour se rapprocher des caractéristiques de débit de PREEMPT_NONE
- Dans Linux 7.0, PREEMPT_NONE a été supprimé sur les architectures CPU modernes, ne laissant plus que PREEMPT_FULL et PREEMPT_LAZY
- Si PREEMPT_LAZY fonctionne comme remplaçant pour la plupart des logiciels serveur, il crée une différence critique pour PostgreSQL
La gestion mémoire de PostgreSQL
- PostgreSQL utilise des pages de données de taille fixe (8 KB par défaut) comme unité de stockage de base ; les lignes de table, les nœuds d’index B-tree, les métadonnées, etc. sont tous stockés dans ces pages
- Pour réduire les lectures disque, il met en cache les pages de données récemment lues dans une grande zone de mémoire partagée appelée shared buffer pool
- Lorsqu’un client se connecte, un processus backend dédié est créé ; si une page n’est pas présente dans le buffer pool, il faut la lire depuis le disque puis trouver un buffer libre ou expulsable
- La fonction chargée de cette sélection de buffer est
StrategyGetBuffer
- La fonction chargée de cette sélection de buffer est
Les spinlocks de PostgreSQL
- Un spinlock est un mécanisme de verrouillage qui, au lieu d’endormir le thread en attente, le fait tourner en boucle en vérifiant sans cesse si le verrou est disponible
- Pour des sections critiques très courtes, cette rotation est plus efficace que le coût d’endormir puis de réveiller le thread
- L’hypothèse clé est la suivante : le thread qui détient le verrou va le libérer très rapidement
StrategyGetBufferutilise un unique spinlock global pour protéger la sélection des buffers- Dans un environnement à 96 vCPU et 1 024 clients, tous les backends se disputent le même verrou
Mémoire virtuelle et TLB
- Tous les processus utilisent des adresses mémoire virtuelles, que le matériel traduit en adresses physiques via des tables de pages organisées en arbre multiniveau
- Comme parcourir la table de pages à chaque accès serait lent, le CPU dispose d’un TLB (Translation Lookaside Buffer) qui met en cache les traductions récentes
- En cas de succès dans le TLB, l’accès est rapide ; en cas de TLB miss, un parcours de la table de pages est nécessaire, ce qui prend du temps
- Linux applique le principe de l’allocation paresseuse (
lazy allocation) : lors de l’allocation de mémoire virtuelle, les pages physiques réelles ne sont mappées qu’au premier accès- Au premier accès, une faute de page mineure survient : le noyau alloue une page physique et enregistre le mapping ; cela prend généralement plusieurs microsecondes, donc bien plus qu’une simple lecture/écriture
Le problème des pages de 4 KB
- Dans le benchmark,
shared_buffersétait réglé sur 120 GB, soit environ 31 millions de pages mémoire avec des pages de 4 KB, donc 31 millions de fautes de page potentielles au premier accès - Dans un benchmark de longue durée utilisant un shared buffer pool de 120 GB, de nouvelles zones mémoire continuent d’entrer dans le working set, de sorte que les fautes de page ne surviennent pas seulement au démarrage, mais en continu
- Si, dans
StrategyGetBuffer, un accès à la mémoire partagée survient alors qu’un spinlock est détenu et que cette zone n’est pas encore mappée, une faute de page mineure se produit - Avec PREEMPT_NONE (avant Linux 7.0), même si le backend A entre dans le gestionnaire de faute de page, il évite les points de réordonnancement volontaires, donc il a peu de chances d’être déschedulé avant la résolution de la faute. Le temps d’attente devient plus long que prévu, mais l’impact reste limité
- Avec PREEMPT_LAZY (après Linux 7.0), l’ordonnanceur peut préempter le backend A à l’intérieur du gestionnaire de faute de page et planifier un autre processus. Même une fois la faute résolue, un délai supplémentaire
ts’ajoute jusqu’à ce que l’ordonnanceur lui rende le contrôle- Ce délai supplémentaire n’entraîne pas seulement un coût de
t, mais un gaspillage CPU amplifié à nombre de backends actuellement en rotation × t - Sur 96 vCPU avec des centaines de backends, cet effet multiplicateur devient critique, au point que 56 % du CPU finit consommé dans
s_lock
- Ce délai supplémentaire n’entraîne pas seulement un coût de
Résolution via Huge Pages
- Avec
shared_buffersà 120 GB, changer la taille des pages mémoire fait chuter drastiquement le nombre de fautes de page potentielles- Pages de 4 KB : ~31 000 000 fautes de page potentielles
- Huge Pages de 2 MB : ~61 440
- Huge Pages de 1 GB : ~120
- Augmenter la taille des pages ne réduit pas seulement le nombre de fautes de page, mais allège aussi la pression sur le TLB : bien moins d’entrées TLB suffisent à couvrir la même quantité de mémoire, ce qui réduit les TLB miss et les parcours de tables de pages
StrategyGetBufferne déclenche alors plus de faute pendant la détention du verrou, le détenteur du verrou termine rapidement et les autres backends n’attendent plus que des microsecondes au lieu de millisecondes. La régression disparaît- Dans PostgreSQL, le réglage des huge pages est contrôlé par le paramètre
huge_pages- Trois valeurs sont prises en charge :
off,on,try(valeur par défaut) tryutilise les huge pages si possible, sinon revient silencieusement à 4 KB, ce qui peut faire passer inaperçue une mauvaise configuration- Avec
on, PostgreSQL échoue au démarrage si les huge pages ne peuvent pas être utilisées, ce qui permet de détecter immédiatement le problème
- Trois valeurs sont prises en charge :
- Compromis : les huge pages reposent sur une allocation/réservation préalable ; même si PostgreSQL n’utilise pas toute cette mémoire, le reste du système ne peut pas s’en servir. Si seule une partie d’une page est utilisée, le reste est perdu. Dans les environnements de production avec de grands
shared_buffers, ce compromis vaut généralement la peine
Et maintenant ?
- Peter Zijlstra, ingénieur noyau chez Intel qui a conçu ce changement de préemption, a proposé que PostgreSQL adopte Restartable Sequences (rseq)
rseqest une fonctionnalité du noyau Linux qui permet au code user space de détecter une préemption ou une migration au sein d’une section critique et de redémarrer cette section- L’application de
rseqau chemin de spinlock de PostgreSQL permettrait d’éviter le scénario où un détenteur de verrou préempté retarde tous les backends en attente
- La réaction de la communauté PostgreSQL a été négative
- Il est difficilement acceptable de devoir adopter une fonctionnalité spécifique du noyau pour retrouver des performances qui étaient auparavant acquises sans effort sous Linux 7.0
- Selon elle, cela va à l’encontre du principe historique du noyau : « ne pas casser l’espace utilisateur » (un logiciel qui fonctionnait correctement avant une mise à niveau du noyau doit continuer à fonctionner correctement après)
Aucun commentaire pour le moment.