Bonnes pratiques liées à la conception de schémas

L'architecture distribuée de Spanner vous permet de concevoir votre schéma pour éviter points d'accès, lorsqu'un trop grand nombre de requêtes sont envoyées au même serveur, sature les ressources du serveur et peut provoquer des latences élevées.

Cette page décrit les bonnes pratiques à suivre pour concevoir vos schémas afin d'éviter en créant des zones cliquables. Une façon d'éviter la création de hotspots est d'ajuster la conception du schéma à Spanner de diviser et de répartir les données sur plusieurs serveurs. La distribution des données sur des serveurs facilite le fonctionnement de votre base de données Spanner. efficacement, en particulier lors d'insertions groupées de données.

Choisir une clé primaire en évitant de créer des hotspots

Comme indiqué dans la section Schéma et modèle de données, vous devez faire attention une clé primaire dans la conception du schéma pour ne pas créer accidentellement de hotspots dans votre base de données. L'une des causes de hotspots est la présence d'une colonne dont la valeur est monotone. est modifié en tant que premier élément clé. Ainsi, toutes les insertions se produisent à la fin de votre espace clé. Ce modèle n'est pas souhaitable, car Spanner utilise des plages de clés pour répartir les données entre les serveurs, ce qui signifie que toutes vos insertions dirigé vers un seul serveur qui finit par faire tout le travail.

Par exemple, supposons que vous souhaitiez conserver une colonne d'horodatage du dernier accès sur des lignes de la table UserAccessLog. La définition de table suivante utilise un une clé primaire basée sur le code temporel comme premier élément de clé. Nous vous déconseillons de le faire si le tableau constate un taux d'insertion élevé:

GoogleSQL


CREATE TABLE UserAccessLog (
LastAccess TIMESTAMP NOT NULL,
UserId     INT64 NOT NULL,
...
) PRIMARY KEY (LastAccess, UserId);

PostgreSQL


CREATE TABLE UserAccessLog (
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (LastAccess, UserId)
);

Le problème ici est que les lignes sont écrites dans cette table par ordre de dernière le code temporel d'accès, et comme les codes temporels de dernier accès ne cessent d'augmenter, elles sont toujours écrites à la fin de la table. La zone cliquable est créée, car un le serveur Spanner unique reçoit toutes les écritures, ce qui surcharge sur un seul serveur.

Le schéma suivant illustre ce piège:

Table UserAccessLog classée par horodatage avec le hotspot correspondant

