Maison >interface Web >js tutoriel >Gestion des données fragmentées dans les systèmes distribués : analyse approfondie des jointures, des diffusions et de l'optimisation des requêtes

Gestion des données fragmentées dans les systèmes distribués : analyse approfondie des jointures, des diffusions et de l'optimisation des requêtes

Patricia Arquette
Patricia Arquetteoriginal
2024-12-23 13:50:18981parcourir

Handling Sharded Data in Distributed Systems: A Deep Dive into Joins, Broadcasts, and Query Optimization

Dans les bases de données distribuées modernes, la nécessité de mettre à l'échelle les données horizontalement a conduit à l'adoption généralisée du sharding. Bien que le partitionnement permette de gérer de grands ensembles de données sur plusieurs nœuds, il introduit des défis, en particulier lors de l'exécution de jointures et de la garantie d'une récupération efficace des données. Dans cet article, nous explorons divers concepts et techniques qui répondent à ces défis, en nous concentrant particulièrement sur les jointures de diffusion, l'alignement des clés de partition et les moteurs de requêtes distribués comme Presto et BigQuery. De plus, nous montrons comment gérer ces problèmes dans des applications du monde réel à l'aide de Node.js et Express.


Exemple de partage dans Node.js avec Express.js

Voici comment implémenter le partitionnement dans PostgreSQL à l'aide de Node.js et Express.js.

Exemple de partage PostgreSQL

Utilisation de Citus ou partitionnement logique manuel avec Node.js :

Exemple avec partage logique

  1. Tableaux de configuration pour les fragments :
    Utilisez des tables pour les fragments (user_data sur shard1 et user_data sur shard2).

  2. Créer une API Express.js :
    Distribuez des requêtes basées sur une clé de partition (par exemple, user_id).

   const express = require('express');
   const { Pool } = require('pg');

   const poolShard1 = new Pool({ connectionString: 'postgresql://localhost/shard1' });
   const poolShard2 = new Pool({ connectionString: 'postgresql://localhost/shard2' });

   const app = express();
   app.use(express.json());

   const getShardPool = (userId) => (userId % 2 === 0 ? poolShard1 : poolShard2);

   app.post('/user', async (req, res) => {
       const { userId, data } = req.body;
       const pool = getShardPool(userId);
       try {
           await pool.query('INSERT INTO user_data (user_id, data) VALUES (, )', [userId, data]);
           res.status(200).send('User added successfully');
       } catch (err) {
           console.error(err);
           res.status(500).send('Error inserting user');
       }
   });

   app.get('/user/:userId', async (req, res) => {
       const userId = parseInt(req.params.userId, 10);
       const pool = getShardPool(userId);
       try {
           const result = await pool.query('SELECT * FROM user_data WHERE user_id = ', [userId]);
           res.status(200).json(result.rows);
       } catch (err) {
           console.error(err);
           res.status(500).send('Error retrieving user');
       }
   });

   app.listen(3000, () => console.log('Server running on port 3000'));

1. Partage dans les bases de données distribuées

Le partitionnement est le processus de partitionnement horizontal des données sur plusieurs instances de base de données, ou fragments, pour améliorer les performances, l'évolutivité et la disponibilité. Le partage est souvent nécessaire lorsqu'une seule instance de base de données ne peut pas gérer le volume de données ou de trafic.

Stratégies de partage :

  • Partage basé sur la plage : les données sont distribuées entre les fragments en fonction de la plage d'une clé, par exemple en partitionnant les commandes par date_de-commande.
  • Partage basé sur le hachage : les données sont hachées par une clé de partition (par exemple, user_id) pour répartir les données uniformément entre les partitions.
  • Partage basé sur un répertoire : un répertoire central assure le suivi de l'emplacement des données dans le système.

Cependant, lorsque des tables associées sont fragmentées sur des clés différentes, ou lorsqu'une table nécessite une jointure avec une autre table sur plusieurs fragments, les performances peuvent se dégrader en raison de la nécessité d'opérations de dispersion-gather. C'est là que la compréhension des jointures de diffusion et de l'alignement des clés de partition devient cruciale.


2. Défis liés aux jointures dans les systèmes fragmentés

Lorsque les données résident dans différentes partitions, effectuer des jointures entre ces partitions peut être complexe. Voici un aperçu des défis courants :

1. Désalignement des clés de fragment :

