Bonnes pratiques relatives à la conception de schémas

Cette page décrit les bonnes pratiques relatives à la conception de schémas Cloud Spanner qui permettent d'éviter la création de hotspots, ainsi qu'au chargement de données dans Cloud Spanner.

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 lorsque vous choisissez une clé primaire afin de ne pas créer de hotspots par inadvertance dans votre base de données. Les hotspots peuvent être engendrés par une colonne dont la valeur augmente de façon linéaire en tant que premier élément de clé, car dans ce cas, toutes les insertions se produisent à la fin de votre espace clé. Ce phénomène n'est pas souhaitable, car Cloud Spanner divise les données entre les serveurs par plages de clés, ce qui signifie que les insertions seront dirigées vers un seul serveur qui finira par faire tout le travail.

Par exemple, supposons que vous souhaitiez conserver une colonne d'horodatage de dernier accès sur les lignes de la table UserAccessLog. La définition de table suivante qui utilise une clé primaire basée sur l'horodatage comme première partie de clé est un anti-modèle si la table présente un taux d'insertion élevé :

-- ANTI-PATTERN: USING A COLUMN WHOSE VALUE MONOTONICALLY INCREASES OR
-- DECREASES AS THE FIRST KEY PART OF A HIGH WRITE RATE TABLE

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

Le problème est le suivant : les lignes vont être écrites dans cette table dans l'ordre d'horodatage de dernier accès, et comme les horodatages de dernier accès ne cessent d'augmenter, ils sont toujours écrits à la fin de la table. La création du hotspot est due au fait qu'un seul serveur Cloud Spanner va recevoir toutes les écritures, ce qui le surchargera.

Le schéma ci-dessous illustre ce problème :

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 schéma indique également l'ordre d'insertion des lignes (les flèches étiquetées indiquent l'ordre des écritures pour chaque ligne). Les insertions étant classées par horodatage et la valeur de l'horodatage ne cessant d'augmenter, les insertions sont toujours ajoutées à la fin du tableau et sont dirigées vers la même division. (Comme indiqué dans la section Schéma et modèle de données, une division est un ensemble de lignes provenant d'une ou de plusieurs tables liées qui sont stockées par ordre de clé de lignes.)

Cela pose un problème, car Cloud Spanner attribue des tâches à différents serveurs sous forme d'unités de divisions. Le serveur affecté à cette division finit par gérer 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 risque alors de se transformer en hotspot, comme indiqué par l'encadré rouge ci-dessus. Notez que dans cette illustration simplifiée, chaque serveur traite une division au maximum, mais en réalité, chaque serveur Cloud Spanner peut se voir attribuer plusieurs divisions.

Lorsque plusieurs lignes sont ajoutées à la table, la division augmente et lorsqu'elle atteint sa taille maximale (environ 4 Go), Cloud Spanner crée une autre division, comme décrit dans la section Répartition basée sur la charge. Les nouvelles lignes suivantes sont ajoutées à cette nouvelle division et le serveur qui lui est attribué 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é.

Permuter l'ordre des clés

Une manière de répartir les écritures sur l'espace clé consiste à permuter l'ordre des clés de sorte que la colonne contenant la valeur qui augmente ou diminue de façon linéaire ne constitue pas le premier élément de clé :

CREATE TABLE UserAccessLog (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP 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 divisions, car il est peu probable qu'un seul utilisateur produise des milliers d'événements par seconde.

Le diagramme ci-dessous illustre les cinq lignes de la table UserAccessLog, classées par UserId plutôt que par horodatage d'accès :

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

Ici, les données UserAccessLog sont réparties en trois divisions, chaque division contenant un millier de lignes de valeurs ordonnées UserId. Il s'agit d'une estimation raisonnable de la façon dont les données utilisateur peuvent être divisées, en supposant que chaque ligne contienne environ 1 Mo de données utilisateur et une taille de division maximale d'environ 4 Go. Même si les événements utilisateur se sont produits à environ une milliseconde d'intervalle, chaque événement a été déclenché par un utilisateur différent. Par conséquent, l'ordre des insertions est beaucoup moins susceptible de créer un hotspot que l'ordre des horodatages.

Consultez également les bonnes pratiques associées au classement des clés par horodatage.

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 bases de données. Dans une base de données segmentée de manière logique, les segments sont définis par les données de la table.) 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 :

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

Pour calculer la valeur ShardId, vous devez hacher une combinaison des colonnes de clé primaire et calculer le modulo N du hachage, 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. Cloud Spanner crée ensuite des divisions de lignes pour optimiser les performances. Il se peut que les divisions ne correspondent pas aux segments logiques.

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.

