1.1 Qu'est-ce que le partage ?

Si vous cherchez constamment sur Google, il s'avère qu'il existe une frontière assez floue entre le soi-disant partitionnement et le soi-disant sharding. Chacun appelle ce qu'il veut, ce qu'il veut. Certaines personnes font la distinction entre le partitionnement horizontal et le sharding. D'autres disent que le sharding est un certain type de partitionnement horizontal.

Je n'ai pas trouvé une seule norme terminologique qui serait approuvée par les pères fondateurs et certifiée par l'ISO. La conviction intérieure personnelle est quelque chose comme ça : partitionner en moyenne, c'est « couper la base en morceaux » d'une manière arbitrairement prise.

  • Partitionnement vertical - par colonne. Par exemple, il y a une table géante avec quelques milliards d'enregistrements dans 60 colonnes. Au lieu de conserver une telle table géante, nous gardons au moins 60 tables géantes de 2 milliards d'enregistrements chacune - et ce n'est pas une base de colonne, mais un partitionnement vertical (comme exemple de terminologie).
  • Partitionnement horizontal - nous coupons ligne par ligne, peut-être à l'intérieur du serveur.

Le moment gênant ici est la différence subtile entre le partitionnement horizontal et le sharding. Je peux être coupé en morceaux, mais je ne peux pas vous dire avec certitude ce que c'est. On a le sentiment que le sharding et le partitionnement horizontal sont à peu près la même chose.

Le sharding est, en général, lorsqu'une grande table en termes de bases de données ou une pro-collection de documents, d'objets, si vous n'avez pas de base de données, mais un magasin de documents, est coupée exactement par des objets. C'est-à-dire que parmi 2 milliards d'objets, des pièces sont sélectionnées quelle que soit leur taille. Les objets eux-mêmes à l'intérieur de chaque objet ne sont pas découpés en morceaux, nous ne les répartissons pas en colonnes séparées, à savoir, nous les répartissons par lots à différents endroits.

Il existe de subtiles différences terminologiques. Par exemple, relativement parlant, les développeurs Postgres peuvent dire que le partitionnement horizontal se produit lorsque toutes les tables dans lesquelles la table principale est divisée se trouvent dans le même schéma, et lorsqu'elles sont sur des machines différentes, c'est déjà du sharding.

De manière générale, sans être lié à la terminologie d'une base de données spécifique et d'un système de gestion de données spécifique, on a l'impression que le sharding consiste simplement à découper ligne par ligne / document par document, et ainsi de suite - c'est tout.

J'insiste sur typique. Dans le sens où nous faisons tout cela non seulement pour découper 2 milliards de documents en 20 tables, dont chacune serait plus gérable, mais dans le but de le répartir sur de nombreux cœurs, de nombreux disques ou de nombreux serveurs physiques ou virtuels différents.

1.2 Diviser l'indivisible

Il est entendu que nous faisons cela pour que chaque fragment - chaque élément de données - soit répliqué plusieurs fois. Mais vraiment, non.

INSERT INTO docs00 
SELECT * FROM documents WHERE (id%16)=0 
... 
 
INSERT INTO docs15 
SELECT * FROM documents WHERE (id%16)=15 

En fait, si vous faites un tel découpage de données, et à partir d'une table SQL géante sur MySQL sur votre vaillant portable, vous allez générer 16 petites tables, sans dépasser un seul portable, pas un seul schéma, pas une seule base de données, etc. . et ainsi de suite. - ça y est, vous avez déjà le sharding.

Cela se traduit par ce qui suit :

  • La bande passante augmente.
  • La latence ne change pas, c'est-à-dire que chacun, pour ainsi dire, travailleur ou consommateur dans ce cas, obtient le sien. Différentes demandes sont traitées à peu près au même moment.
  • Ou les deux, et un autre, et aussi une haute disponibilité (réplication).

Pourquoi la bande passante ? Nous pouvons parfois avoir de tels volumes de données qui ne rentrent pas - on ne sait pas où, mais ils ne rentrent pas - sur 1 {kernel | disque | serveur | ...}. Il n'y a tout simplement pas assez de ressources, c'est tout. Pour travailler avec ce grand ensemble de données, vous devez le couper.

