Serveur d'impression

Transfert de Big Data à la vitesse de l'éclair – Bien choisir son serveur d impression

Le 2 juin 2020 - 29 minutes de lecture

Points clés à retenir

  • Arrow propose des transferts de données sans copie pour les applications d'analyse
  • La flèche permet le traitement de données en mémoire et en colonnes
  • Arrow est un échange de données interopérable et multiplateforme
  • La flèche est l'épine dorsale des systèmes de Big Data

De par sa nature même, le Big Data est trop volumineux pour tenir sur une seule machine. Les jeux de données doivent être partitionnés sur plusieurs machines. Chaque partition est affectée à une machine principale, avec des affectations de sauvegarde facultatives. Par conséquent, chaque machine contient plusieurs partitions. La plupart des frameworks Big Data utilisent une stratégie aléatoire pour attribuer des partitions aux machines. Si chaque travail de calcul utilise une partition, cette stratégie se traduit par une bonne répartition de la charge de calcul sur un cluster. Cependant, si un travail a besoin de plusieurs partitions, il y a de fortes chances qu'il doive récupérer des partitions à partir d'autres machines. Le transfert de données est toujours une pénalité de performance.

Apache Arrow propose un format de données en mémoire multilingue et multiplateforme pour les données. Il élimine le besoin de sérialisation car les données sont représentées par les mêmes octets sur chaque plate-forme et langage de programmation. Ce format commun permet un transfert de données sans copie dans les systèmes de Big Data, afin de minimiser l'impact sur les performances du transfert de données.

Le but de cet article est de présenter Apache Arrow et vous familiariser avec les concepts de base de la Bibliothèque Java Apache Arrow. Le code source accompagnant cet article peut être trouvé ici.

En règle générale, un transfert de données consiste en:

  • sérialisation données dans un format
  • Envoi en cours les données sérialisées sur une connexion réseau
  • désérialisation les données côté réception

Pensez par exemple à la communication entre frontend et backend dans une application web. Généralement, le format JSON (JavaScript Object Notation) est utilisé pour sérialiser les données. Pour de petites quantités de données, cela convient parfaitement. Les frais généraux de sérialisation et de désérialisation sont négligeables, et JSON est lisible par l'homme, ce qui simplifie le débogage. Cependant, lorsque les volumes de données augmentent, le coût de sérialisation peut devenir le facteur de performance prédominant. Sans soins appropriés, les systèmes peuvent finir par passer la plupart de leur temps à sérialiser des données. De toute évidence, il y a des choses plus utiles à faire avec nos cycles CPU.

Dans ce processus, il y a un facteur que nous contrôlons dans le logiciel: la (dé) sérialisation. Inutile de dire qu'il existe une multitude de cadres de sérialisation. Pensez à ProtoBuf, Thrift, MessagePack et bien d'autres. Beaucoup d'entre eux ont pour objectif principal de minimiser les coûts de sérialisation.

Malgré leurs efforts pour minimiser la sérialisation, il y a inévitablement encore une étape de (dé) sérialisation. Les objets sur lesquels votre code agit ne sont pas les octets envoyés sur le réseau. Les octets qui sont reçus sur le fil, ne sont pas les objets que le code de l'autre côté craque. À la fin, la sérialisation la plus rapide n'est pas une sérialisation.

Apache Arrow est-il pour moi?

Conceptuellement, Apache Arrow est conçu comme une épine dorsale pour les systèmes Big Data, par exemple, Ballista ou Dremio, ou pour Intégrations de systèmes Big Data. Si vos cas d'utilisation ne sont pas dans le domaine des systèmes Big Data, alors la surcharge d'Apache Arrow ne vaut pas la peine. Vous êtes probablement mieux avec un cadre de sérialisation qui a une large adoption dans l'industrie, comme ProtoBuf, FlatBuffers, Thrift, MessagePack ou autres.