Dans de nombreux systèmes, les tables sont partagées sur différentes clés. Par exemple :

  • La table users peut être partagée par user_id.
  • Le tableau commandes peut être divisé par région.

Lors de l'exécution d'une jointure (par exemple,orders.user_id = users.user_id), le système doit récupérer les données de plusieurs partitions, car les enregistrements pertinents peuvent ne pas résider dans la même partition.

2. Rejointements Scatter-Gather :

Dans une jointure scatter-gather, le système doit :

  • Envoyer des requêtes à tous les fragments contenant des données pertinentes.
  • Agréger les résultats sur plusieurs fragments. Cela peut dégrader considérablement les performances, en particulier lorsque les données sont réparties sur plusieurs fragments.

3. Diffusion des jointures :

Une jointure par diffusion se produit lorsqu'une des tables jointes est suffisamment petite pour être diffusée sur toutes les partitions. Dans ce cas :

  • La petite table (par exemple, les utilisateurs) est répliquée sur tous les nœuds où réside la plus grande table fragmentée (par exemple, les commandes).
  • Chaque nœud peut ensuite joindre ses données locales aux données diffusées, évitant ainsi le besoin de communication entre fragments.

3. Utilisation de moteurs de requêtes distribués pour les données fragmentées

Les moteurs de requêtes distribués tels que Presto et BigQuery sont conçus pour gérer les données fragmentées et joindre efficacement les requêtes sur les systèmes distribués.

Presto/Trino :

Presto est un moteur de requête SQL distribué conçu pour interroger de grands ensembles de données sur des sources de données hétérogènes (par exemple, bases de données relationnelles, bases de données NoSQL, lacs de données). Presto effectue des jointures sur des sources de données distribuées et peut optimiser les requêtes en minimisant le mouvement des données entre les nœuds.

Exemple de cas d'utilisation : joindre des données partagées avec Presto

Dans un scénario où les commandes sont partagées par région et les utilisateurs sont partagées par user_id, Presto peut effectuer une jointure sur différentes partitions à l'aide de son modèle d'exécution distribué.

Requête :

   const express = require('express');
   const { Pool } = require('pg');

   const poolShard1 = new Pool({ connectionString: 'postgresql://localhost/shard1' });
   const poolShard2 = new Pool({ connectionString: 'postgresql://localhost/shard2' });

   const app = express();
   app.use(express.json());

   const getShardPool = (userId) => (userId % 2 === 0 ? poolShard1 : poolShard2);

   app.post('/user', async (req, res) => {
       const { userId, data } = req.body;
       const pool = getShardPool(userId);
       try {
           await pool.query('INSERT INTO user_data (user_id, data) VALUES (, )', [userId, data]);
           res.status(200).send('User added successfully');
       } catch (err) {
           console.error(err);
           res.status(500).send('Error inserting user');
       }
   });

   app.get('/user/:userId', async (req, res) => {
       const userId = parseInt(req.params.userId, 10);
       const pool = getShardPool(userId);
       try {
           const result = await pool.query('SELECT * FROM user_data WHERE user_id = ', [userId]);
           res.status(200).json(result.rows);
       } catch (err) {
           console.error(err);
           res.status(500).send('Error retrieving user');
       }
   });

   app.listen(3000, () => console.log('Server running on port 3000'));

Presto :

  1. Utilisez dispersion-gather pour récupérer les enregistrements d'utilisateurs pertinents.
  2. Joignez les données entre les nœuds.

Google BigQuery :

BigQuery est un entrepôt de données sans serveur entièrement géré qui excelle dans l'exécution de requêtes analytiques à grande échelle. Tandis que BigQuery élimine les détails du partitionnement, il partitionne et distribue automatiquement les données sur de nombreux nœuds pour des requêtes optimisées. Il peut gérer facilement de grands ensembles de données et est particulièrement efficace pour les requêtes analytiques où les données sont partitionnées par temps ou par d'autres dimensions.