Pourquoi la latence ? Sur un cœur, l'analyse d'une table de 2 milliards de lignes est 20 fois plus lente que l'analyse de 20 tables sur 20 cœurs, en le faisant en parallèle. Les données sont traitées trop lentement sur une seule ressource.

Pourquoi la haute disponibilité ? Soit on découpe les données pour faire les deux en même temps, et en même temps plusieurs copies de chaque shard - la réplication assure une haute disponibilité.

1.3 Un exemple simple "comment le faire à la main"

Le partitionnement conditionnel peut être découpé à l'aide de la table de test test.documents pour 32 documents, et en générant 16 tables de test à partir de cette table, environ 2 documents chacun test.docs00, 01, 02, ..., 15.

INSERT INTO docs00 
SELECT * FROM documents WHERE (id%16)=0 
... 
 
INSERT INTO docs15 
SELECT * FROM documents WHERE (id%16)=15 

Pourquoi environ? Car a priori on ne sait pas comment sont répartis les id, si de 1 à 32 inclus, alors il y aura exactement 2 documents chacun, sinon non.

Nous le faisons ici pourquoi. Après avoir fait 16 tables, nous pouvons "saisir" 16 de ce dont nous avons besoin. Indépendamment de ce que nous frappons, nous pouvons paralléliser ces ressources. Par exemple, s'il n'y a pas assez d'espace disque, il serait logique de décomposer ces tables sur des disques séparés.

Tout cela, malheureusement, n'est pas gratuit. Je soupçonne que dans le cas de la norme SQL canonique (je n'ai pas relu la norme SQL depuis longtemps, peut-être qu'elle n'a pas été mise à jour depuis longtemps), il n'y a pas de syntaxe standardisée officielle pour dire à n'importe quel serveur SQL : "Cher serveur SQL, fais-moi 32 partitions et divise-les en 4 disques. Mais dans les implémentations individuelles, il existe souvent une syntaxe spécifique pour faire essentiellement la même chose. PostgreSQL a des mécanismes de partitionnement, MySQL a MariaDB, Oracle a probablement fait tout cela il y a longtemps.

Néanmoins, si nous le faisons à la main, sans support de base de données et dans le cadre de la norme, alors nous payons conditionnellement avec la complexité de l'accès aux données . Là où il y avait un simple SELECT * FROM documents WHERE id=123, maintenant 16 x SELECT * FROM docsXX. Et c'est bien si nous avons essayé d'obtenir le dossier par clé. Beaucoup plus intéressant si nous essayions d'obtenir une première gamme d'enregistrements. Maintenant (si nous, je le souligne, sommes pour ainsi dire des imbéciles et restons dans le cadre de la norme), les résultats de ces 16 SELECT * FROM devront être combinés dans l'application.

À quel changement de performance pouvez-vous vous attendre ?

  • Intuitivement - linéaire.
  • Théoriquement - sous-linéaire, car la loi d'Amdahl.
  • Pratiquement, peut-être presque linéairement, peut-être pas.

En fait, la bonne réponse est inconnue. Avec une application intelligente de la technique de sharding, vous pouvez obtenir une dégradation super-linéaire significative des performances de votre application, et même le DBA se mettra en marche avec un poker brûlant.

Voyons comment cela peut être réalisé. Il est clair que le simple fait de définir le paramètre sur PostgreSQL shards=16, puis qu'il décolle tout seul, n'est pas intéressant. Réfléchissons à la façon dont nous pouvons nous assurer que nous ralentissons le sharding de 16 fois par 32 - c'est intéressant du point de vue de la façon de ne pas le faire.

Nos tentatives d'accélération ou de ralentissement se heurteront toujours aux classiques - la bonne vieille loi Amdahl, qui dit qu'il n'y a pas de parallélisation parfaite d'une demande, il y a toujours une partie cohérente.

1.4 Loi Amdahl

Il y a toujours une pièce sérialisée.

Il y a toujours une partie de l'exécution de la requête qui est parallélisée et il y a toujours une partie qui n'est pas parallélisée. Même s'il vous semble qu'une requête parfaitement parallèle, au moins la collection de la ligne de résultat que vous allez envoyer au client à partir des lignes reçues de chaque shard est toujours là, et elle est toujours séquentielle.