Le codage avec Apache Arrow est très différent du codage avec de vieux objets Java simples, en ce sens qu'il n'y a pas d'objets Java. Le code fonctionne sur les tampons tout le long. Les bibliothèques d'utilitaires existantes, par exemple Apache Commons, Guava, etc., ne sont plus utilisables. Vous devrez peut-être réimplémenter certains algorithmes pour travailler avec des tampons d'octets. Et enfin et surtout, vous devez toujours penser en termes de colonnes plutôt qu'en termes d'objets.

Pour créer un système au-dessus d'Apache Arrow, vous devez lire, écrire, respirer et transpirer les tampons d'Arrow. Si vous construisez un système qui fonctionne sur collections d'objets de données (c'est-à-dire, une sorte de base de données), veulent calculer des choses qui sont adapté aux colonnes, et envisagez de l'exécuter dans un grappe, alors Arrow vaut vraiment l'investissement.

L'intégration avec Parquet (discutée plus loin) rend la persistance relativement facile. L'aspect multiplateforme et multilingue prend en charge les architectures de microservices polyglottes et permet une intégration facile avec le paysage Big Data existant. Le cadre RPC intégré appelé Arrow Flight facilite le partage / service des ensembles de données d'une manière standardisée et efficace.

Transfert de données sans copie

Pourquoi avons-nous besoin de la sérialisation en premier lieu? Dans une application Java, vous travaillez généralement avec des objets et des valeurs primitives. Ces objets sont en quelque sorte mappés en octets dans la mémoire RAM de votre ordinateur. Le JDK comprend comment les objets sont mappés en octets sur votre ordinateur. Mais ce mappage peut être différent sur une autre machine. Pensez par exemple à l'ordre des octets (endianness alias). De plus, tous les langages de programmation n'ont pas le même ensemble de types primitifs ou même stockent des types similaires de la même manière.

Sérialisation convertit la mémoire utilisée par les objets en un format commun. Le format a un spécificationet pour chaque langage de programmation et plate-forme, une bibliothèque est fournie pour convertir les objets en forme sérialisée et inversement. En d'autres termes, la sérialisation consiste à partager des données, sans perturber les manières idiosyncratiques de chaque langage de programmation et plate-forme. La sérialisation atténue toutes les différences de plate-forme et de langage de programmation, permettant à chaque programmeur de travailler comme il le souhaite. Tout comme les traducteurs aplanissent les barrières linguistiques entre les personnes parlant des langues différentes.

La sérialisation est une chose très utile dans la plupart des circonstances. Cependant, lorsque nous transférons de nombreuses données, cela deviendra un gros goulot d'étranglement. Par conséquent, pouvons-nous éliminer le processus de sérialisation dans ces cas? C’est en fait l’objectif de frameworks de sérialisation zéro copie, comme Apache Arrow et FlatBuffers. Vous pourriez penser que cela fonctionne sur les données sérialisées elles-mêmes au lieu de travailler sur des objets, afin d'éviter l'étape de sérialisation. Zéro copie fait référence ici au fait que les octets sur lesquels votre application fonctionne peuvent être transférés sur le câble sans aucune modification. De même, du côté réception, l'application peut commencer à travailler sur les octets tels quels, sans étape de désérialisation.

Le gros avantage ici est que les données peuvent être transférées telles quelles d'un environnement à un autre sans traduction car les données sont comprises telles quelles des deux côtés de la connexion.

L'inconvénient majeur est la perte d'idiosyncrasies dans la programmation. Toutes les opérations sont effectuées sur des tampons d'octets. Il n'y a pas d'entier, il y a une séquence d'octets. Il n'y a pas de tableau, il y a une séquence d'octets. Il n'y a pas d'objet, il y a une collection de séquences d'octets. Naturellement, vous pouvez toujours convertir les données au format commun en entiers, tableaux et objets. Mais, alors vous feriez la désérialisation, et cela irait à l'encontre du but de la copie nulle. Une fois transféré aux objets Java, ce n'est encore que Java qui peut travailler avec les données.

Comment cela fonctionne-t-il dans la pratique? Voyons rapidement deux cadres de sérialisation sans copie: Apache Arrow et FlatBuffers de Google. Bien que les deux soient des frameworks sans copie, ce sont des versions différentes servant des cas d'utilisation différents.