La fonction de hachage que vous choisissez déterminera la qualité de la répartition de vos insertions sur la plage de clés. Bien qu'un hachage de chiffrement ne soit pas nécessaire, il peut s'avérer utile. Lorsque vous choisissez une fonction de hachage, vous devez prendre en compte les facteurs suivants :

  • Réduction du nombre de hotspots. 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 un identifiant unique universel (UUID)

Vous pouvez utiliser en tant que clé primaire un identifiant unique universel (UUID) défini par la RFC 4122. La version 4 d'UUID est recommandée, car elle utilise des valeurs aléatoires dans la séquence de bits. La version 1 d'UUID stocke l'horodatage dans les bits d'ordre supérieur et n'est pas recommandée.

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)

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.
  • Ils n'indiquent aucune information sur l'enregistrement. Par exemple, contrairement à l'UUID, la clé primaire d'ID de chanteur et d'ID d'album revêt une signification inhérente.
  • Vous perdez les données de localité des enregistrements liés. C'est la raison pour laquelle l'utilisation d'un UUID élimine les hotspots.

Inverser les bits des valeurs séquentielles

Lorsque vous générez des clés primaires uniques numériques, les bits d'ordre supérieur des nombres qui suivent doivent être répartis de manière à peu près égale sur tout l'espace des nombres. Pour ce faire, vous pouvez générer des nombres séquentiels à l'aide de méthodes conventionnelles, puis inverser leurs bits pour obtenir les valeurs finales.

L'inversion des bits permet de conserver des valeurs uniques sur les clés primaires. Vous ne devez stocker que la valeur inversée, car vous pouvez recalculer la valeur d'origine dans votre code d'application.

Limiter la taille des lignes

La taille d'une ligne doit être inférieure à 4 Go pour des performances optimales. La taille d'une ligne inclut la ligne de premier niveau, ainsi que toutes ses lignes enfants entrelacées et lignes d'index. Cloud Spanner crée généralement une nouvelle division lorsqu'une division existante atteint 4 Go. Il ne peut former des divisions que parmi les lignes de niveau supérieur. Une ligne qui dépasse 4 Go peut avoir une incidence sur le débit d'écriture.

Concevoir des tables entrelacées pour éviter la création de hotspots

Cloud Spanner ne peut créer des divisions que parmi les lignes de niveau supérieur. Examinons cet exemple comprenant trois tables entrelacées :

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)

Cloud Spanner crée des divisions qui conservent tous les albums et tous les titres de chaque chanteur dans la même division. Si les titres d'un seul chanteur se transformaient en hotspot pour les lectures ou les écritures, Cloud Spanner ne pourrait pas répartir la table Songs sur plusieurs serveurs. Si en revanche, la table Songs est de niveau supérieur, Cloud Spanner peut créer des divisions en fonction des titres :

-- Schema hierarchy:
-- + Singers (top-level table)
--   + Albums (interleaved table, child table of Singers)
-- + Songs (top-level table)

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

Si vous disposez d'une table pour l'historique classée par horodatage, envisagez de classer les colonnes de clé par ordre décroissant si l'un des scénarios suivants s'applique à votre situation :

  • Vous utilisez une table entrelacée pour l'historique et souhaitez également lire 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 les deux cas, vous souhaiterez commencer par les entrées les plus récentes et lire séquentiellement les plus anciennes jusqu'à ce que votre condition soit remplie. Cloud Spanner s'avère plus efficace pour cette tâche lorsqu'il exploite des clés d'horodatage stockées dans l'ordre décroissant.

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

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 : Utilisez l'ordre décroissant pour les clés basées sur l'horodatage.

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

Comme pour l'anti-patron de clé primaire précédent, il est déconseillé de créer des index non entrelacés sur des colonnes dont les valeurs augmentent ou diminuent de manière linéaire, 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 :

CREATE TABLE Users (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP,
  ...
) 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 :

-- ANTI-PATTERN: CREATING A NON-INTERLEAVED INDEX ON A COLUMN WHOSE VALUE
-- MONOTONICALLY INCREASES OR DECREASES ON A HIGH WRITE RATE COLUMN

CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess)

Cette action aboutit néanmoins au même problème que celui décrit dans la bonne pratique précédente, car les index sont implémentés sous forme de tables en arrière-plan, et la table d'index qui en résulte utiliserait en tant que premier élément de clé une colonne dont la valeur augmenterait de façon linéaire.

Vous pouvez toutefois créer un index entrelacé comme celui-ci, car les lignes d'index entrelacés sont entrelacées dans les lignes parent correspondantes, et il est peu probable qu'une ligne parent unique génère des milliers d'événements par seconde.

Bonne pratique de conception de schéma n° 3 : Ne créez pas d'index non entrelacé sur une colonne à taux d'écriture élevé dont la valeur augmente ou diminue de façon linéaire.

Étape suivante