Il y a toujours une partie cohérente. Il peut être minuscule, complètement invisible dans le contexte général, il peut être gigantesque et, par conséquent, affecter fortement la parallélisation, mais il existe toujours.

De plus, son influence évolue et peut s'accroître sensiblement, par exemple, si nous coupons notre table - faisons monter les enchères - de 64 fiches en 16 tables de 4 fiches, cette partie va changer. Bien sûr, à en juger par des quantités de données aussi gigantesques, nous travaillons sur un téléphone mobile et un processeur 2 MHz 86, et nous n'avons pas assez de fichiers qui peuvent être maintenus ouverts en même temps. Apparemment, avec de telles entrées, nous ouvrons un fichier à la fois.

  • C'était Total = Série + Parallèle . Où, par exemple, parallèle est tout le travail à l'intérieur de la base de données, et série envoie le résultat au client.
  • Est devenu Total2 = Série + Parallèle/N + Xsérie . Par exemple, lorsque ORDER BY global, Xserial>0.

Avec cet exemple simple, j'essaie de montrer que certains Xserial apparaissent. En plus du fait qu'il y a toujours une partie sérialisée, et du fait qu'on essaie de travailler avec des données en parallèle, il y a une partie supplémentaire pour fournir ce découpage des données. Grosso modo, nous aurons peut-être besoin de :

  • trouver ces 16 tables dans le dictionnaire interne de la base de données ;
  • Ouvrir des fichiers;
  • allouer de la mémoire ;
  • mémoire non allouée ;
  • fusionner les résultats ;
  • synchroniser entre les cœurs.

Certains effets désynchronisés apparaissent toujours. Ils peuvent être insignifiants et occuper un milliardième du temps total, mais ils sont toujours non nuls et toujours là. Avec leur aide, nous pouvons considérablement perdre des performances après le sharding.

Ceci est une image standard de la loi d'Amdahl. L'important ici est que les lignes, qui devraient idéalement être droites et croître linéairement, se heurtent à une asymptote. Mais comme le graphique d'Internet est illisible, j'ai fait, à mon avis, des tableaux plus visuels avec des chiffres.

Disons que nous avons une partie sérialisée du traitement de la demande qui ne prend que 5 % : serial = 0.05 = 1 / 20 .

Intuitivement, il semblerait qu'avec une partie sérialisée qui ne prend que 1/20 du traitement de la requête, si nous parallélisons le traitement de la requête pour 20 cœurs, cela deviendra environ 20, dans le pire des cas 18, fois plus rapide.

En fait, les mathématiques sont une chose sans cœur :

mur = 0,05 + 0,95/num_cores, accélération = 1 / (0,05 + 0,95/num_cores)

Il s'avère que si vous calculez soigneusement, avec une partie sérialisée de 5%, l'accélération sera de 10 fois (10,3), soit 51% par rapport à l'idéal théorique.

8 cœurs = 5,9 = 74 %
10 cœurs = 6,9 = 69 %
20 cœurs = 10,3 = 51 %
40 cœurs = 13,6 = 34 %
128 cœurs = 17,4 = 14 %

Après avoir utilisé 20 cœurs (20 disques, si vous voulez) pour la tâche sur laquelle on travaillait, nous n'obtiendrons même jamais théoriquement une accélération de plus de 20 fois, mais en pratique - beaucoup moins. De plus, avec une augmentation du nombre de parallèles, l'inefficacité augmente fortement.

Lorsqu'il ne reste que 1 % du travail sérialisé et que 99 % sont parallélisés, les valeurs d'accélération s'améliorent quelque peu :

8 cœurs = 7,5 = 93 %
16 cœurs = 13,9 = 87%
32 cœurs = 24,4 = 76 %
64 cœurs = 39,3 = 61 %

Pour une requête parfaitement thermonucléaire, qui prend naturellement des heures à accomplir, et le travail préparatoire et l'assemblage du résultat prennent très peu de temps (série = 0,001), on verra déjà une bonne efficacité :

8 cœurs = 7,94 = 99 %
16 cœurs = 15,76 = 99 %
32 cœurs = 31.04 = 97 %
64 cœurs = 60,20 = 94 %

S'il vous plaît noter que nous ne verrons jamais 100% . Dans les cas particulièrement bons, vous pouvez voir, par exemple, 99,999 %, mais pas exactement 100 %.