FlatBuffers a été initialement développé pour prendre en charge les jeux mobiles. L'accent est mis sur la transmission rapide des données du serveur au client, avec un minimum de frais généraux. Vous pouvez envoyer un seul objet ou une collection d'objets. Les données sont stockées dans (sur le tas) ByteBuffers, formatées dans la disposition de données commune FlatBuffers. Le compilateur FlatBuffers générera du code, basé sur la spécification des données, qui simplifie votre interaction avec les ByteBuffers. Vous pouvez travailler avec les données comme s'il s'agissait d'un tableau, d'un objet ou d'une primitive. Dans les coulisses, chaque méthode d'accesseur récupère les octets correspondants et traduit les octets en constructions compréhensibles pour la JVM et votre code. Si vous avez besoin, pour une raison quelconque, d'accéder aux octets, vous pouvez toujours.

Arrow se distingue des FlatBuffers par la façon dont ils présentent les listes / tableaux / tableaux en mémoire. Alors que FlatBuffers utilise un orienté ligne format de ses tableaux, Arrow utilise un format en colonnes pour stocker les données tabulaires. Et cela fait toute la différence pour les requêtes analytiques (OLAP) sur des ensembles de données volumineux.

Arrow est destiné aux systèmes de Big Data dans lesquels vous ne transférez généralement pas d'objets uniques, mais plutôt de grandes collections d'objets. FlatBuffers, d'autre part, est commercialisé (et utilisé) comme un cadre de sérialisation. En d'autres termes, le code de votre application fonctionne sur les objets et les primitives Java et ne transforme les données qu'en disposition mémoire de FlatBuffers lors de l'envoi de données. Si le côté récepteur est en lecture seule, ils n'ont pas à désérialiser les données en objets Java, les données peuvent être lues directement à partir des ByteBuffers de FlatBuffers.

Dans un grand ensemble de données, le nombre de lignes peut généralement varier de milliers à des milliards de lignes. Un tel ensemble de données peut comprendre de quelques à plusieurs milliers de colonnes.

Une requête analytique typique sur un tel ensemble de données fait référence à une poignée de colonnes. Imaginez par exemple un ensemble de données de transactions de commerce électronique. Vous pouvez imaginer qu'un directeur des ventes souhaite un aperçu des ventes, d'une région spécifique, regroupées par catégorie d'article. Il ne veut pas voir chaque vente individuelle. Le prix de vente moyen est suffisant. Une telle requête peut être répondue en trois étapes:

  • traversant toutes les valeurs dans la colonne de région, en gardant une trace de tous les ID de ligne / objet des ventes dans la région demandée
  • regrouper les identifiants filtrés en fonction des valeurs correspondantes dans la colonne de catégorie d'élément
  • calcul des agrégations pour chaque groupe

Essentiellement, un processeur de requêtes n'a besoin que d'une colonne en mémoire à un moment donné. En stockant une collection dans un format en colonnes, nous pouvons accéder à toutes les valeurs d'un seul champ / colonne séparément. Dans des formats bien conçus, cela se fait de manière à ce que la disposition soit optimisée pour les instructions SIMD des CPU. Pour de telles charges de travail analytiques, la disposition en colonnes Apache Arrow est mieux adaptée que la disposition orientée ligne FlatBuffers.

Flèche Apache

Le cœur d'Apache Arrow est le format de disposition des données en mémoire. En plus du format, Apache Arrow propose un ensemble de bibliothèques (y compris C, C ++, C #, Go, Java, JavaScript, MATLAB, Python, R, Ruby et Rust), pour travailler avec des données au format Apache Arrow. Le reste de cet article explique comment se familiariser avec les concepts de base d'Arrow et comment écrire une application Java à l'aide d'Apache Arrow.

Concepts de base

Racine de schéma vectoriel

Imaginons que nous modélisons les ventes d'une chaîne de magasins. En règle générale, vous rencontrez un objet pour représenter une vente. Un tel objet aura diverses propriétés, telles que

  • un identifiant
  • des informations sur le magasin dans lequel la vente a été effectuée, comme la région, la ville et peut-être le type de magasin
  • quelques informations client
  • une id du bien vendu
  • une catégorie (et éventuellement une sous-catégorie) du bien vendu
  • combien de marchandises ont été vendues
  • etc…