Exemple de cas d'utilisation : Rejoindre des tables fragmentées dans BigQuery
   const express = require('express');
   const { Pool } = require('pg');

   const poolShard1 = new Pool({ connectionString: 'postgresql://localhost/shard1' });
   const poolShard2 = new Pool({ connectionString: 'postgresql://localhost/shard2' });

   const app = express();
   app.use(express.json());

   const getShardPool = (userId) => (userId % 2 === 0 ? poolShard1 : poolShard2);

   app.post('/user', async (req, res) => {
       const { userId, data } = req.body;
       const pool = getShardPool(userId);
       try {
           await pool.query('INSERT INTO user_data (user_id, data) VALUES (, )', [userId, data]);
           res.status(200).send('User added successfully');
       } catch (err) {
           console.error(err);
           res.status(500).send('Error inserting user');
       }
   });

   app.get('/user/:userId', async (req, res) => {
       const userId = parseInt(req.params.userId, 10);
       const pool = getShardPool(userId);
       try {
           const result = await pool.query('SELECT * FROM user_data WHERE user_id = ', [userId]);
           res.status(200).json(result.rows);
       } catch (err) {
           console.error(err);
           res.status(500).send('Error retrieving user');
       }
   });

   app.listen(3000, () => console.log('Server running on port 3000'));

BigQuery gère automatiquement le partitionnement et la distribution, minimisant ainsi le besoin de partitionnement manuel.


4. Gestion du désalignement des clés de fragment dans les applications Node.js

Lorsque vous traitez des données fragmentées dans les applications Node.js, des problèmes tels que des clés de fragmentation mal alignées et la nécessité de jointures dispersées surviennent souvent. Voici comment relever ces défis en utilisant Node.js et Express.

Gestion des jointures de diffusion dans Node.js

Si une jointure nécessite la diffusion d'une petite table (par exemple, les utilisateurs) sur toutes les partitions, vous pouvez implémenter la jointure dans la couche d'application en récupérant la petite table une fois et en l'utilisant pour joindre les données des tables fragmentées.

SELECT o.order_id, u.user_name
FROM orders o
JOIN users u
ON o.user_id = u.user_id;

Gestion des requêtes Scatter-Gather dans Node.js

Pour les requêtes qui impliquent des jointures par dispersion (par exemple, lorsque les clés de partition sont mal alignées), vous devrez interroger toutes les partitions et regrouper les résultats dans votre couche d'application.

SELECT o.order_id, u.user_name
FROM `project.dataset.orders` o
JOIN `project.dataset.users` u
ON o.user_id = u.user_id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-12-31';

5. Bonnes pratiques pour l'optimisation des requêtes avec des données fragmentées

Lorsque vous traitez des données fragmentées et effectuez des jointures, tenez compte des bonnes pratiques suivantes :

  1. Aligner les clés de partition : lorsque cela est possible, assurez-vous que les tables associées utilisent la même clé de partition. Cela minimise le besoin de jointures entre fragments et améliore les performances.

  2. Dénormalisation : Dans les scénarios où les jointures sont fréquentes, envisagez de dénormaliser vos données. Par exemple, vous pouvez stocker les informations utilisateur directement dans le tableau des publications, réduisant ainsi le besoin de jointure.

  3. Utiliser les jointures de diffusion pour les petites tables : si l'une des tables est suffisamment petite, diffusez-la à tous les nœuds pour éviter les requêtes de dispersion.

  4. Pre-Join Data : Pour les données fréquemment consultées, pensez à pré-joindre et à stocker les résultats dans une vue matérialisée ou un cache.

  5. Tirer parti des moteurs de requêtes distribués : pour les requêtes analytiques complexes, utilisez des systèmes tels que Presto ou BigQuery qui gèrent automatiquement les jointures distribuées et les optimisations.


6. Meilleures pratiques pour la pagination basée sur un curseur avec des données fragmentées

Dans un système distribué avec un tel partitionnement, la pagination basée sur le curseur doit être gérée avec soin, en particulier parce que les données sont réparties sur plusieurs partitions. La clé est de :

  1. Divisez les requêtes  : interrogez chaque fragment indépendamment pour obtenir des données pertinentes.
  2. Gérer la pagination en morceaux : décidez comment paginer les données de partition (soit sur les publications, soit sur les utilisateurs) et collectez des résultats pertinents.
  3. Rejoindre au niveau de l'application : récupérez les résultats de chaque fragment, joignez les données en mémoire, puis appliquez la logique du curseur pour la page suivante.

Voyons comment nous pouvons implémenter cela avec Node.js et Express, en tenant compte du fait que les données résident sur des fragments différents et nécessitent des jointures post-récupération au niveau de l'application.

Comment gérer la pagination et les jointures avec des tables fragmentées

Supposons que nous ayons :

  • Table posts partagée par user_id.
  • Table users partagée par user_id.

