Cette étude de cas décrit le pipeline d’indexation que nous avons mis en place pour le projet Share-VDE, un environnement collaboratif de découverte qui recueille des données auprès de plusieurs institutions bibliographiques.
L’un des aspects critiques du projet, que l’on decrira dans cet article, est représenté par la quantité massive de données que le système peut gérer. Ces données sont:
- Modifiable: les éditeurs peuvent ajouter, supprimer ou mettre à jour des entités dans le jeu de données.
- Interrogeable: l’ensemble du jeu de données est accessible via un ensemble étendu d’API de recherche.
La couche de source de données comprend plusieurs stockages, chacun ayant un but spécifique. Plus précisément, bien que les données soient principalement stockées dans PostgreSQL, les services de recherche sont fournis par Apache Solr.
Par conséquent, on a dû déplacer une quantité importante de données entre le SGBD et le moteur de recherche. Ce ‘mouvement massif’ se produit principalement dans les scénarios suivants:
- Indexation à partir de zéro: au tout début, pour indexer l’ensemble du contenu du SGBD.
- Indexation delta massive: les institutions fournissent périodiquement des ensembles de données de petite/moyenne taille représentant des données mises à jour.
- Récupération après sinistre: objectif différent, même sujet que ci-dessus ; tout doit être (ré)indexé à partir de zéro.
Le projet
Share-VDE est une initiative pilotée par des bibliothèques qui rassemble les catalogues bibliographiques et les fichiers d’autorité d’une communauté de bibliothèques dans un environnement de découverte partagé basé sur les données liées.
Share-VDE a élargi son champ d’application pour inclure une communauté plus étendue d’institutions des domaines de l’art et de la musique, en construisant la famille Share.
Le projet Share Family est promu par le fournisseur de bibliothèques Casalini Libri et @Cult, la société de logiciels axée sur le développement de solutions de partage des connaissances.
La plateforme Share-VDE héberge les données des institutions membres suivantes:
- Duke University
- Library of Congress
- National Library of Finland
- National Library of Norway
- New York University
- Stanford University
- The British Library
- University of Alberta / NEOS Library Consortium
- University of Chicago
- University of Michigan Ann Arbor
- University of Pennsylvania
- Yale University
Les institutions bénéficiant du support du cycle de vie des données liées de Share-VDE et contribuant à la base de connaissances Share-VDE des données bibliothécaires sont:
- Cornell University
- Frick Art Reference Library
- Harry Ransom Center
- Harvard University
- National Library of Medicine
- Northwestern University
- Princeton University
- Texas A&M University
- University of California, Davis
- University of California, San Diego
- University of Colorado Boulder
- University of Minnesota
- University of Washington
Les institutions qui ont participé aux phases de recherche et développement sont:
L'idée : En quelques mots
L’image suivante illustre la mise en œuvre du pipeline d’indexation.
Les données sont stockées dans PostgreSQL ; elles sont extraites sous forme de fichiers CSV grâce aux directives COPY de PostgreSQL. Ensuite, elles traversent un pipeline Hadoop MapReduce qui manipule les fichiers texte, crée et envoie les documents vers une infrastructure de recherche SolrCloud.
Un examen plus approfondi
Le paragraphe succinct ci-dessus donne immédiatement une idée des éléments constitutifs de la mise en œuvre.
Cependant, vous voudrez peut-être examiner de plus près le rôle joué par chaque composant dans le pipeline : c’est précisément l’objectif des sections suivantes.
PostgreSQL
Les données sont principalement stockées dans PostgreSQL : bien que le schéma soit assez complexe, les entités, les propriétés et les éléments que nous souhaitons rechercher sont stockés dans quelques tables.
Ces tables suivent une approche flexible orientée lignes : étant un projet collaboratif où plusieurs institutions contribuent à la base de connaissances, l’ensemble complet des propriétés n’est pas précisément connu à l’avance ; des propriétés peuvent être ajoutées ou supprimées à l’exécution.
Pour cette raison, les entités (par exemple, les personnes dans l’exemple ci-dessous) ne peuvent pas être représentées à l’aide d’une table traditionnelle comme celle-ci :
TABLE PERSON
L’approche orientée table / orientée colonne présente les inconvénients suivants :
- Le nombre exact de types est inconnu à l’avance, et nous devons avoir une structure flexible qui nous permet d’ajouter/supprimer des types. La conception ci-dessus nécessiterait la création/suppression de tables, ce qui est une sorte de modification structurelle que nous souhaitons éviter dans un environnement de production.
- L’exigence de “flexibilité” mentionnée précédemment peut également s’appliquer aux propriétés : les propriétés d’un type donné peuvent changer au fil du temps. Une fois de plus, une approche orientée colonne comme celle-ci nécessiterait des modifications de tables chaque fois que nous travaillons sur une propriété donnée. Nous devons avoir la possibilité d’ajouter/supprimer/mettre à jour une définition de propriété à l’exécution.
- Même au sein du même type d’entité, les propriétés sont dispersées : une personne donnée pourrait avoir une date de naissance, une profession et une date de décès, tandis qu’une autre pourrait fournir uniquement un lieu de décès.
Voici plutôt l’approche orientée lignes que nous avons utilisée pour représenter les mêmes données:
TABLE DICTIONARY
Une table qui répertorie les propriétés que chaque entité dans le modèle de domaine peut avoir. Chaque propriété est associée à un type de données, une cardinalité et d’autres métadonnées qui qualifient la propriété.
Les propriétés sont divisées en trois catégories :
- attributs : valeurs littérales (comme du texte, des nombres, des dates, des booléens)
- relations : connexions entre des entités existantes
- liens : connexions entre des entités et des références externes
TABLE ENTITY
Pour chaque entité dans le système, il y a une ligne dans cette table. Les entités ont plusieurs métadonnées associées, telles que l’identifiant, le type (par exemple, Agent, Work), et quelques autres données administratives.
TABLE: PROPERTY
La table capture les propriétés (les instances réelles de propriétés) associées aux entités.
Veuillez noter que ce qui est décrit ci-dessus est une simplification visant à illustrer l’approche de conception du schéma. Pour vous donner une idée, en plus de la table “property”, il y a deux autres tables principales représentant des liens et des relations.
Le défi a été de savoir comment lire les données “indexables” de manière efficace. C’est beaucoup de données ; l’un des inconvénients entre l’approche orientée colonnes et l’approche orientée lignes consiste en une sorte de “multiplication des données” en termes de cardinalité.
Voyez l’exemple ci-dessus : la table “PERSON” contient quelques lignes et quatre colonnes, huit cellules au total ; la même représentation dans l’approche orientée lignes nécessite deux tables et environ 40 cellules (en réalité, plus, rappelez-vous que c’est une simplification).
Nous avons réalisé quelques prototypes :
- Un en utilisant Apache Spark.
- Un autre en utilisant Spring Batch.
- Un autre en utilisant une application multi-threading simpliste.
Le verdict était plus ou moins le même : un client JDBC qui récupère de telles données de la base de données n’est pas une solution fiable car cela prend beaucoup de temps. De plus, cela nécessite la mise en œuvre d’une logique asynchrone pour gérer les différentes vitesses entre les lecteurs et les travailleurs d’indexation.
The PostgreSQL COPY Command
La commande PostgreSQL COPY déplace les données entre les tables et les fichiers. C’est un outil intégré et il offre un débit d’exportation très élevé, en particulier si la commande SELECT encapsulée est maintenue aussi simple que possible (par exemple, sans clauses ORDER BY, GROUP BY ou DISTINCT).
Jusque-là, tout va bien. Nous n’avons pas besoin de regroupement ni de résultat distinct, mais qu’en est-il de l’ordre ? Pour construire un document, tout (propriétés, relations, liens) appartenant à une entité donnée doit être regroupé dans le fichier exporté ; sinon, il est impossible de savoir quand se termine la définition de l’entité sans parcourir l’ensemble complet, ce que nous voulons éviter.
De plus, la définition de l’entité est répartie sur plusieurs tables : cela nous oblige à réfléchir à une approche alternative pour regrouper les éléments appartenant à une entité donnée dans l’exportation. En d’autres termes, il n’y a pas une seule commande COPY pour exporter les éléments, et chaque sortie de commande a un ensemble de résultats séparé, dans un fichier distinct, fournissant un certain nombre de propriétés pour une entité donnée.
Illustrons rapidement le problème. Voici les commandes d’exportation :
COPY
(SELECT ... FROM PROPERTY ...)
TO STDOUT (DELIMITER u&'\001E');
COPY
(SELECT ... FROM RELATIONSHIPS ...)
TO STDOUT (DELIMITER u&'\001E');
COPY
(SELECT ... FROM LINKS ...)
TO STDOUT (DELIMITER u&'\001E');
Les commandes écrivent leur sortie dans la sortie standard. La commande COPY permet d’indiquer directement un fichier comme sortie, mais dans ce cas, il est censé s’agir d’un fichier local (c’est-à-dire que le fichier est créé sur la machine de la base de données). C’est un problème, surtout si la machine de la base de données n’est pas sur site et ne fournit pas d’accès SSH/SFTP. Il est préférable d’appeler la commande COPY et d’obtenir sa sortie standard pour créer un fichier quelque part (dans AWS S3, dans notre cas).
Autre chose que vous pourriez vous demander : pourquoi y a-t-il plusieurs commandes COPY au lieu d’une seule utilisant une UNION? En faisant ainsi, nous pouvons les exécuter en parallèle.
Comme mentionné, la sortie est un ensemble de fichiers CSV (19, au moment de l’écriture) ; une entité donnée aura potentiellement une définition partielle dans chacun d’eux.
Comment les traiter efficacement pour créer les documents envoyés à Solr ? Considérez que chaque fichier, dans un environnement de production, atteint une taille importante (entre 150 Go et 200 Go en fonction du contenu).
Amazon Elastic MapReduce (EMR)
Les données d’entrée de notre processus d’indexation sont parfaitement adaptées à un travail Hadoop MapReduce:
- un petit nombre de gros fichiers texte
- les fichiers texte sont structurés (CSV)
- chaque ligne représente une propriété d’entité (par exemple, un attribut, une relation, un lien)
- le premier champ de chaque ligne est l’identifiant de l’entité
- les lignes ne sont pas triées par l’identifiant de l’entité
- un identifiant d’entité donné (c’est-à-dire des lignes appartenant à une entité donnée) pourrait apparaître dans de nombreux fichiers
Voici un exemple des données d’entrées que nous avons:
> more attributes.dump
1,Person,257,Claire
2,Person,327,Carnby
1,Person,327,Redfield
1,Person,128,1927
2,Person,257,Edward
2,Person,128,1992
2,Person,201,Detective
...
> more relationships.dump
1,828,377,484,2292
1,223,332,233,1290
2,828,112,992,9294
2,192,117,433,1027
1,111,212,243,998
> more links.dump
1,abstract,837,https://example.org/02383.pdf
1,record,992,https://somewhere.org/records#88823
1,toc,123,https://oa.net/docs/8283.pdf
2,cover,888,https://pics.org/xyz/8348343.png
...
Nous exploitons les capacités de map, shuffle et sort d’une tâche MapReduce qui met en œuvre
- un mappeur qui effectue une manipulation minimale
- un réducteur qui crée, regroupe et indexe des documents
- Au lieu de compter sur un cluster Hadoop sur site, nous avons utilisé le service géré Amazon Elastic MapReduce (EMR) pour nous concentrer directement sur la logique du système au lieu de traiter les tâches liées à l’infrastructure.
Le mappeur analyse chaque ligne et la divise en une clé (l’identifiant de l’entité) et une valeur (la partie restante de la ligne).
Par exemple, les lignes:
1,Person,257,Claire
2,Person,327,Carnby
1,Person,327,Redfield
1,Person,128,1927
2,Person,257,Edward
2,Person,128,1992
2,Person,201,Detective
...
1,828,377,484,2292
1,223,332,233,1290
2,828,112,992,9294
2,192,117,433,1027
1,111,212,243,998
...
1,jlis,837,https://example.org/02383.pdf
1,record,992,https://somewhere.org/records#88823
1,toc,123,https://oa.net/docs/8283.pdf
2,cover,888,https://pics.org/xyz/8348343.png
...
deviennent
1 => Person,257,Claire
2 => Person,327,Carnby
1 => Person,327,Redfield
1 => Person,128,1927
2 => Person,257,Edward
2 => Person,128,1992
2 => Person,201,Detective
...
1 => 828,377,484,2292
1 => 223,332,233,1290
2 => 828,112,992,9294
2 => 192,117,433,1027
1 => 111,212,243,998
...
1 => jlis,837,https://example.org/02383.pdf
1 => record,992,https://somewhere.org/records#88823
1 => toc,123,https://oa.net/docs/8283.pdf
2 => cover,888,https://pics.org/xyz/8348343.png
...
La phase suivante est appelée shuffle : les données sont transférées des mappeurs aux réducteurs.
Avant le démarrage du réducteur, toutes les paires clé-valeur intermédiaires générées par le mappeur sont triées et regroupées par clé, et c’est exactement ce dont nous avons besoin pour construire nos documents.
En suivant l’exemple ci-dessus, un réducteur sera appelé deux fois avec les entrées suivantes:
1 => ["Person,257,Claire",
"Person,327,Redfield",
"Person,128,1927",
"828,377,484,2292",
"223,332,233,1290",
"111,212,243,998",
"jlis,837,https://example.org/02383.pdf",
"record,992,https://somewhere.org/records#88823",
"toc,123,https://oa.net/docs/8283.pdf"]
et
2 => ["Person,327,Carnby",
"Person,257,Edward",
"Person,128,1992",
"Person,201,Detective",
"828,112,992,9294",
"192,117,433,1027",
"cover,888,https://pics.org/xyz/8348343.png"
Les données ci-dessus sont tout ce dont nous avons besoin pour construire les documents destinés à Solr.
Voici une image qui résume le processus d’indexation:
Apache Solr
La dernière pièce du puzzle est Apache Solr, la plateforme de recherche d’entreprise populaire et open source construite sur Apache Lucene.
Du point de vue de l’indexation, le principal défi du cluster Solr est toujours lié aux données, à la taille initiale et au taux de croissance attendu. Solr nécessite la création de l’index distribué avec un nombre prédéfini de tranches (shards), donc à un moment donné, avec une certaine quantité de données et un taux de croissance/incrémentation donné, nous devrions toujours nous assurer que
- le cluster est bien équilibré: chaque tranche devrait gérer une quantité raisonnable de données ; “raisonnable” ici se réfère aux ressources possédées par le nœud d’hébergement
- les ressources matérielles (par exemple, RAM, CPU, disque) sont correctement allouées en fonction des besoins en termes de débit d’indexation et de requête
Dans Share-VDE, on s’attend à ce que les données croissent à grande échelle : nous avons commencé avec quelques millions d’entités (15-20), et nous savons que le système de production contiendra plus ou moins deux milliards de documents.
Estimer à l’avance un cluster qui devrait contenir une telle quantité de données est assez complexe ; la sous/surestimation est un risque élevé qui pourrait entraîner des conséquences importantes en termes financiers.
Nous avons choisi une approche incrémentielle :
- Planifier les mises à jour cumulatives en termes de données entrantes des institutions.
- Commencer avec X comme taille initiale des données, Y comme taux de croissance attendu jusqu’à ce que le cluster atteigne Z.
- Estimer et créer un cluster Solr pour contenir le scénario de taille X-Z.
- Lorsque le cluster atteint Z, il devient le nouveau X (nouvelle taille initiale), la boucle démarre à un niveau plus élevé, et nous réindexons tout dans le nouveau cluster plus grand.
Nous avons constaté deux avantages significatifs de l’utilisation de l’approche ci-dessus :
- Chaque estimation de cluster est très liée aux données qu’il contient.
- Les itérations d’estimation sont très courtes.
Cela nous permet de comprendre, estimer avec précision et adapter la limite supérieure de chaque instance de cluster, dès que la taille des données augmente.
Éviter les problèmes
Dans cette section, nous résumons une série de leçons apprises du projet où nous avons mis en œuvre tout ce qui a été décrit dans cet article.
Java 8
Java 8 est la machine virtuelle Java (JVM) prise en charge pour les instances de cluster créées à l’aide de la version 5.0.0 ou ultérieure d’Amazon EMR.
Bien que Java 8 soit un peu ancien à l’heure actuelle, cela ne pose aucun problème dans 99 % des cas.
La seule chose à laquelle vous devez faire attention, c’est d’en être conscient à l’avance, afin d’éviter (comme cela nous est arrivé) une refonte importante parce que votre code utilise des fonctionnalités avancées introduites plus tard.
Traitement par lots
L’unité d’indexation atomique dans Apache Solr est appelée Document (en réalité, une instance SolrInputDocument). Peu importe la manière dont les documents sont reçus, une fois qu’ils arrivent dans Solr, ils sont indexés un par un.
Les clients peuvent améliorer le débit global d’indexation en regroupant les documents au lieu de les envoyer séparément dès qu’ils sont créés.
Il existe plusieurs classes de clients d’indexation disponibles dans Solrj, le client Java officiel de Solr, mais à part ConcurrentUpdateSolrClient, qui accumule automatiquement les documents et crée des lots, les autres sous-classes de SolrClient nécessitent que l’appelant s’occupe de ce travail de regroupement ; CloudSolrClient, le client que nous utilisons pour cibler un SolrCloud, ne fait pas exception à cette règle ; dans un scénario de mise à jour massive, il est toujours préférable de créer un lot de documents, comme dans l’exemple suivant:
var batch = new ArrayList();
...
batch.add(doc1);
batch.add(doc2);
...
batch.add(docn);
solrClient.add(batch);
Une chose à retenir: CloudSolrClient divise votre lot en petits sous-groupes, chacun ciblant la shard leader correspondante. C’est important pour définir la taille du lot, qui devrait prendre en compte la taille du cluster.
Isoler les échecs d'indexation
Les documents sont toujours indexés un par un, indépendamment de s’ils sont envoyés par lots ou non.
Lorsque des lots de documents sont envoyés à Apache Solr pour l’indexation, par défaut, le processus s’arrête au premier échec, sautant ainsi les documents restants. Ce comportement est contre-intuitif car le résultat de l’opération est imprévisible du point de vue du client.
Dans un monde idéal, il ne devrait y avoir aucun échec d’indexation, mais en réalité, des erreurs se produisent pour de nombreuses raisons qui ne sont pas toujours directement sous notre contrôle.
Prenons un exemple. Nous avons la liste de documents suivante:
[ d1, d2, d3, d4, d5]
Le sous-ensemble réel de documents qui sont indexés change en fonction de l’endroit où se produit le premier échec : s’il se produit au niveau de d1, aucun document ne sera indexé ; s’il se produit au niveau de d2, seul d1 sera indexé, et ainsi de suite.
Étant donné que les documents sont des unités isolées et disjointes, ce dont nous avions besoin pour notre cas d’utilisation était un comportement plus “indulgent” où :
- tous les documents en échec sont consignés (pas seulement le premier)
- le lot est traité intégralement, sans s’arrêter au premier échec
Apache Solr vous permet de personnaliser le pipeline d’indexation à l’aide de composants appelés UpdateRequestProcessors. Heureusement, le processeur qui injecte le comportement dont nous avons besoin fait partie de la distribution Solr ; il s’appelle TolerantUpdateRequestProcessor ; nous avons juste besoin de le configurer :
Que se passe-t-il en cas d’un ou de plusieurs échecs ? La réponse sera toujours OK, mais elle contiendra une section qui signale les échecs. Pour chaque document en échec, il y a une entrée dans la réponse qui comprend :
- la valeur de la clé unique
- le type d’opération (par exemple ADD, DELETE)
- le message d’erreur
Cette interaction permet de collecter chaque échec sans que l’ensemble du lot échoue. Chaque échec est ensuite stocké dans une table de base de données et, en tant que solution de secours en cas d’exceptions SQL, dans un fichier journal.
Évitez d'envoyer des données inutiles
Je comprends qu’à première vue, ce conseil puisse sembler un peu trivial.
Comme décrit ci-dessus, la manière dont nous avons capturé le modèle de données dans la base de données est générique. C’est un choix typique lorsqu’on travaille avec plusieurs domaines ou lorsque les entités ne sont pas précisément connues à l’avance en termes de types, d’attributs et de relations.
Vous trouverez probablement une approche similaire dans les systèmes de gestion de contenu (par exemple, Alfresco, Nuxeo, Drupal, Magento).
L’inconvénient de cette approche est que vous n’avez pas de types statiques ou de définitions statiques de types : les choses changent en fonction du dictionnaire : quelles propriétés ont été définies à un moment donné ?
Des données : quelles propriétés ont concrètement une valeur dans l’ensemble de données ?
Après la première ingestion dans notre environnement de préproduction, nous avons remarqué beaucoup de champs qui étaient inutiles d’un point de vue de la recherche, des champs qui, en d’autres termes, n’encapsulent aucun signal de pertinence.
Nous avons donc exclu ces champs de l’indexation, en utilisant un mélange d’approches:
Database
Dans le cadre de chaque définition d’attribut de propriété, dans notre table “dictionnaire”, nous avons ajouté une colonne “indexable”. Par conséquent, les commandes COPY utilisées pour exporter les données tiennent compte de cette colonne, filtrant ainsi les éléments que nous ne voulons pas indexer.
Indexing Client
Les champs que nous indexons dans Solr ont un nom construit dynamiquement, à partir du nom de la propriété dans le dictionnaire de la base de données. Sans entrer dans les détails de la mise en œuvre, une propriété (exemple) appelée “birthPlace” dans le dictionnaire devient dans Solr un champ dont le nom est quelque chose comme ceci : dil_eng_birthPlace.
De plus, la cardinalité entre une propriété de dictionnaire et les champs Solr est généralement de 1 à plusieurs. Cela est dû au fait que ce qui est logiquement défini comme une seule propriété dans le dictionnaire (par exemple, birthPlace) génère plusieurs champs dans Solr, chacun d’eux étant associé à un objectif (par exemple, recherche, facettage, tri).
Dans le client d’indexation, le mécanisme qui crée et remplit les champs Solr à partir d’une propriété est générique. Ainsi, nous étions souvent dans une situation où le champ lui-même était marqué comme “indexable”, mais nous n’étions pas intéressés par des fonctionnalités telles que le facettage ou le tri. Dans ces cas, nous ne voulons pas ajouter de champs inutiles aux documents envoyés à Solr.
Le client d’indexation a une liste configurable de champs qui ne sont pas ajoutés aux documents construits.
Apache Solr
Les deux derniers endroits où nous avons défini les exclusions sont tous deux définis dans Apache Solr. Le premier est encore un UpdateRequestProcessor, et du point de vue fonctionnel, il suit la même approche que celle que nous utilisons déjà dans le client d’indexation:
La différence est que les champs sont supprimés une fois qu’ils arrivent dans Solr. Pour cette raison, il est fort probable que nous supprimerons ce composant en faveur d’ajouts côté client (évitant ainsi d’envoyer des données inutiles à Solr).
Enfin, dans le schéma Solr, nous avons un champ dynamique qui agit comme une dernière chance:
Avec ou sans Solr?
La création d’une infrastructure SolrCloud n’est pas une chose triviale, alors avant d’expérimenter avec un vrai cluster, nous avons ajouté quelques paramètres dans notre travail MapReduce pour émuler l’appel bloquant à Solr :
- dry.run : un indicateur qui désactive l’interaction avec Solr. Le travail MapReduce construit et collecte des documents, puis au lieu d’envoyer des lots, il ne fait tout simplement rien.
- dry.run.io.wait : le nombre de millisecondes utilisé pour émuler l’appel à Solr. Le thread appelant s’arrête pendant cette période.
Configurations spécifiques à Hadoop
Étonnamment, nous n’avons pas tellement modifié la configuration de Hadoop ; cela signifie probablement que les valeurs par défaut fonctionnent assez bien, du moins dans les benchmarks que nous avons effectués jusqu’à présent.
Les paramètres que nous avons modifiés sont les suivants :
- dfs.blocksize : dès que les données augmentent (3 Go, 30 Go, 300 Go), nous avons constaté un léger avantage à augmenter la taille des blocs de 128 à 512 Mo.
- mapreduce.reduce.merge.inmem.threshold (-1), mapreduce.task.io.sort.mb (300) : ces changements ont réduit le nombre total d’enregistrements transférés en mémoire, initialement signalé comme étant trop élevé (3 fois le nombre d’enregistrements de sortie du map).
Comme mentionné, même en utilisant les valeurs par défaut sans aucun changement, les résultats ont été légèrement améliorés, pas tellement différents de ce que vous verrez dans la section suivante.
Benchmarks
Scénario n°1 : petits documents
Voici les informations sur l’environnement que nous avons mis en place pour le premier scénario.
deux blocs de test, le premier avec 20 millions, le second avec 120 millions
Taille moyenne du document dans l’index : 4 Ko dans le premier bloc, 7 à 9 Ko dans le second bloc
Nombre moyen de champs par document : 24 dans le premier bloc, 45 dans le second bloc
Cluster Amazon EMR
- Master : 1 m4.large (2 vCore, 8 Go de mémoire), EBS : 64 Go
- Cœurs : 10 m4.large (identique au-dessus)
Nombre de réducteurs : 10
- Apache Solr:
5 nœuds EC2 en trois itérations progressives
- r5a.large (2 vCPUs, RAM 16 GB)
- m5a.xlarge (4 vCPUs, RAM 16 GB)
- c5a.2xlarge (8 vCPUs, RAM 16 GB)
- shards: 5
facteur de réplication : 2
Avant de cibler un vrai cluster Solr, nous avons effectué des expériences en utilisant les paramètres dry.run et dry.run.iowait décrits ci-dessus. L’image suivante illustre les résultats :
Nous avons effectué l’indexation réelle en ciblant le SolrCloud décrit ci-dessus en utilisant plusieurs configurations de nœuds : 2, 4 et 8 CPU (par nœud).
La première configuration, avec 2 CPU, n’était pas suffisante : outre la durée, qui était très élevée, le comportement du cluster était instable et peu fiable ; il ne pouvait pas gérer la charge d’entrée provenant des dix réducteurs.
En augmentant le nombre de CPU par nœud de 2 à 4, le processus s’est amélioré : quelques erreurs et une tendance qui suivait le graphique ci-dessus. Le problème était le rendement global : la durée du pipeline d’indexation était entre 46 et 49 minutes, indiquant que la communication avec Solr prenait environ 70 à 75 millisecondes par appel.
Nous avons augmenté à nouveau le nombre de CPU, passant de 4 à 8, et enfin, nous avons obtenu un résultat intéressant : 20 millions de documents en 8 minutes sans aucune erreur. Très important, nous avons également obtenu le même taux en utilisant le deuxième jeu de données de test (120 millions).
En utilisant le graphique ci-dessus, nous pouvons estimer une moyenne de 11 millisecondes par appel Solr, ce que nous considérons comme un bon chiffre.
Scénario n.2: documents réels
Comme décrit dans la première partie de cet article, les fichiers de sauvegarde exportés depuis PostgreSQL sont au nombre de 19. Par conséquent, la définition réelle (moyenne) des documents est un peu plus grande que dans le scénario précédent.
Ce deuxième test utilise tous ces fichiers. Il est important de comprendre que les documents :
- ont la même cardinalité,
- ont un nombre plus important de champs/valeurs.
L’infrastructure de recherche cible est assez similaire au test précédent, avec :
- 5 nœuds EC2 répartis sur trois itérations progressives,
- m5a.xlarge (4 vCPUs, RAM 16 Go),
- c5a.2xlarge (8 vCPUs, RAM 16 Go),
- c5a.4xlarge (16 vCPUs, RAM 32 Go),
- Shards: 5
- facteur de réplication : 2
Nous avons dû abandonner les nœuds r5a.large car ils n’étaient pas suffisants, même pour un scénario minimal ; à la place, nous sommes passés de c5a.2xlarge à c5a.4xlarge dans la dernière étape, ce qui nous a donné des résultats très satisfaisants.
Voici le diagramme qui compare le débit entre les petits documents (test précédent) et les documents réels (ce test).
la ligne rouge est déplacée vers le haut principalement en raison de la taille des données d’entrée : les mappers mettent 2 minutes pour traiter 3 Go de données et 10 minutes pour 30 Go. Après avoir doublé la taille des blocs (passant de 128 Mo à 256 Mo), 30 Go ont pris environ 8 minutes. C’est très bon : un signe que nos mappers s’échelonnent très bien.
Les reducers sont un sujet différent : en raison de l’appel bloquant à Solr I/O, ils représentent un goulot d’étranglement. Nous avons dû augmenter le nombre de CPU par nœud de 8 à 16 (32 Go de RAM), ce qui représente certainement beaucoup en termes de ressources et de coûts.
Conclusions
Le projet Share-VDE est toujours en cours, de même que les itérations d’indexation. Pour cette raison, nous continuerons de mettre à jour le paragraphe “Benchmarks” dès que de nouvelles données seront collectées, et la section “Évitez des problèmes” pour chaque conseil ou astuce qui pourrait apporter même un avantage mineur.
En résumé, les résultats ci-dessus montrent que le processus/approche fonctionne, s’échelonne et peut offrir des débits satisfaisants. Cependant, comme on pouvait s’y attendre :
Comme mentionné, nous continuerons de mettre à jour cet article avec de nouvelles données ; en attendant, tout retour d’information ou question est chaleureusement accueilli.