En Java, une vente est modélisée par une classe Sale. Le cours contient toutes les informations d'une seule vente. Toutes les ventes sont représentées (en mémoire) par une collection d'objets Sale. Du point de vue de la base de données, une collection d'objets Sale équivaut à une base de données relationnelle orientée ligne. En effet, généralement dans une telle application, la collection d'objets est mappée à une table relationnelle dans une base de données pour la persistance.

Dans une base de données orientée colonnes, la collection d'objets est décomposée en une collection de colonnes. Tous les identifiants sont stockés dans une seule colonne. En mémoire, tous les identifiants sont stockés séquentiellement. De même, il y a une colonne pour stocker toutes les villes de magasins pour chaque vente. Conceptuellement, ce format en colonnes peut être considéré comme la décomposition d'une collection d'objets en un ensemble de tableaux de longueur égale. Un tableau par champ dans un objet.

Pour reconstruire un objet spécifique, les tableaux en décomposition sont combinés en choisissant les valeurs de chaque colonne / tableau à un indice donné. Par exemple, la 10e vente est recomposée en prenant la 10e valeur du tableau d'id, la 10e valeur du tableau de ville de magasin, etc.

Apache Arrow fonctionne comme une base de données relationnelle orientée colonne. Une collection d'objets Java est décomposée en une collection de colonnes, appelées vecteurs dans Arrow. Un vecteur est l'unité de base au format colonnaire Flèche.

La mère de tous les vecteurs est le FieldVector. Il existe des types de vecteurs pour le type primitif, tels que Int4Vector et Float8Vector. Il existe un type de vecteur pour les chaînes: le VarCharVector. Il existe un type de vecteur pour les données binaires arbitraires: VarBinaryVector. Il existe plusieurs types de vecteurs pour modéliser l'heure, tels que TimeStampVector, TimeStampSecVector, TimeStampTZVector et TimeMicroVector.

Des structures plus complexes peuvent être composées. Un StructVector est utilisé pour regrouper un ensemble de vecteurs dans un champ. Pensez par exemple aux informations du magasin dans l'exemple de vente ci-dessus. Toutes les informations du magasin (région, ville et type) peuvent être regroupées dans un StructVector. Un ListVector permet de stocker une liste d'éléments de longueur variable dans un champ. Un MapVector stocke un mappage de valeurs-clés dans un vecteur.

Poursuivant l'analogie de la base de données, une collection d'objets est représentée par une table. Pour identifier les valeurs d'une table, une table a un schéma: un nom pour taper le mappage. Dans une base de données orientée lignes, chaque ligne associe un nom à une valeur du type prédéfini. En Java, un schéma correspond à l'ensemble des variables membres d'une définition de classe. Une base de données orientée colonne possède également un schéma. Dans une table, chaque nom du schéma correspond à une colonne du type prédéfini.

Dans la terminologie Apache Arrow, une collection de vecteurs est représentée par un VectorSchemaRoot. Un VectorSchemaRoot contient également un schéma, des noms de mappage (a.k.a. Des champs) aux colonnes (alias Vecteurs).

Allocateur de tampon

Où sont stockées les valeurs que nous ajoutons à un vecteur? Un vecteur flèche est soutenu par un tampon. Il s'agit généralement d'un java.nio.ByteBuffer. Les tampons sont regroupés dans un allocateur de tampon. Vous pouvez demander à un allocateur de tampon de créer un tampon d'une certaine taille, ou vous pouvez laisser l'allocateur de tampon se charger de la création et de l'expansion automatique des tampons pour stocker de nouvelles valeurs. L'allocateur de tampons garde la trace de tous les tampons alloués.

Un vecteur est géré par un allocateur. Nous disons que l'allocateur possède le tampon qui soutient le vecteur. Vecteur la possession peut être transféré d'un allocateur à un autre.

