Applications C ++ distribuées avec un minimum d'effort

Le but de mon article est de parler de l'API C ++ de la base de données distribuée Apache Ignite appelée Ignite C ++, ainsi que de ses fonctionnalités.


À propos d' Apache Ignite sur un habr a déjà écrit plus d'une fois, donc certains d'entre vous savent déjà à peu près ce que c'est et pourquoi c'est nécessaire.


Brièvement sur Apache Ignite pour ceux qui ne le connaissent pas encore


Je n'entrerai pas dans les détails sur la façon dont Apache Ignite est né et comment il diffère des bases de données classiques. Toutes ces questions ont déjà été soulevées ici , ici ou ici .


Ainsi, Apache Ignite est essentiellement une base de données distribuée rapide optimisée pour travailler avec la RAM. Ignite s'est développé à partir d'une grille de données en mémoire et, jusqu'à récemment, était positionné comme un cache distribué très rapide et entièrement en mémoire basé sur une table de hachage distribuée. C'est pourquoi, en plus de stocker des données, il possède de nombreuses fonctionnalités pratiques pour leur traitement distribué rapide: Map-Reduce, opérations de données atomiques, transactions ACID à part entière, requêtes de données SQL, ce qu'on appelle les requêtes continues, qui permettent de surveiller les modifications de certaines données et d'autres.


Récemment, cependant, la plateforme a ajouté la prise en charge du stockage persistant des données sur disque . Après cela, Apache Ignite a obtenu tous les avantages d'une base de données orientée objet à part entière, tout en préservant la commodité, la richesse des outils, la flexibilité et la vitesse de la date de la grille.


Un peu de théorie


Une partie importante pour comprendre le travail avec Apache Ignite est qu'il est écrit en Java. Vous demanderez: "Quelle différence cela fait-il à ce que la base de données est écrite si je communique avec elle de toute façon via SQL?" Il y a du vrai là-dedans. Si vous souhaitez utiliser Ignite uniquement comme base de données, vous pouvez utiliser le pilote ODBC ou JDBC fourni avec Ignite, augmenter le nombre de nœuds de serveur dont vous avez besoin à l'aide du script ignite.sh spécialement créé, les configurer à l'aide de configurations flexibles et surtout plane sur le langage, travaillant avec Ignite même depuis PHP, même depuis Go.


L'interface native Ignite offre bien plus de fonctionnalités que le simple SQL. Du plus simple: opérations atomiques rapides avec des objets dans la base de données, objets de synchronisation distribués et calcul distribué dans un cluster sur des données locales, lorsque vous n'avez pas besoin de tirer des centaines de mégaoctets de données vers le client pour les calculs. Comme vous le comprenez, cette partie de l'API ne fonctionne pas via SQL, mais est écrite dans des langages de programmation à usage général très spécifiques.


Naturellement, comme Ignite est écrit en Java, l'API la plus complète est implémentée dans ce langage de programmation. Cependant, outre Java, il existe également des versions d'API pour C # .NET et C ++. Ce sont les clients dits "épais" - en fait, le nœud Ignite dans la JVM, lancé à partir de C ++ ou C #, avec lequel la communication se fait via le JNI. Ce type de nœud est nécessaire, entre autres, pour que le cluster puisse exécuter l'informatique distribuée dans les langages correspondants - C ++ et C #.


De plus, il existe un protocole ouvert pour les clients dits «légers». Ce sont déjà des bibliothèques légères dans divers langages de programmation qui communiquent avec le cluster via TCP / IP. Ils occupent beaucoup moins d'espace mémoire, démarrent presque instantanément, ne nécessitent pas de machine virtuelle Java sur la machine, mais ils ont une latence légèrement pire et des API moins riches par rapport aux clients "épais". Aujourd'hui, il existe des clients légers en Java, C # et Node.js, des clients en C ++, PHP, Python3, Go sont activement développés.


Dans un article, je vais regarder l'API Ignite thick pour l'API C ++, car c'est elle qui fournit actuellement l'API la plus complète.


Pour commencer


Je ne m'attarderai pas en détail sur l'installation et la configuration du framework lui-même - le processus est routinier, pas très intéressant et est bien décrit, par exemple, dans la documentation officielle . Passons directement au code.


Apache Ignite étant une plate-forme distribuée, pour commencer, la première chose à faire est d'exécuter au moins un nœud. Cela se fait très simplement en utilisant la classe ignite::Ignition :


 #include <iostream> #include <ignite/ignition.h> using namespace ignite; int main() { IgniteConfiguration cfg; Ignite node = Ignition::Start(cfg); std::cout << "Node started. Press 'Enter' to stop" << std::endl; std::cin.get(); Ignition::StopAll(false); std::cout << "Node stopped" << std::endl; return 0; } 

