Revenir au contenu principal
Data Engineering

Amélioration du cache de calcul Lakebase Postgres

Partie 1 : Comment les grands nœuds de calcul Postgres s'exécutent jusqu'à 2x plus vite avec une latence plus faible

par David Wein, Sunil Kamath et Haoyu Huang

  • Le cache Postgres standard vs le cache Postgres Lakebase
  • Comment nous avons créé un cache avec mise à l'échelle automatique qui fonctionne en tandem avec les tampons partagés et conserve autant de données que possible sur les ressources de calcul
  • Résultats en production, notamment un débit 2x plus élevé, moins de lectures à partir de la couche de stockage et une latence plus faible

Le modèle de stockage désagrégé de Lakebase Postgres fournit une plateforme riche en fonctionnalités, flexible et économique. Un cache de données efficace est essentiel pour offrir un débit élevé et une faible latence lorsque les données sont sauvegardées dans un magasin d'objets tel que S3.

Cette mise en cache s'effectue sur deux niveaux : dans le stockage distribué, où les pages Postgres sont matérialisées pour un débit d'écriture élevé et le traitement des lectures ; et sur le calcul Postgres lui-même pour servir les pages fréquemment consultées depuis la DRAM pour un accès ultra-rapide.

Nous avons travaillé dur pour améliorer la mise en cache côté calcul, et dans ce blog, nous présenterons nos plans à court terme et détaillerons ce qui a déjà été livré aux clients.

Tout d'abord, un peu de contexte sur la façon dont nous en sommes arrivés là.

Le cache Postgres standard

Les bases de données sont réputées pour être très gourmandes en DRAM (mémoire). Elles utilisent principalement cette mémoire comme cache de données et s'attendent à ce que l'accès aux lignes du cache se mesure en nanosecondes, soit des ordres de grandeur plus rapides que les disques NVMe les plus rapides.

Postgres organise les données en lignes sur des pages, et les pages activement consultées doivent être chargées dans une zone mémoire appelée "shared buffers". Comme Postgres stocke traditionnellement les pages en utilisant le système de fichiers du système d'exploitation, le noyau de l'OS utilise également son cache de pages flexible pour assurer la mise en cache entre les shared buffers de Postgres et le disque.

Ce schéma combinant shared buffers et cache de pages fonctionne assez bien, mais présente certains inconvénients et défis.

Inconvénients

  1. Le double tamponnage (double buffering), qui réduit la quantité de données que vous pouvez réellement mettre en cache sur le calcul. Prenons l'exemple d'un calcul avec 4 GB de RAM utilisant 1 GB pour les shared buffers. Lorsque vous lisez des pages sur le disque pour remplir le 1 GB de shared buffers, les lectures passent par le cache de pages de l'OS, qui conserve également ces données. Vous consommez alors 2 GB de RAM pour mettre en cache 1 GB de données.
  2. Le cache de pages de l'OS ne sait rien des shared buffers ni du fonctionnement interne de Postgres, il ne peut donc pas prendre de décisions intelligentes sur les pages à remplacer.

Défis techniques

  1. Dans un système de stockage désagrégé tel que Lakebase Postgres, les données lues depuis le stockage ne transitent pas par le système de fichiers ou le cache de pages de l'OS.
  2. Le paramètre shared buffers est statique, ce qui signifie qu'il est défini avant le démarrage de Postgres et ne peut pas être modifié sans redémarrer la base de données. Il s'agit d'un défi de taille pour un système d'autoscaling serverless tel que Lakebase.
  3. Postgres utilise un processus de système d'exploitation distinct pour chaque connexion active. Ainsi, plus les shared buffers sont volumineux (c'est-à-dire plus vous allouez de mémoire à Postgres), plus l'OS doit gérer de mémoire pour chaque connexion, ce qui consomme à son tour de la mémoire.

Le chemin du cache Lakebase

image3.png

Maintenant que nous avons posé les bases, voyons comment nous résolvons ces problèmes chez Databricks.

Notre objectif final est d'optimiser l'utilisation de la DRAM sur votre calcul via des shared buffers dynamiques Postgres qui s'adaptent automatiquement à votre charge de travail et utilisent jusqu'à 75 % de la mémoire disponible.

À terme, nous devons adapter notre plateforme de calcul pour tirer parti de l'autoscaling des shared buffers, mais nous souhaitons également proposer des améliorations progressives et concrètes à nos clients dès qu'elles sont disponibles. Chaque livraison incrémentielle nous permet de déployer en toute confiance une ou plusieurs étapes de notre feuille de route tout en apportant un réel bénéfice aux clients. Ainsi, même si l'objectif reste les calculs avec autoscaling, nous avons commencé par des calculs fixes, comme décrit dans la section suivante.

Voici ce que nous avons implémenté.

Des shared buffers plus volumineux

image1.png

Comme mentionné dans les défis techniques ci-dessus, un système désagrégé tel que Lakebase n'oriente pas ses lectures à travers le système de fichiers standard de l'OS et son cache de pages. Rappelez-vous également que les shared buffers de Postgres sont statiques et ne peuvent pas s'adapter automatiquement.