Par exemple, vous implémentez un flux de données. Le flux se compose d'une séquence d'étapes de traitement. Chaque étape effectue certaines opérations sur les données, avant de passer les données à l'étape suivante. Chaque étape aurait son propre allocateur de tampon, gérant les tampons en cours de traitement. Une fois le traitement terminé, les données sont transmises à l'étape suivante.

En d'autres termes, la propriété des tampons soutenant les vecteurs est transférée à l'allocateur de tampons de l'étape suivante. Maintenant, cet allocateur de tampon est responsable de la gestion de la mémoire et de sa libération lorsqu'elle n'est plus nécessaire.

Les tampons créés par un allocateur sont DirectByteBuffers, ils sont donc stockés hors segment. Cela implique que lorsque vous avez fini d'utiliser les données, la mémoire doit être libérée. Cela semble étrange au début pour un programmeur Java. Mais c'est une partie essentielle du travail avec Apache Arrow. Les vecteurs implémentent l'interface AutoCloseable, il est donc recommandé d'envelopper la création de vecteurs dans un bloc d'essayer avec des ressources qui fermera automatiquement le vecteur, c'est-à-dire, libérera la mémoire.

Exemple: écriture, lecture et traitement

Pour conclure cette introduction, nous allons parcourir un exemple d'application utilisant Apache Arrow. L'idée est de lire une «base de données» de personnes à partir d'un fichier sur disque, de filtrer et d'agréger les données et d'imprimer les résultats.

Notez que Apache Arrow est un format en mémoire. Dans une application réelle, vous êtes mieux avec d'autres formats (en colonnes) optimisés pour un stockage persistant, par exemple, Parquet. Parquet ajoute une compression et des résumés intermédiaires aux données écrites sur le disque. Par conséquent, la lecture et l'écriture de fichiers Parquet à partir du disque devraient être plus rapides que la lecture et l'écriture de fichiers Apache Arrow. La flèche est utilisée dans cet exemple uniquement à des fins éducatives.

Imaginons que nous ayons une personne de classe et une adresse de classe (affichant uniquement les parties pertinentes):

Personne publique (String firstName, String lastName, int age, Address address) 
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;

    this.address = adresse;


Adresse publique (String street, int streetNumber, String city, int postalCode) 
    this.street = street;
    this.streetNumber = streetNumber;
    this.city = ville;
    this.postalCode = postalCode;

Nous allons écrire deux applications. La première application va générer une collection de personnes générées aléatoirement et les écrire, au format Arrow, sur le disque. Ensuite, nous allons écrire une application qui lit la «base de données des personnes» au format Arrow du disque dans la mémoire. Sélectionnez toutes les personnes

  • ayant un nom commençant par «P»
  • ont entre 18 et 35 ans
  • vivent dans une rue se terminant par «chemin»

Pour les personnes sélectionnées, nous calculons l'âge moyen, groupé par ville. Cet exemple devrait vous donner une idée de la façon d'utiliser Apache Arrow pour implémenter l'analyse de données en mémoire.

Le code de cet exemple se trouve dans ce référentiel Git.

Écrire des données

Avant de commencer à écrire des données. Notez que le format Arrow est destiné aux données en mémoire. Il n'est pas optimisé pour le stockage sur disque des données. Dans une application réelle, vous devez rechercher des formats tels que Parquet, qui prennent en charge la compression et d'autres astuces pour accélérer le stockage sur disque des données en colonnes, pour conserver vos données. Ici, nous allons écrire des données au format flèche pour garder la discussion focalisée et courte.

Étant donné un tableau d'objets Person, commençons à écrire des données dans un fichier appelé people.arrow. La première étape consiste à convertir le tableau d'objets Person en un Arrow VectorSchemaRoot. Si vous voulez vraiment tirer le meilleur parti d'Arrow, vous devez écrire toute votre application pour utiliser les vecteurs Arrow. Mais à des fins éducatives, il est utile de faire la conversion ici.