Félicitations, vous avez lancé votre premier nœud Cache Apache Ignite avec les paramètres par défaut. La classe Ignite, à son tour, est le principal point d'entrée pour accéder à l'ensemble de l'API du cluster.


Travailler avec des données


Le composant principal d'Ignite C ++ qui fournit une API pour travailler avec des données est le cache, ignite::cache::Cache<K,V> . Le cache fournit un ensemble de méthodes de base pour travailler avec des données. Étant donné que Cache est essentiellement une interface vers une table de hachage distribuée, les méthodes de base pour travailler avec lui ressemblent à celles utilisées avec des conteneurs ordinaires comme map ou unordered_map .


 #include <string> #include <cassert> #include <cstdint> #include <ignite/ignition.h> using namespace ignite; struct Person { int32_t age; std::string firstName; std::string lastName; } //... int main() { IgniteConfiguration cfg; Ignite node = Ignition::Start(cfg); cache::Cache<int32_t, Person> personCache = node.CreateCache<int32_t, Person>("PersonCache"); Person p1 = { 35, "John", "Smith" }; personCache.Put(42, p1); Person p2 = personCache.Get(42); std::cout << p2 << std::endl; assert(p1 == p2); return 0; } 

Ça a l'air assez simple, non? En fait, les choses se compliquent un peu si nous regardons de plus près les limites du C ++.


Défis d'intégration C ++


Comme je l'ai mentionné, Apache Ignite est entièrement écrit en Java - un langage puissant orienté OOP. Il est logique que de nombreuses fonctionnalités de ce langage, associées, par exemple, à la réflexion du temps d'exécution du programme, aient été activement utilisées pour implémenter les composants Apache Ignite. Par exemple, pour la sérialisation / désérialisation d'objets pour le stockage sur un disque et la transmission sur un réseau.


En C ++, contrairement à Java, il n'y a pas de réflexion aussi puissante. En général, non, pas encore, malheureusement. En particulier, il n'existe aucun moyen de connaître la liste et le type de champs d'un objet, ce qui pourrait permettre de générer automatiquement le code nécessaire à la sérialisation / désérialisation d'objets de types utilisateurs. Par conséquent, la seule option ici est de demander à l'utilisateur de fournir explicitement l'ensemble nécessaire de métadonnées sur le type d'utilisateur et comment travailler avec lui.