Nous souhaitons récupérer les publications paginées pour un utilisateur donné, mais comme les utilisateurs et les publications se trouvent sur des partitions différentes, nous devrons diviser la requête, gérer la pagination, puis effectuer la jointure au niveau de l'application.

Approche:

  1. Interroger les fragments pertinents :

    • Tout d'abord, vous devez interroger la table des publications sur les fragments pour récupérer les publications.
    • Après avoir récupéré les publications pertinentes, utilisez le user_id des publications pour interroger la table des utilisateurs (encore une fois, sur plusieurs fragments).
  2. Stratégie de pagination :

    • Pagination sur les publications : vous pouvez utiliser create_at, post_id ou un autre champ unique pour paginer le tableau des publications.
    • Pagination sur les utilisateurs : vous devrez peut-être récupérer les données utilisateur séparément ou utiliser l'ID utilisateur comme curseur pour paginer entre les utilisateurs.
  3. Rejoindre au niveau de l'application :

    • Après avoir récupéré les données des fragments concernés (pour les publications et les utilisateurs), rejoignez-les au niveau de l'application.
  4. Gestion du curseur :

    • Après avoir récupéré la première page, utilisez le dernier create_at ou post_id (des publications) comme curseur pour la requête suivante.

Exemple de mise en œuvre

1. Interroger les publications sur les fragments

Ici, nous exécuterons des requêtes sur différents fragments de publication, en filtrant par un curseur (par exemple,created_at ou post_id).

2. Interrogez les utilisateurs sur les fragments à l'aide des données de publication

Une fois que nous aurons le post_id et l'user_id pertinents de la première requête, nous récupérerons les données utilisateur des fragments concernés.

   const express = require('express');
   const { Pool } = require('pg');

   const poolShard1 = new Pool({ connectionString: 'postgresql://localhost/shard1' });
   const poolShard2 = new Pool({ connectionString: 'postgresql://localhost/shard2' });

   const app = express();
   app.use(express.json());

   const getShardPool = (userId) => (userId % 2 === 0 ? poolShard1 : poolShard2);

   app.post('/user', async (req, res) => {
       const { userId, data } = req.body;
       const pool = getShardPool(userId);
       try {
           await pool.query('INSERT INTO user_data (user_id, data) VALUES (, )', [userId, data]);
           res.status(200).send('User added successfully');
       } catch (err) {
           console.error(err);
           res.status(500).send('Error inserting user');
       }
   });

   app.get('/user/:userId', async (req, res) => {
       const userId = parseInt(req.params.userId, 10);
       const pool = getShardPool(userId);
       try {
           const result = await pool.query('SELECT * FROM user_data WHERE user_id = ', [userId]);
           res.status(200).json(result.rows);
       } catch (err) {
           console.error(err);
           res.status(500).send('Error retrieving user');
       }
   });

   app.listen(3000, () => console.log('Server running on port 3000'));

Détails clés :

  1. Pagination sur les publications : Le curseur est basé sur le champ créé_at ou sur un autre champ unique dans les publications, qui est utilisé pour paginer dans les résultats.
  2. Interroger les fragments indépendamment : étant donné que les publications et les utilisateurs sont partagés sur des clés différentes, nous interrogeons chaque fragment indépendamment, en collectant les données de tous les fragments avant d'effectuer la jointure au niveau de l'application.
  3. Gestion du curseur : Après avoir récupéré les résultats, nous utilisons le dernier créé_at (ou post_id) des publications pour générer le curseur de la page suivante.
  4. Rejoindre au niveau de l'application : après avoir récupéré les données des fragments pertinents, nous rejoignons les publications avec les données utilisateur basées sur l'ID utilisateur en mémoire.

Conclusion

La gestion des données fragmentées dans des systèmes distribués présente des défis uniques, en particulier lorsqu'il s'agit d'effectuer des jointures efficaces. Comprendre des techniques telles que les jointures de diffusion, les jointures par dispersion et l'exploitation des moteurs de requêtes distribués peuvent améliorer considérablement les performances des requêtes. De plus, dans les requêtes au niveau des applications, il est essentiel de prendre en compte l'alignement des clés de partition, la dénormalisation et les stratégies de requête optimisées. En suivant ces bonnes pratiques et en utilisant les bons outils, les développeurs peuvent garantir que leurs applications gèrent efficacement les données fragmentées et maintiennent les performances à grande échelle.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn