16 points par GN⁺ 2026-05-04 | 11 commentaires | Partager sur WhatsApp
  • 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 fonction s_lock
    • Chemin d’appel : StartReadBufferGetVictimBufferStrategyGetBuffers_lock

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

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
  • StrategyGetBuffer utilise 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 t s’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

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
  • StrategyGetBuffer ne 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)
    • try utilise 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
  • 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)
    • rseq est 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 rseq au 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)

11 commentaires

 
y15un 2026-05-04

Franchement, plus j’y pense, plus je trouve que le titre est mal choisi.

https://fr.news.hada.io/topic?id=28241#cid54772
Le mainteneur principal du kernel dit qu’il s’agit d’un point qu’il recommandait à postgres depuis très longtemps ; du coup, le bon titre serait plutôt « Pourquoi Postgres ralentit sur Linux 7.0 » que de dire que 7.0 a cassé Postgres.

Même si, à strictement parler, le kernel ne suit pas toujours le semver, ça reste quand même un changement de version majeure ; sérieusement, présenter un problème qu’on s’est soi-même créé de cette façon ??

 
domuji6 2026-05-05

rseq a bien été proposé comme alternative, mais comme cela impliquerait d’introduire du code spécifique à Linux, cela me semble être une proposition difficile à accepter facilement pour un projet open source qui doit prendre en compte la compatibilité multiplateforme.

On peut comprendre qu’un changement de comportement survienne lors d’une montée de version majeure, mais si cela entraîne au final une baisse de performances de 50 %, j’ai l’impression que, du point de vue de l’exploitation d’une infrastructure, on ne peut qu’aborder avec prudence la mise à niveau du kernel elle-même.

 
y15un 2026-05-06

Wow, j’ai posté un commentaire pendant un déplacement, et en arrivant à l’hôtel, j’ai vu que trois personnes avaient déjà donné leur avis. Merci.

Je comprends tout à fait le point de vue que vous avez exprimé, mais je considère tout de même qu’il s’agit ici d’une dette technique du côté de postgres, et qu’au final, c’est à postgres de dénouer le problème. (J’ai l’impression qu’on a déjà suffisamment payé le prix de hacks utilisés pour gagner en performance immédiate avec Spectre...)
Au final, il va sans doute falloir observer cette affaire encore un moment.

Passez une excellente journée. :)

 
ilsubyeega 2026-05-06

Je suis d’accord. J’entends parfois parler de départs à la retraite d’ingénieurs Linux chez Intel, et si on continue à laisser traîner les comportements existants, ça finira un jour par devenir comme Windows o,o..

 
xenoside 2026-05-05

Cette implémentation est déjà une zone remplie de code assembleur dédié à chaque plateforme pour des performances optimales,
donc le fait qu’un code spécifique à une plateforme donnée soit ajouté ne me semble pas pouvoir constituer une raison valable.
(Résumé après avoir interrogé Gemini.)

 
nanashi222 2026-05-06

J’ai regardé le code, et ce n’est pas une fonction remplie d’assembleur à ce point ; et le fait qu’elle contienne beaucoup d’assembleur ne signifie pas, à mon avis, que l’ajout de code spécifique à une plateforme poserait problème. Par « code assembleur », il semble qu’on parle de fonctions d’opérations atomiques (les built-ins __atomic__ de GCC), mais si on regarde uniquement la fonction elle-même, on ne peut pas vraiment ajouter du code de façon particulière pour Linux.

 
jjw9512151 2026-05-11

Comme c’est de l’open source, j’imagine que modifier ça et le tester représente aussi une certaine charge… Et il y a énormément d’utilisateurs aussi.

 
hiseob 2026-05-05

Certes, on se souvient que Linus-god avait déjà grondé avec un « WE DO NOT BREAK USERSPACE! », donc on peut aussi se dire qu’il aurait peut-être fallu proposer une option.
Mais d’un autre côté, vouloir absolument continuer à utiliser un spinlock en userspace, ça ne me paraît pas très logique non plus.
C’est un peu cette impression-là.

 
cafedead 2026-05-04

« Ne cassez pas l’espace utilisateur » vs « Ne faites pas de spinlock dans l’espace utilisateur »

 
savvykang 2026-05-05

J’ai du mal à comprendre que, pendant 30 ans, en réinventant en partie des fonctionnalités de l’OS, personne n’ait pensé à documenter les limites ni les raisons. Il y avait pourtant sûrement des raisons valables, il y a 30 ans, pour implémenter sa propre synchronisation, sa propre gestion mémoire et même son propre modèle de processus.

 
hungryman 2026-05-05

À l’ère de l’IA, entendre qu’il y a quand même ce genre de réaction négative, ça veut dire que de ce côté-là, au niveau de l’architecture, c’est devenu un enchevêtrement complexe ?