Dans Ignite C ++, cela est implémenté via la spécialisation du modèle ignite::binary::BinaryType<T> . Cette approche est utilisée aussi bien dans les clients "épais" que dans les clients "légers". Pour la classe Person présentée ci-dessus, une spécialisation similaire pourrait ressembler à ceci:


 namespace ignite { namespace binary { template<> struct BinaryType<Person> { static int32_t GetTypeId() { return GetBinaryStringHashCode("Person"); } static void GetTypeName(std::string& name) { name = "Person"; } static int32_t GetFieldId(const char* name) { return GetBinaryStringHashCode(name); } static bool IsNull(const Person& obj) { return false; } static void GetNull(Person& dst) { dst = Person(); } static void Write(BinaryWriter& writer, const Person& obj) { writer.WriteInt32("age", obj.age; writer.WriteString("firstName", obj.firstName); writer.WriteString("lastName", obj.lastName); } static void Read(BinaryReader& reader, Person& dst) { dst.age = reader.ReadInt32("age"); dst.firstName = reader.ReadString("firstName"); dst.lastName = reader.ReadString("lastName"); } }; } // namespace binary } // namespace ignite 

Comme vous pouvez le voir, en plus des BinaryType<Person>::Write sérialisation / désérialisation BinaryType<Person>::Write , BinaryType<Person>::Read , il existe plusieurs autres méthodes. Ils sont nécessaires pour expliquer à la plateforme comment travailler avec des types C ++ personnalisés dans d'autres langages, en particulier Java. Examinons de plus près chaque méthode:


  • GetTypeName() - Renvoie le nom du type. Le nom du type doit être le même sur toutes les plateformes sur lesquelles le type est utilisé. Si vous utilisez le type uniquement dans Ignite C ++, le nom peut être n'importe quoi.
  • GetTypeId() - Cette méthode renvoie un identifiant unique multiplateforme pour le type. Pour un travail correct avec un type sur différentes plateformes, il faut qu'il soit calculé de la même manière partout. La GetBinaryStringHashCode(TypeName) renvoie le même ID de type que sur toutes les autres plates-formes par défaut, c'est-à-dire que cette implémentation de cette méthode vous permet de travailler correctement avec ce type à partir d'autres plates-formes.
  • GetFieldId() - Retourne un identifiant unique pour le nom du type. Encore une fois, pour le travail multiplateforme correct, il vaut la peine d'utiliser la méthode GetBinaryStringHashCode() ;
  • IsNull() - Vérifie si une instance d'une classe est un objet de type NULL . Utilisé pour sérialiser correctement les valeurs NULL . Pas très utile avec les instances de la classe elle-même, mais cela peut être extrêmement pratique si l'utilisateur souhaite travailler avec des pointeurs intelligents et définir la spécialisation, par exemple, pour BinaryType< std::unique_ptr<Person> > .
  • GetNull() - Appelé lors de la tentative de désérialisation d'une valeur NULL . Tout ce qui est dit sur IsNull est également vrai pour GetNull() .

SQL


Si nous établissons une analogie avec les bases de données classiques, le cache est un schéma de base de données avec un nom de classe contenant une table - avec un nom de type. En plus des schémas de cache, il existe un schéma général appelé PUBLIC , dans lequel vous pouvez créer / supprimer un nombre illimité de tables à l'aide de commandes DDL standard, telles que CREATE TABLE , DROP TABLE , etc. C'est au schéma PUBLIC qu'ils se connectent généralement via ODBC / JDBC s'ils veulent utiliser Ignite simplement comme base de données distribuée.


Ignite prend en charge les requêtes SQL complètes, y compris DML et DDL. Il n'y a pas encore de support pour les transactions SQL, mais la communauté travaille maintenant activement sur l'implémentation de MVCC, qui ajoutera des transactions, et, pour autant que je sache, les principaux changements ont été récemment introduits dans master.


Pour travailler avec des données de cache via SQL, vous devez spécifier explicitement dans la configuration de cache quels champs de l'objet seront utilisés dans les requêtes SQL. La configuration est écrite dans le fichier XML, après quoi le chemin d'accès au fichier de configuration est spécifié au démarrage du nœud:


 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration"> <property name="cacheConfiguration"> <list> <bean class="org.apache.ignite.configuration.CacheConfiguration"> <property name="name" value="PersonCache"/> <property name="queryEntities"> <list> <bean class="org.apache.ignite.cache.QueryEntity"> <property name="keyType" value="java.lang.Integer"/> <property name="valueType" value="Person"/> <property name="fields"> <map> <entry key="age" value="java.lang.Integer"/> <entry key="firstName" value="java.lang.String"/> <entry key="lastName" value="java.lang.String"/> </map> </property> </bean> </list> </property> </bean> </list> </property> </bean> </beans> 

La configuration est analysée par le moteur Java, donc les types de base doivent également être spécifiés pour Java. Une fois le fichier de configuration créé, vous devez démarrer le nœud, obtenir l'instance de cache et vous pouvez commencer à utiliser SQL:


 //... int main() { IgniteConfiguration cfg; cfg.springCfgPath = "config.xml"; Ignite node = Ignition::Start(cfg); cache::Cache<int32_t, Person> personCache = node.GetCache<int32_t, Person>("PersonCache"); personCache.Put(1, Person(35, "John", "Smith")); personCache.Put(2, Person(31, "Jane", "Doe")); personCache.Put(3, Person(12, "Harry", "Potter")); personCache.Put(4, Person(12, "Ronald", "Weasley")); cache::query::SqlFieldsQuery qry( "select firstName, lastName from Person where age = ?"); qry.AddArgument<int32_t>(12); cache::query::QueryFieldsCursor cursor = cache.Query(qry); while (cursor.HasNext()) { QueryFieldsRow row = cursor.GetNext(); std::cout << row.GetNext<std::string>() << ", "; std::cout << row.GetNext<std::string>() << std::endl; } return 0; } 

De la même manière, vous pouvez utiliser l' insert , la update , la create table et d'autres requêtes. Bien sûr, les demandes de cache croisé sont également prises en charge. Cependant, dans ce cas, le nom du cache doit être indiqué dans la demande entre guillemets comme nom du schéma. Par exemple, au lieu de


 select * from Person inner join Profession 

devrait écrire


 select * from "PersonCache".Person inner join "ProfessionCache".Profession 

Et ainsi de suite


Il y a vraiment beaucoup de possibilités dans Apache Ignite et, bien sûr, dans un poste, il était impossible de les couvrir toutes. L'API C ++ se développe activement maintenant, donc bientôt il y aura plus d'intéressant. Il est possible que j'écrive quelques articles supplémentaires où j'analyserai certaines fonctionnalités plus en détail.


PS Je suis un committer Apache Ignite depuis 2017 et je développe activement l'API C ++ pour ce produit. Si vous êtes assez familier avec C ++, Java ou .NET et souhaitez participer au développement d'un produit ouvert avec une communauté active et conviviale, nous trouverons toujours quelques autres tâches intéressantes pour vous.

Source: https://habr.com/ru/post/fr420623/


All Articles