private void vectorizePerson (int index, Person person, VectorSchemaRoot schemaRoot) 
    // Utilisation de setSafe: augmente la capacité du tampon si nécessaire
    ((VarCharVector) schemaRoot.getVector ("firstName")). SetSafe (index, person.getFirstName (). GetBytes ());
    ((VarCharVector) schemaRoot.getVector ("lastName")). SetSafe (index, person.getLastName (). GetBytes ());
    ((UInt4Vector) schemaRoot.getVector ("age")). SetSafe (index, person.getAge ());

    liste childrenFromFields = schemaRoot.getVector ("adresse"). getChildrenFromFields ();

    Adresse adresse = person.getAddress ();
    ((VarCharVector) childrenFromFields.get (0)). SetSafe (index, address.getStreet (). GetBytes ());
    ((UInt4Vector) childrenFromFields.get (1)). SetSafe (index, address.getStreetNumber ());
    ((VarCharVector) childrenFromFields.get (2)). SetSafe (index, address.getCity (). GetBytes ());
    ((UInt4Vector) childrenFromFields.get (3)). SetSafe (index, address.getPostalCode ());

Dans vectorizePerson, un objet Person est mappé aux vecteurs dans le schemaRoot avec le schéma person. La méthode setSafe garantit que le tampon de sauvegarde est suffisamment grand pour contenir la valeur suivante. Si le tampon de sauvegarde n'est pas assez grand, le tampon sera étendu.

Un VectorSchemaRoot est un conteneur pour un schéma et une collection de vecteurs. En tant que telle, la classe VectorSchemaRoot peut être considérée comme une base de données sans schéma, le schéma n'est connu que lorsque le schéma est passé dans le constructeur, lors de l'instanciation d'objet. Par conséquent, toutes les méthodes, par exemple, getVector, ont des types de retour très génériques, FieldVector dans ce cas. Par conséquent, un gros casting, basé sur le schéma ou la connaissance de l'ensemble de données, est requis.

