Chargements en masse plus rapides et charges de travail OLTP plus sûres grâce à l'architecture LTAP
par Yecheng Yang, Nolan Biscaro, Szu-Po Wang et Pranav Aurora
Les bases de données opérationnelles comme Postgres sont conçues pour exécuter de manière fiable des requêtes hautement concurrentes et à faible latence sur des sous-ensembles de données. Ce qui nuit souvent à cette fiabilité, ce sont les opérations en masse, comme le chargement de téraoctets de données ou l'exécution de requêtes analytiques qui balayent une table entière. Ces opérations entrent en concurrence pour les mêmes ressources que vos charges de travail applicatives, ce qui risque d'entraîner une dégradation des performances ou des interruptions de service.
Notre objectif est de faire de Lakebase Postgres l'endroit le plus sûr et le plus fiable pour vos charges de travail opérationnelles. Nous y parvenons en déchargeant les opérations par lots lourdes du calcul principal vers des moteurs distribués comme Spark, conçus spécialement pour ces tâches. Cela est rendu possible par l'architecture LTAP (Lake Transactional/Analytical Processing), qui permet aux moteurs transactionnels et analytiques de travailler exactement sur les mêmes données dans le lake.
Sans cette isolation, les équipes sont souvent contraintes de se montrer extrêmement prudentes avec les opérations en masse. Elles sacrifient la fraîcheur des données, exécutent rarement les chargements pendant les heures creuses et gèrent manuellement les backfills et le checkpointing complexes. Aujourd'hui, en tirant parti de l'architecture LTAP, elles déchargent entièrement leurs pipelines d'ingestion vers Spark, garantissant ainsi que les applications bénéficient de données fraîches sans compromettre les performances du système en production.
Durant notre phase bêta, un client utilisait Synced Tables pour charger environ 1 milliard de lignes dans Lakebase chaque jour. En exploitant l'architecture LTAP, nous avons considérablement accéléré ces chargements tout en maintenant leurs charges de travail opérationnelles totalement indemnes.

Auparavant, leur chargement en masse prenait plus de 8 heures et saturait complètement leur CPU et leur mémoire. Même avec l'autoscaling de Lakebase, ils étaient contraints de surprovisionner lourdement leurs ressources OLTP juste pour survivre à la synchronisation. Cela s'explique par le fait que les architectures Postgres traditionnelles font de l'instance principale le seul garant de l'état durable. Les chargements en masse sont forcés de passer par ce goulet d'étranglement unique, ce qui signifie que chaque ligne importée génère des pages de tas (heap pages), met à jour des index et écrit des enregistrements WAL sur la même instance servant votre trafic applicatif en direct.
L'architecture LTAP a entièrement soulagé la pression sur leur instance principale, protégeant ainsi le trafic applicatif en direct. Vous bénéficiez de toute la puissance d'un moteur distribué, qui augmente le débit de chargement de manière presque linéaire à mesure que vos données augmentent. Nos benchmarks internes montrent que le chargement de 1 To prend désormais moins de 5 minutes.

Remarque : Ce benchmark mesure le temps de chargement des données (construction des heap pages). Nous travaillons activement à la parallélisation de la construction des index pour ces données chargées également.

Dans la suite de cet article, nous plongeons au cœur des défis liés au chargement en masse de données dans une base de données OLTP comme Postgres, et nous explorons comment nous tirons parti de l'architecture LTAP pour les résoudre.
La commande Postgres native COPY est efficace pour les ingestions standard et de plus petite taille.
Cependant, à mesure que les clients rapprochent leurs environnements opérationnels et analytiques, l'échelle change. Une charge de travail de plus en plus courante consiste à servir d'imposantes tables Lakehouse de niveau Gold à des applications ayant des modèles de requêtes opérationnelles. Injecter des données à ce volume extrême dans une base de données de production révèle deux limites fondamentales :
Cela reste vrai même si vous lancez le chargement sous la forme d'un job Spark distribué. Spark peut lire les partitions sources en parallèle, mais chaque ligne doit tout de même passer par un seul scripteur Postgres :
COPYLes chargements en masse traditionnels sont goulotés par un scripteur unique
Le côté source peut faire l'objet d'une mise à l'échelle horizontale, mais pas le côté destination. Ajouter des exécuteurs Spark accélère le balayage, mais ne supprime pas le goulet d'étranglement du scripteur unique. De plus, cette même instance principale Postgres sert vos transactions applicatives en ligne. Un import volumineux entre en concurrence avec elles pour le CPU, la mémoire, les E/S, les connexions et la bande passante WAL. La latence augmente, les équipes planifient les chargements sur des plages de faible activité et approvisionnent souvent l'instance principale pour l'import le plus important plutôt que pour le trafic quotidien.
L'architecture LTAP offre une alternative, car l'instance principale n'est plus le seul moyen de créer un état Postgres durable. Le calcul transactionnel est sans état dans lakebase : l'état durable réside dans une couche de stockage distribuée, et non sur le disque local de l'instance principale. D'une certaine manière, Postgres est un client du stockage — il sert les requêtes et les transactions, mais n'a pas besoin d'être le processus qui matérialise chaque nouvelle page.