La table UserAccessLog ci-dessus comprend cinq exemples de lignes de données, qui représentent cinq utilisateurs différents effectuant une action d'utilisateur quelconque à environ une milliseconde d'intervalle. Le diagramme annote également l'ordre dans lequel Spanner insère les lignes (les flèches libellées indiquent l'ordre des écrit pour chaque ligne). Comme les insertions sont classées par code temporel et que les la valeur du code temporel augmente constamment, Spanner ajoute toujours les insertions à la fin de la table et les dirige vers la même division. (Comme indiqué dans Schéma et modèle de données : est un ensemble de lignes issues d'une ou de plusieurs tables associées que Spanner stocke par ordre de clé de ligne.)

Cela pose problème, car Spanner attribue des tâches à différents serveurs dans des unités de division, de sorte que le serveur affecté à cette division particulière finit pour traiter toutes les requêtes d'insertion. À mesure que la fréquence des événements d'accès utilisateur augmente, la fréquence des requêtes d'insertion adressées au serveur correspondant augmente aussi. Le serveur peut alors devenir un point d'accès et ressemble la bordure et l'arrière-plan rouges ci-dessus. Notez que dans cette illustration simplifiée, chaque serveur gère au maximum une division. En réalité, Spanner peut attribuer à chaque serveur plusieurs divisions.

Lorsque Spanner ajoute des lignes à la table, la division augmente, atteint environ 8 Go, Spanner crée une autre comme décrit dans la section Répartition basée sur la charge. Spanner ajoute les nouvelles lignes suivantes à cette nouvelle division, et le serveur attribué à l'écran fractionné devient le nouveau hotspot potentiel.

En présence de hotspots, vous remarquerez peut-être que les insertions prennent du temps et que d'autres tâches sur le même serveur ralentissent. La modification de l'ordre de la colonne LastAccess par ordre croissant ne résout pas ce problème, car toutes les écritures sont insérées en haut de la table, et toutes les insertions sont donc envoyées à un seul serveur.

Bonne pratique de conception de schéma n° 1 : Ne choisissez pas une colonne dont la valeur augmente ou diminue de façon linéaire en tant que premier élément clé d'une table à taux d'écriture élevé.

Utiliser un identifiant unique universel (UUID)

Vous pouvez utiliser en tant que clé primaire un identifiant unique universel (UUID) défini par la RFC 4122. Nous vous recommandons d'utiliser la version 4 de l'UUID, car il utilise des valeurs aléatoires dans la séquence de bits. Nous déconseillons d'utiliser les UUID version 1 car ils stockent l'horodatage dans les bits de poids fort.

Il existe plusieurs façons de stocker l'UUID en tant que clé primaire :

  • Dans une colonne STRING(36)
  • Dans une paire de colonnes INT64
  • Dans une colonne BYTES(16)

Pour une colonne STRING(36), vous pouvez utiliser la clé GENERATE_UUID() de Spanner. (GoogleSQL ou PostgreSQL) comme valeur par défaut de la colonne. pour que Spanner génère automatiquement des valeurs UUID.

L'utilisation d'UUID présente néanmoins quelques inconvénients :

  • Ils sont plutôt volumineux et utilisent 16 octets, voire plus. Les autres options de clés primaires ne consomment pas autant d'espace de stockage.
  • Elles ne contiennent aucune information sur l'enregistrement. Par exemple, une clé primaire de SingerId et AlbumId ont une signification inhérente, contrairement à un UUID.
  • Vous perdez la localité entre des enregistrements associés, c'est pourquoi l'utilisation d'un UUID élimine les hotspots.

Inverser les bits des valeurs séquentielles

Vous devez vous assurer que les valeurs numériques (INT64 en GoogleSQL ou bigint en PostgreSQL) les clés primaires n'augmentent ou ne diminuent pas de manière séquentielle. Instance principale séquentielle peuvent entraîner un hotspotting à grande échelle. Une façon d'éviter il s'agit d'inverser les bits des valeurs séquentielles, en veillant à distribuer valeurs de clé primaire uniformément dans l'espace clé.

Spanner est compatible avec les séquences inversées sur les bits, ce qui génère des valeurs entières inversées sur les bits. Vous pouvez utiliser une séquence dans la première (ou uniquement) dans une clé primaire pour éviter les problèmes de hotspotting. Pour plus d'informations, Voir la section Séquence de bits inversée.

Permuter l'ordre des clés

Une façon de répartir les écritures de manière plus uniforme sur l'espace clé consiste à permuter l'ordre des clés, de sorte que la colonne qui contient la valeur monotone ne soit pas Premier élément clé:

GoogleSQL

CREATE TABLE UserAccessLog (
LastAccess TIMESTAMP NOT NULL,
UserId     INT64 NOT NULL,
...
) PRIMARY KEY (UserId, LastAccess);

PostgreSQL

CREATE TABLE UserAccessLog (
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (UserId, LastAccess)
);

Dans ce schéma modifié, les insertions sont désormais triées en priorité par UserId, plutôt que par horodatage de dernier accès chronologique. Ce schéma répartit les écritures entre différentes répartitions, car il est peu probable qu'un seul utilisateur génère des d'événements par seconde.

Vous trouverez ci-dessous les cinq lignes de la table UserAccessLog. Spanner commande avec UserId au lieu du code temporel d'accès:

Table UserAccessLog des utilisateurs classée par ID d'utilisateur avec un débit d'écriture équilibré

Dans ce cas, Spanner divise les données UserAccessLog en trois partitions, avec chaque fractionnement contenant environ un millier de lignes de valeurs UserId ordonnées. Il s'agit d'une estimation raisonnable de la façon dont les données utilisateur pourraient être réparties, en supposant que chaque contient environ 1 Mo de données utilisateur, avec une taille de fractionnement maximale de environ 8 Go. Même si les événements utilisateur se sont produits à 1 milliseconde d'intervalle, chaque événement est déclenché par un utilisateur différent. est beaucoup moins susceptible de créer une zone cliquable le code temporel pour la commande.

Consultez également les bonnes pratiques associées pour classer les annonces en fonction de l'horodatage clés.

Hacher la clé unique et répartir les écritures sur des segments logiques

Une autre technique courante de répartition de la charge sur plusieurs serveurs consiste à créer une colonne contenant le hachage de la clé unique réelle, puis à utiliser la colonne de hachage (ou la colonne de hachage et les colonnes de clé unique) comme clé primaire. Ce procédé permet d'éviter la création de hotspots, car les nouvelles lignes sont réparties sur l'espace clé de manière plus uniforme.

La valeur de hachage peut vous permettre de créer des segments logiques, ou partitions, dans votre base de données. Dans une base de données physiquement segmentée, les lignes sont réparties sur plusieurs serveurs de base de données. Dans une base de données segmentée logiquement, les données de la table définissent segments. Par exemple, pour répartir les écritures dans la table UserAccessLog sur N segments logiques, vous pouvez ajouter une colonne de clé ShardId en début de table :

GoogleSQL

CREATE TABLE UserAccessLog (
ShardId     INT64 NOT NULL,
LastAccess  TIMESTAMP NOT NULL,
UserId      INT64 NOT NULL,
...
) PRIMARY KEY (ShardId, LastAccess, UserId);

PostgreSQL

CREATE TABLE UserAccessLog (
ShardId bigint NOT NULL,
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (ShardId, LastAccess, UserId)
);

Pour calculer la valeur ShardId, hachez une combinaison des colonnes de clé primaire, puis calculer le modulo N du hachage. Exemple :

ShardId = hash(LastAccess and UserId) % N

La fonction de hachage et la combinaison de colonnes que vous choisissez déterminent la répartition de vos insertions dans l'espace de clés. Spanner créera ensuite des divisions optimiser les performances.

Le schéma ci-dessous montre comment l'utilisation d'un hachage pour créer trois segments logiques permet de répartir le débit d'écriture de manière plus uniforme sur les serveurs :

Table UserAccessLog triée par ID de segment avec débit d'écriture équilibré

Ici, la table UserAccessLog est classée par ShardId, cette valeur étant calculée comme une fonction de hachage des colonnes de clé. Les cinq lignes UserAccessLog sont divisées en trois segments logiques, chacun d'entre eux appartenant à une division différente. Les insertions sont réparties uniformément entre les divisions, ce qui équilibre le débit d'écriture entre les trois serveurs qui gèrent les divisions.

Spanner vous permet également de créer une fonction de hachage colonne générée.

Pour effectuer cette opération dans Google SQL, utilisez la méthode <ph type="x-smartling-placeholder"></ph> FARM_FINGERPRINT pendant l'écriture, comme illustré dans l'exemple suivant:

GoogleSQL

CREATE TABLE UserAccessLog (
ShardId INT64 NOT NULL
AS (MOD(FARM_FINGERPRINT(CAST(LastAccess AS STRING)), 2048)) STORED,
LastAccess TIMESTAMP NOT NULL,
UserId    INT64 NOT NULL,
) PRIMARY KEY (ShardId, LastAccess, UserId);

La fonction de hachage que vous choisissez détermine la façon dont vos insertions sont réparties sur l'ensemble de la plage de clés. Vous n'avez pas besoin d'un hachage cryptographique, même si le hachage cryptographique peut être un bon choix. Lorsque vous choisissez une fonction de hachage, vous devez tenir compte des facteurs suivants:

  • Évitement du point d'accès. Une fonction qui génère davantage de valeurs de hachage a tendance à réduire les hotspots.
  • Efficacité de la lecture. Moins il y a de valeurs de hachage à analyser, plus les lectures de l'ensemble des valeurs de hachage sont rapides.
  • Nombre de nœuds.

Utiliser l'ordre décroissant pour les clés basées sur l'horodatage

Si vous avez une table pour votre historique qui utilise le code temporel comme clé, envisagez d'utiliser l'ordre décroissant pour la colonne de clé si l'un des appliquer:

  • Si vous souhaitez lire l'historique le plus récent, vous utilisez un entrelacée pour l'historique. qui lit la ligne parente. dans ce cas, une colonne d'horodatage DESC permet de stocker les dernières entrées d'historique à côté de la ligne parente. Sinon, la lecture de la ligne parente et de son historique récent nécessitera une recherche intermédiaire afin d'ignorer l'historique plus ancien.
  • Vous lisez des entrées séquentielles dans l'ordre chronologique inverse et ne savez pas exactement combien d'entrées vous devez parcourir : vous pouvez par exemple exécuter une requête SQL avec une valeur LIMIT pour obtenir les N événements les plus récents, ou planifier l'annulation de la lecture une fois que vous avez lu un certain nombre de lignes. Dans ces vous devez commencer par les entrées les plus récentes et les lire dans l'ordre les entrées plus anciennes jusqu'à ce que votre condition soit remplie, ce que Spanner pour les clés d'horodatage stockées par Spanner ordre décroissant.

Ajoutez le mot clé DESC pour classer les clés d'horodatage dans l'ordre décroissant. Exemple :

GoogleSQL

CREATE TABLE UserAccessLog (
UserId     INT64 NOT NULL,
LastAccess TIMESTAMP NOT NULL,
...
) PRIMARY KEY (UserId, LastAccess DESC);

Bonne pratique de conception de schéma n° 2: Ordre décroissant ou ordre croissant dépend des requêtes des utilisateurs. Par exemple, "top" correspond à la plus récente et "top" est la plus ancienne.

Utiliser un index entrelacé sur une colonne dont la valeur augmente ou diminue de façon linéaire

Comme dans l'exemple de clé primaire précédent à éviter, il s'agit également il est déconseillé de créer des index non entrelacés sur des colonnes dont les valeurs sont augmentant ou diminuant de manière monotone, même s'il ne s'agit pas de colonnes de clé primaire.

Par exemple, imaginons que vous définissiez la table suivante, dans laquelle LastAccess correspond à une colonne de clé non primaire :

GoogleSQL

CREATE TABLE Users (
UserId     INT64 NOT NULL,
LastAccess TIMESTAMP,
...
) PRIMARY KEY (UserId);

PostgreSQL

CREATE TABLE Users (
UserId     bigint NOT NULL,
LastAccess TIMESTAMPTZ,
...
PRIMARY KEY (UserId)
);

Cela peut paraître utile de définir un index sur la colonne LastAccess pour interroger rapidement la base de données sur les accès d'utilisateur "depuis la période X", comme ceci :

GoogleSQL

CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess);

PostgreSQL

CREATE INDEX UsersByLastAccess ON Users(LastAccess)
WHERE LastAccess IS NOT NULL;

Toutefois, il en résulte le même piège que celui décrit dans les car Spanner implémente les index sous forme de tables en arrière-plan, et que la table d'index résultante utilise une colonne dont la valeur augmente de façon linéaire comme premier élément clé.

Il est toutefois possible de créer un index entrelacé comme celui-ci, car les lignes de les index entrelacés sont entrelacés dans les lignes parent correspondantes et peu probable qu'une seule ligne parent produise des milliers d'événements par seconde.

Bonne pratique de conception de schéma n° 3: Ne créez pas de schéma index non entrelacé sur une colonne à taux d'écriture élevé dont la valeur est monotone augmente ou diminue. Au lieu d'utiliser des index entrelacés, utilisez des techniques comme celles que vous utiliseriez pour la conception de la clé primaire de la table de base lors de la conception de colonnes d'index, exemple, ajoutez "shardId".

Étape suivante