Pour résoudre ce problème, nous avons créé une couche appelée local file cache (LFC). Le LFC a servi d'intermédiaire, créant un cache avec autoscaling qui fonctionnait en tandem avec les shared buffers et conservait autant de données que possible en cache sur le calcul. Cette solution ingénieuse et pragmatique a permis à Lakebase Postgres de lancer l'autoscaling et est utilisée sur tous les calculs depuis le lancement.

Bien qu'elle soit présentée aux utilisateurs comme un cache de calcul unique à haute vitesse, l'architecture sous-jacente prend en charge jusqu'à deux niveaux :

  • Shared buffers : le tampon partagé en mémoire de Postgres, qui représente le chemin d'accès à la plus faible latence.
  • Local file cache : un cache secondaire étendu résidant sur le NVMe local du nœud de calcul, offrant une capacité supérieure à celle de la mémoire mais nécessitant des E/S disque pour accéder à une page.

Les shared buffers ont été configurés de manière prudente afin de ne pas consommer trop de mémoire lors de l'exécution au minimum de CU configuré, la taille maximale jamais configurée étant de 1 GB pour les shared buffers, le LFC consommant le reste de la capacité totale du cache de calcul (jusqu'à 75 % de la DRAM). Toute requête qui se solde par un échec (miss) sur les deux niveaux est redirigée du nœud de calcul vers la couche de stockage distribué.

Sur des ensembles de données de travail plus volumineux, le plafonnement des shared buffers à 1 GB obligeait la plupart des accès réussis au cache (cache hits) à passer par le niveau LFC, plus lent. Le LFC nous a bien servi, mais notre intention est de retirer sa forme actuelle à mesure que nous progressons vers des shared buffers entièrement dynamiques.

Remarque : les calculs fixes ont été prioritaires

Notre premier déploiement de shared buffers plus volumineux cible les calculs de taille fixe, car les shared buffers ne sont pas encore dynamiques. Sur ceux-ci, nous désactivons désormais le LFC et définissons les shared buffers à 75 % de la DRAM. C'est effectif aujourd'hui pour les calculs de taille fixe avec CU >= 80. L'élimination du plafond de tampon d'environ 1 GB permet de conserver les pages actives (hot pages) dans la couche mémoire la plus rapide au lieu de les faire descendre vers le stockage de fichiers local.

Pour vérifier si les shared buffers volumineux sont activés pour votre calcul, exécutez show shared_buffers au sein d'une connexion Postgres. Un point de terminaison Lakebase de 80 CU dans Databricks devrait afficher une valeur de 15278640.

Conserver les données actives dans les shared buffers plutôt que dans le cache de pages de l'OS permet également de résoudre les inconvénients décrits précédemment. Il n'y a pas de double tamponnage, de sorte que 1 GB de données mises en cache consomme 1 GB de RAM au lieu de 2 GB. Et comme le cache réside dans Postgres plutôt que dans le noyau, les décisions d'éviction peuvent être prises en connaissant l'état de la base de données, ce qui nous permet d'envisager des politiques de remplacement plus intelligentes que ce que l'OS peut proposer.

Le dimensionnement des shared buffers à 75 % de la DRAM sur les calculs de taille fixe n'a pas été aussi simple qu'une simple modification de configuration. Cela est dû au troisième défi technique, à savoir l'architecture d'un processus par backend.

Cette section suivante décrit notre solution.

Résoudre la surcharge de mémoire et de traduction avec les huge pages

Postgres utilise une structure basée sur des processus dans laquelle chaque backend mappe les shared buffers dans son propre espace d'adressage, ce qui nécessite ses propres entrées de table de pages (les structures gérées par le noyau que le matériel parcourt pour traduire les adresses virtuelles en mémoire physique). Par défaut, Linux effectue ce mappage sur des pages de 4 KB.

Quelques chiffres simples : chaque 1 GB de shared buffers correspond à 262 144 entrées de table de pages par processus. Avec 32 GB de shared buffers et 512 backends, cela représente environ 4,3 milliards d'entrées, soit environ 32 GB de tables de pages pour mapper 32 GB de cache.

Cet ensemble de travail dépasse également de loin la capacité du Translation Lookaside Buffer (TLB), un cache dans l'unité de gestion de mémoire du CPU qui accélère la traduction virtuel-physique. Même un accès réussi au shared buffer subit alors une pénalité due aux échecs de TLB (TLB misses) et aux parcours de tables de pages.

Pour atténuer ce problème, la communauté Postgres conseille d'utiliser un mécanisme de l'OS appelé huge pages (de 2 MB chacune) avec des shared buffers volumineux. Le passage aux huge pages réduit la taille des tables de pages d'un facteur 512 et diminue considérablement les taux d'échec de TLB.