Dans cet exemple, nous aurions pu choisir de pré-allouer les UInt4Vectors et UInt2Vector (car nous savons combien de personnes il y a dans un lot à l'avance). Ensuite, nous aurions pu utiliser la méthode set pour éviter les contrôles de taille de tampon et les réallocations pour étendre le tampon.

La fonction vectorizePerson peut être transmise à un ChunkedWriter, une abstraction qui gère la segmentation et l'écriture dans un fichier binaire au format Arrow.

void writeToArrowFile (Person[] personnes) lève IOException 
   new ChunkedWriter <> (CHUNK_SIZE, this :: vectorizePerson) .write (new File ("people.arrow"), people);


Le ChunkedWriter a une méthode d'écriture qui ressemble à ceci:
écriture publique nulle (fichier, personne[] valeurs) lève IOException 
   DictionaryProvider.MapDictionaryProvider dictProvider = new DictionaryProvider.MapDictionaryProvider ();

   try (RootAllocator allocator = new RootAllocator ();
        VectorSchemaRoot schemaRoot = VectorSchemaRoot.create (personSchema (), allocateur);
        FileOutputStream fd = new FileOutputStream (fichier);
        ArrowFileWriter fileWriter = new ArrowFileWriter (schemaRoot, dictProvider, fd.getChannel ())) 
       fileWriter.start ();

       int index = 0;
       while (index <values.length) 
           schemaRoot.allocateNew ();
           int chunkIndex = 0;
           while (chunkIndex <chunkSize && index + chunkIndex <values.length) 
               vectorizer.vectorize (valeurs[index + chunkIndex], chunkIndex, schemaRoot);
               chunkIndex ++;
           
           schemaRoot.setRowCount (chunkIndex);
           fileWriter.writeBatch ();

           index + = chunkIndex;
           schemaRoot.clear ();
       
       fileWriter.end ();
   

Décomposons cela. Tout d'abord, nous créons un (i) allocateur, (ii) schemaRoot et (iii) dictProvider. Nous en avons besoin pour (i) allouer des tampons de mémoire, (ii) être un conteneur pour les vecteurs (soutenu par des tampons), et (iii) faciliter la compression du dictionnaire (vous pouvez l'ignorer pour l'instant).

Ensuite, dans (2), un ArrowFileWriter est créé. Il gère l'écriture sur le disque, sur la base d'un VectorSchemaRoot. L'écriture d'un jeu de données par lots est très simple de cette façon. Enfin, n'oubliez pas de commencer l'écrivain.

Le reste de la méthode consiste à vectoriser le tableau Person, en morceaux, dans la racine du schéma vectoriel et à l'écrire lot par lot.

Quel est l'avantage d'écrire par lots? À un moment donné, les données sont lues sur le disque. Si les données sont écrites en un seul lot, nous devons lire toutes les données à la fois et les stocker dans la mémoire principale. En écrivant des lots, nous permettons au lecteur de traiter les données en petits morceaux, limitant ainsi l'empreinte mémoire.

N'oubliez jamais de définir le nombre de valeurs d'un vecteur ou le nombre de lignes d'une racine de schéma vectoriel (qui définit indirectement le nombre de valeurs de tous les vecteurs contenus). Sans définir le nombre, un vecteur apparaîtra vide, même après avoir enregistré des valeurs dans le vecteur.

Enfin, lorsque toutes les données sont stockées dans des vecteurs, fileWriter.writeBatch () les valide sur le disque.

Une note sur la gestion de la mémoire

Notez les schemaRoot.clear () et allocator.close () sur les lignes (3) et (4). Le premier efface toutes les données de tous les vecteurs contenus dans le VectorSchemaRoot et remet à zéro le nombre de lignes et de valeurs. Ce dernier ferme l'allocateur. Si vous avez oublié de libérer les tampons alloués, cet appel vous informera qu'il y a une fuite de mémoire.

Dans ce paramètre, la fermeture est un peu superflue, car le programme se termine peu de temps après la fermeture de l'allocateur. Cependant, dans une application réelle et de longue durée, la gestion de la mémoire est essentielle.

Les problèmes de gestion de la mémoire sembleront étrangers aux programmeurs Java. Mais dans ce cas, c'est le prix à payer pour les performances. Soyez très conscient des tampons alloués et de les libérer à la fin de leur vie.

Lecture des données

La lecture des données d'un fichier au format Arrow est similaire à l'écriture. Vous configurez un allocateur, une racine de schéma vectoriel (sans schéma, il fait partie du fichier), ouvrez un fichier et laissez ArrowFileReader s'occuper du reste. N'oubliez pas d'initialiser, car cela lira dans le schéma du fichier.

Pour lire un lot, appelez fileReader.loadNextBatch (). Le lot suivant, s'il en existe encore, est lu à partir du disque et les tampons des vecteurs dans schemaRoot sont remplis de données, prêts à être traités.

L'extrait de code suivant décrit brièvement comment lire un fichier Arrow. Pour chaque exécution de la boucle while, un lot sera chargé dans le VectorSchemaRoot. Le contenu du lot est décrit par le VectorSchemaRoot: (i) le schéma du VectorSchemaRoot, et (ii) le nombre de valeurs, est égal au nombre d'entrées.

try (FileInputStream fd = new FileInputStream ("people.arrow");
    ArrowFileReader fileReader = nouveau ArrowFileReader (nouveau SeekableReadChannel (fd.getChannel ()), allocateur)) 
   // Configuration du lecteur de fichiers
   fileReader.initialize ();
   VectorSchemaRoot schemaRoot = fileReader.getVectorSchemaRoot ();

   // Agrégation: utiliser ByteString car c'est plus rapide que de créer une chaîne à partir d'un octet[]
   while (fileReader.loadNextBatch ()) 
      // En traitement …
   

Données en cours

Enfin, les étapes de filtrage, de regroupement et d'agrégation devraient vous donner un aperçu de la façon de travailler avec les vecteurs Arrow dans un logiciel d'analyse de données. Je ne veux certainement pas prétendre que c'est la façon de travailler avec les vecteurs Arrow, mais cela devrait fournir une base de départ solide pour explorer Apache Arrow. Jetez un œil au code source du moteur de traitement Gandiva pour le code Arrow du monde réel. Le traitement des données avec Apache Arrow est un gros sujet. Vous pouvez littéralement écrire un livre à ce sujet.

Notez que l'exemple de code est très spécifique pour le cas d'utilisation Personne. Lors de la construction, par exemple, d'un processeur de requêtes avec des vecteurs Flèche, les noms et types de vecteurs ne sont pas connus à l'avance, ce qui conduit à un code plus générique et plus difficile à comprendre.

Comme Arrow est un format en colonnes, nous pouvons appliquer les étapes de filtrage indépendamment, en utilisant une seule colonne.

private IntArrayList filterOnAge (VectorSchemaRoot schemaRoot) 
    UInt4Vector age = (UInt4Vector) schemaRoot.getVector ("age");
    IntArrayList ageSelectedIndexes = new IntArrayList ();
    for (int i = 0; i <schemaRoot.getRowCount (); i ++) 
        int currentAge = age.get (i);
        if (18 <= currentAge && currentAge <= 35) 
            ageSelectedIndexes.add (i);
        
    
    ageSelectedIndexes.trim ();
    return ageSelectedIndexes;

Cette méthode collecte tous les index dans le bloc chargé du vecteur d'âge pour lequel la valeur est comprise entre 18 et 35.

Chaque filtre produit une liste triée de ces index. Dans l'étape suivante, nous croisons / fusionnons ces listes en une seule liste d'index sélectionnés. Cette liste contient tous les index des lignes répondant à tous les critères.

L'extrait de code suivant montre comment nous pouvons facilement remplir les structures de données d'agrégation (mappage de la ville à un nombre et une somme), à ​​partir des vecteurs et de la collection des identifiants sélectionnés.

VarCharVector cityVector = (VarCharVector) ((StructVector) schemaRoot.getVector ("adresse")). GetChild ("city");
UInt4Vector ageDataVector = (UInt4Vector) schemaRoot.getVector ("age");

for (int selectedIndex: selectedIndexes) 
   String city = new String (cityVector.get (selectedIndex));
   perCityCount.put (ville, perCityCount.getOrDefault (ville, 0L) + 1);
   perCitySum.put (city, perCitySum.getOrDefault (city, 0L) + ageDataVector.get (selectedIndex));

Une fois la structure des données d'agrégation remplie, il est très facile d'imprimer l'âge moyen par ville:

for (String city: perCityCount.keySet ()) 
    double moyenne = (double) perCitySum.get (ville) / perCityCount.get (ville);
    LOGGER.info ("Ville = ; Moyenne = ", ville, moyenne);

Conclusion

Cet article présente Apache Arrow, un format de mise en page de données multilingue en colonne et en mémoire. Il s'agit d'un élément de base pour les systèmes de Big Data, se concentrant sur des transferts de données efficaces entre les machines d'un cluster et entre différents systèmes de Big Data. Pour commencer à développer des applications Java à l'aide d'Apache Arrow, nous avons examiné deux exemples d'applications qui écrivent et lisent des données au format Arrow. Nous avons également eu un premier aperçu du traitement des données avec la bibliothèque Java Apache Arrow.

Apache Arrow est un format en colonnes. Une disposition orientée colonne est généralement mieux adaptée aux charges de travail analytiques que les dispositions orientées ligne. Cependant, il y a toujours des compromis. Pour votre charge de travail spécifique, un format orienté ligne peut donner de meilleurs résultats.

Les VectorSchemaRoots, les tampons et la gestion de la mémoire ne ressembleront pas à votre code Java idiomatique. Si vous pouvez obtenir toutes les performances dont vous avez besoin, à partir d'un cadre différent, par exemple FlatBuffers, cette façon de travailler moins idiomatique pourrait jouer un rôle dans votre décision d'adopter Apache Arrow dans votre application.

Auteur l'auteur

Joris Gillis est développeur de recherche chez TrendMiner. TrendMiner crée un logiciel d'analyse en libre-service pour les données de séries chronologiques IIoT. En tant que développeur de recherche, il travaille sur des algorithmes d'analyse évolutifs, des bases de données de séries chronologiques et la connectivité à des sources de données externes de séries chronologiques.

Commentaires

Laisser un commentaire

Votre commentaire sera révisé par les administrateurs si besoin.