Pour les chargements en masse, cela signifie que Spark peut construire l'état Postgres et l'écrire dans le stockage, tandis que l'instance principale se contente de publier le résultat.
Les conséquences opérationnelles sont :
COPY des autres.Il existe différentes optimisations pour construire les fichiers Postgres et générer l'index de clé primaire.
Les pages générées par Spark doivent être valides pour la base de données de destination, tout comme si l'instance principale les avait construites elle-même.
Chaque exécuteur Spark lance une instance Postgres isolée (sandbox) en mode binary-upgrade, le même mécanisme que pg_upgrade utilise pour préserver les OID du catalogue lors des mises à niveau vers une version majeure. Nous l'utilisons pour transplanter les OID du catalogue de destination dans chaque sandbox, garantissant que les OID intégrés dans les pages générées correspondent à ceux de la destination. Le driver attribue également des plages d'OID disjointes aux sandboxes afin que les objets créés simultanément n'entrent pas en collision.
À l'intérieur de chaque sandbox, un binaire COPY avec FREEZE construit la portion du heap associée à cet exécuteur. Le « freezing » marque les n-uplets importés comme étant déjà validés, de sorte que Postgres peut les considérer comme visibles sans consulter l'historique des transactions de la sandbox.
Chaque worker calcule ensuite des sommes de contrôle (checksums) pour chaque page générée. Les pageservers de Lakebase valident ces sommes de contrôle lorsqu'ils ingèrent les fichiers, ce qui permet de détecter la corruption avant que les pages importées ne fassent autorité.
Ensemble, le mode binary-upgrade, les n-uplets congelés (frozen tuples), les OID coordonnés et la validation par somme de contrôle garantissent que Spark produit des pages que la destination peut lire comme de simples pages Postgres. Nous mettons cela en œuvre via des extensions Postgres et une méthode d'accès aux tables, sans modifier le cœur de Postgres.
Les sandboxes jetables permettent également des optimisations de performances, telles que les tables d'aide UNLOGGED. Il s'agit d'optimisations et non de mécanismes de sécurité : elles évitent les verrouillages inutilement concurrents et l'utilisation superflue du journal WAL, car aucun trafic applicatif ne partage la sandbox.
| S'agit-il toujours de « simplement Postgres » ? |
|---|
| Oui. Les fichiers écrits par Spark sont des pages Postgres, et non un format d'importation que l'instance principale traduit ultérieurement. Nous avons ajouté des extensions et une méthode d'accès aux tables autour de cette voie, ce qui constitue l'un des superpouvoirs de Postgres, nous permettant d'enrichir ses fonctionnalités sans modifier le code source. |
Un B-tree est une structure ordonnée sur tout l'espace de clés, et ses entrées feuilles contiennent des paires de clés et d'ID de tuple qui pointent vers le heap.
Normalement, Postgres obtient ces paires en balayant le heap. Ici, cependant, le heap est distribué entre plusieurs tranches transférées, et tout télécharger vers un worker de création d'index annulerait une grande partie des avantages de la construction en parallèle.
Le constructeur d'index n'a pas réellement besoin du contenu du heap. Il a besoin du flux de paires (key, ctid) qu'un balayage de heap produirait. Chaque worker de heap de l'étape précédente exporte donc ses colonnes clés et ses ID de tuple vers un fichier de stockage d'objets distinct tout en construisant sa tranche.
Nous injectons ces enregistrements dans une table auxiliaire contenant uniquement les colonnes clés et l'ID de tuple exportés plutôt que les lignes d'origine. Pendant CREATE INDEX, une méthode d'accès à la table personnalisée balaye la table auxiliaire de façon identique à heapam, mais écrit le ctid dans chaque entrée d'index au lieu de l'ID de tuple physique propre à la ligne auxiliaire. À mesure que les enregistrements sont chargés, chaque ctid local à la tranche est décalé de la taille cumulative des tranches de heap précédentes, ce qui le fait pointer vers l'emplacement final du tuple dans le heap concaténé. En résumé, le constructeur B-tree standard de Postgres peut balayer cette table auxiliaire sans modification et produire un index sur un heap qu'il n'a jamais téléchargé. Cela remplace le déplacement de l'ensemble du heap par le déplacement de la représentation clé-et-ID-de-tuple, beaucoup plus petite.
Une fois les tranches de heap et d'index dans le stockage d'objets, le driver Spark écrit un manifeste et invoque une fonction SQL sur la destination. Le nœud principal enregistre l'importation sous forme d'enregistrement WAL compact. Un COPY conventionnel enverrait la totalité du volume de données via le quorum de safekeepers. Nous n'envoyons que la description de l'importation.
Les pageservers revendiquent les fichiers transférés comme pages faisant autorité et valident leurs sommes de contrôle. Ils préchauffent également les pages sur le SSD local afin que les premières lectures n'entraînent pas de récupérations à froid sur le stockage d'objets. Enfin, le nœud principal permute de manière atomique les données préparées dans la table synchronisée visible par l'utilisateur.
Jusqu'à ce que cette transaction soit validée, la table importée reste isolée. Ensuite, le nœud principal voit des pages standard de heap et de B-tree produites via les formats et interfaces Postgres habituels.
Nous utilisons l'architecture LTAP pour alimenter les Synced Tables, ce qui permet des synchronisations beaucoup plus rapides lors de la diffusion de jeux de données gold depuis votre Lakehouse. Si vous exécutez des tâches de ReverseETL manuelles depuis le Lakehouse avec Lakebase, vous devriez envisager d'utiliser Lakebase.
Nous sommes ravis de généraliser ce protocole pour gérer de grandes opérations et maintenances, comme la création d'index ou même les migrations. Lakebase avec l'architecture LTAP est le meilleur endroit pour exécuter des charges de travail OLTP.
Si vous n'avez pas encore essayé Lakebase et Synced Tables, lancez-vous dès aujourd'hui.
(Cet article de blog a été traduit à l'aide d'outils basés sur l'intelligence artificielle) Article original
Abonnez-vous à notre blog et recevez les derniers articles directement dans votre boîte mail.