Lors de nos tests de référence (benchmarks), la configuration de Postgres avec des huge pages a permis de réduire la latence de lecture de traîne (tail latency) jusqu'à environ 40 % et de diminuer l'utilisation du CPU jusqu'à environ 30 %.

Prise en charge des huge pages dans les environnements virtualisés

Lakebase Postgres s'exécute au sein de machines virtuelles invitées légères sur des hôtes bare-metal. La traduction d'adresses mémoire implique deux couches virtualisées. Tirer parti des huge pages nécessite une implémentation cohérente sur l'ensemble de la pile : de la réservation au niveau de l'hôte, en passant par l'hyperviseur qui soutient la mémoire de la VM, jusqu'au noyau invité. Une défaillance à n'importe quel niveau dégrade les gains de performance obtenus.

Nous avons récemment introduit une prise en charge dédiée des huge pages sur l'ensemble de notre infrastructure de VM. Nous avons choisi d'utiliser des pages HugeTLB explicites de 2 MB plutôt que de nous appuyer sur des transparent huge pages au mieux (best-effort). Désormais, les VM allouées pour les calculs de grande taille fixe s'initialisent avec un volume prédéterminé de huge pages suffisant pour le démarrage de Postgres. Afin d'optimiser les ressources système, le démarrage du calcul libère automatiquement toutes les huge pages excédentaires par rapport à celles requises par Postgres.

Conseil : Pour vérifier si les grandes huge pages explicites sont activées pour votre calcul, exécutez show huge_pages au sein d'une connexion Postgres. Un point de terminaison Lakebase de 80 CU devrait afficher une valeur de "on"

Résultats en production

Le déploiement a commencé région par région il y a quelques semaines. Les exemples ci-dessous ont été mesurés sur de grands points de terminaison de production après le redémarrage qui a activé la nouvelle configuration.

Exemple 1 : débit ~2× supérieur, 5× moins de lectures depuis le stockage

Sur un grand point de terminaison, le changement est devenu actif vers 06:10 UTC le 11 août. Le nombre de blocs Postgres accédés par seconde a doublé, ce que nous utilisons ici comme indicateur du débit. Le client a signalé une latence p50 et p99 plus faible par rapport au jour, à la semaine et au mois précédents.

image2.png

Ce point de terminaison a configuré un grand cache de fichiers local. Avec des tampons partagés plus grands, le GetPage/s du stockage est passé d'environ 8K par seconde à environ 1,5K.

image4.png

Exemple 2 : débit 1,3× supérieur

Sur un autre grand point de terminaison, le changement est devenu actif vers 01:30 UTC le 14 août. Le débit a augmenté d'environ 43 %.

image7.png

Le taux de réussite du cache de calcul a atteint près de 100 %, les requêtes étant presque entièrement servies à partir des tampons partagés.

image8.png

Exemple 3 : utilisation du CPU 5× plus faible, débit 2× plus élevé

Sur cette charge de travail, l'utilisation du CPU est passée de 20 cœurs à 4 après le déploiement du 15 août. Le taux de réussite du cache de calcul a grimpé à près de 100 %, et le débit mesuré a doublé.

image5.png

image6.png

Partie 2 : mise à l'échelle automatique

Nous travaillons actuellement à l'intégration de tampons partagés plus grands pour les calculs Postgres avec mise à l'échelle automatique. La mise à l'échelle automatique introduit une complexité supplémentaire : nous devons étendre dynamiquement les tampons partagés lors de la mise à l'échelle vers le haut et les réduire lors de la mise à l'échelle vers le bas, tout en allouant le volume exact requis de pages géantes (huge pages).

Pour aller au-delà des calculs à taille fixe, nous avons développé un protocole pour la mise à l'échelle automatique des pages géantes (huge pages) fournies à l'invité. Les pages géantes sont mises à l'échelle de concert avec les tampons partagés dynamiques, ce qui nous permet de maintenir une traduction d'adresses efficace même avec une simultanéité et des tailles de mémoire élevées. Notre prochain article (partie 2) abordera les détails techniques de cette implémentation de tampons partagés dynamiques, y compris l'état actuel de Postgres open source et les domaines dans lesquels nous avons choisi de faire progresser la fonctionnalité et de contribuer en amont.

Essayez-le

Toutes ces améliorations de performances découlent de l'architecture Lakebase Postgres. La couche de stockage sert de système d'enregistrement de référence, un nœud de calcul est sans état et sa mémoire sert de couche de mise en cache.

Déployez Lakebase Postgres et mettez les performances à l'épreuve. Commencez dès aujourd'hui.

Lakebase Postgres peut être utilisé comme une base de données autonome, et vous pouvez également l'intégrer au reste de la plateforme Databricks Data + AI : gouvernance Unity Catalog, analyses lakehouse, notebooks et flux de travail AI.

(Cet article de blog a été traduit à l'aide d'outils basés sur l'intelligence artificielle) Article original

Recevez les derniers articles dans votre boîte mail

Abonnez-vous à notre blog et recevez les derniers articles directement dans votre boîte mail.