Aplicaciones C ++ distribuidas con un mínimo de esfuerzo.

El propósito de mi publicación es hablar sobre la API de C ++ de la base de datos distribuida Apache Ignite llamada Ignite C ++, así como sus características.


Acerca del Apache Ignite en un habr ya escribió más de una vez, por lo que seguramente algunos de ustedes ya saben aproximadamente qué es y por qué es necesario.


Brevemente sobre Apache Ignite para aquellos que aún no están familiarizados con él.


No entraré en detalles sobre cómo surgió Apache Ignite y cómo difiere de las bases de datos clásicas. Todas estas preguntas ya se han planteado aquí , aquí o aquí .


Entonces, Apache Ignite es esencialmente una base de datos distribuida rápidamente optimizada para trabajar con RAM. Ignite surgió de una cuadrícula de datos en memoria y, hasta hace poco, se posicionó como una caché distribuida muy rápida y completamente en memoria basada en una tabla hash distribuida. Es por eso que, además de almacenar datos, tiene muchas características convenientes para su procesamiento distribuido rápidamente: Map-Reduce, operaciones de datos atómicos, transacciones ACID completas, consultas de datos SQL, las llamadas Consultas continuas, que permiten monitorear cambios en ciertos datos y otros


Recientemente, sin embargo, la plataforma ha agregado soporte para el almacenamiento persistente de datos en el disco . Después de eso, Apache Ignite obtuvo todos los beneficios de una base de datos orientada a objetos completa, manteniendo la comodidad, la riqueza de las herramientas, la flexibilidad y la velocidad de la fecha de la cuadrícula.


Poco de teoría


Una parte importante para entender el trabajo con Apache Ignite es que está escrito en Java. Usted preguntará: "¿Qué diferencia hay en lo que se escribe la base de datos si me comunico con ella de todos modos a través de SQL?" Hay algo de verdad en esto. Si desea usar Ignite solo como una base de datos, puede tomar el controlador ODBC o JDBC que viene con Ignite, aumentar la cantidad de nodos de servidor que necesita usando el script ignite.sh especialmente creado, configurarlos usando configuraciones flexibles y especialmente se dispara sobre el lenguaje, trabajando con Ignite incluso desde PHP, incluso desde Go.


La interfaz nativa de Ignite proporciona muchas más funciones que solo SQL. Desde lo más simple: operaciones atómicas rápidas con objetos en la base de datos, objetos de sincronización distribuida y computación distribuida en un clúster de datos locales, cuando no es necesario extraer cientos de megabytes de datos al cliente para los cálculos. Como comprenderá, esta parte de la API no funciona a través de SQL, sino que está escrita en lenguajes de programación de propósito general muy específicos.


Naturalmente, dado que Ignite está escrito en Java, la API más completa se implementa en este lenguaje de programación. Sin embargo, además de Java, también hay versiones de API para C # .NET y C ++. Estos son los llamados clientes "gruesos", de hecho, el nodo Ignite en la JVM, lanzado desde C ++ o C #, cuya comunicación ocurre a través de la JNI. Este tipo de nodo es necesario, entre otras cosas, para que el clúster pueda ejecutar computación distribuida en los lenguajes correspondientes: C ++ y C #.


Además, hay un protocolo abierto para los llamados clientes "delgados". Estas ya son bibliotecas ligeras en varios lenguajes de programación que se comunican con el clúster a través de TCP / IP. Ocupan mucho menos espacio de memoria, se inician casi instantáneamente, no requieren una JVM en la máquina, pero tienen una latencia ligeramente peor y API no tan ricas en comparación con los clientes "gruesos". Hoy en día hay clientes ligeros en Java, C # y Node.js, los clientes en C ++, PHP, Python3, Go se desarrollan activamente.


En una publicación, miraré la API gruesa Ignite para la API C ++, ya que es la que actualmente proporciona la API más completa.


Empezando


No me detendré en detalle en la instalación y configuración del marco en sí mismo: el proceso es rutinario, no muy interesante y está bien descrito, por ejemplo, en la documentación oficial . Vayamos directamente al código.


Dado que Apache Ignite es una plataforma distribuida, para comenzar, lo primero que debe hacer es ejecutar al menos un nodo. Esto se hace simplemente usando la clase 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; } 

Felicitaciones, lanzó su primer nodo Apache Ignite de caché con la configuración predeterminada. La clase Ignite, a su vez, es el principal punto de entrada para acceder a toda la API del clúster.


Trabajar con datos


El componente principal de Ignite C ++ que proporciona una API para trabajar con datos es el caché, ignite::cache::Cache<K,V> . El caché proporciona un conjunto básico de métodos para trabajar con datos. Dado que Cache es esencialmente una interfaz para una tabla hash distribuida, los métodos básicos para trabajar con ella se parecen a trabajar con contenedores comunes como map o 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; } 

Parece bastante simple, ¿verdad? De hecho, las cosas se complican un poco si observamos más de cerca las limitaciones de C ++.


Desafíos de integración de C ++


Como mencioné, Apache Ignite está completamente escrito en Java, un poderoso lenguaje impulsado por OOP. Es lógico que muchas de las características de este lenguaje, asociadas, por ejemplo, con el reflejo del tiempo de ejecución del programa, se usaran activamente para implementar componentes Apache Ignite. Por ejemplo, para la serialización / deserialización de objetos para almacenamiento en un disco y transmisión a través de una red.


En C ++, a diferencia de Java, no existe una reflexión tan poderosa. En general, no, todavía no, desafortunadamente. En particular, no hay formas de averiguar la lista y el tipo de campos de un objeto, lo que podría permitir generar automáticamente el código necesario para serializar / deserializar objetos de tipos de usuario. Por lo tanto, la única opción aquí es pedirle al usuario que proporcione explícitamente el conjunto de metadatos necesarios sobre el tipo de usuario y cómo trabajar con él.


En Ignite C ++, esto se implementa a través de la especialización de la plantilla ignite::binary::BinaryType<T> . Este enfoque se utiliza tanto en clientes "gruesos" como en "ligeros". Para la clase Persona presentada anteriormente, una especialización similar podría verse así:


 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 

Como puede ver, además de los BinaryType<Person>::Write serialización / deserialización BinaryType<Person>::Write , BinaryType<Person>::Read , hay varios otros métodos. Son necesarios para explicar a la plataforma cómo trabajar con tipos personalizados de C ++ en otros lenguajes, en particular Java. Echemos un vistazo más de cerca a cada método:


  • GetTypeName() : devuelve el nombre del tipo. El nombre del tipo debe ser el mismo en todas las plataformas en las que se usa el tipo. Si usa el tipo solo en Ignite C ++, el nombre puede ser cualquier cosa.
  • GetTypeId() : este método devuelve un identificador único multiplataforma para el tipo. Para un trabajo correcto con un tipo en diferentes plataformas, es necesario que se calcule igual en todas partes. El GetBinaryStringHashCode(TypeName) devuelve el mismo ID de tipo que en todas las demás plataformas de forma predeterminada, es decir, esta implementación de este método le permite trabajar correctamente con este tipo desde otras plataformas.
  • GetFieldId() : devuelve un identificador único para el nombre del tipo. Nuevamente, para el trabajo multiplataforma correcto, vale la pena usar el método GetBinaryStringHashCode() ;
  • IsNull() : comprueba si una instancia de una clase es un objeto de tipo NULL . Se utiliza para serializar correctamente los valores NULL . No es muy útil con instancias de la clase en sí, pero puede ser extremadamente conveniente si el usuario desea trabajar con punteros inteligentes y definir la especialización, por ejemplo, para BinaryType< std::unique_ptr<Person> > .
  • GetNull() : se llama al intentar deserializar un valor NULL . Todo lo dicho sobre IsNull también es cierto para GetNull() .

SQL


Si hacemos una analogía con las bases de datos clásicas, entonces el caché es un esquema de base de datos con un nombre de clase que contiene una tabla, con un nombre de tipo. Además de los esquemas de caché, existe un esquema general llamado PUBLIC , en el que puede crear / eliminar un número ilimitado de tablas utilizando comandos DDL estándar, como CREATE TABLE , DROP TABLE etc. Es al esquema PÚBLICO que generalmente se conectan a través de ODBC / JDBC si quieren usar Ignite simplemente como una base de datos distribuida.


Ignite admite consultas SQL completas, incluidas DML y DDL. Todavía no hay soporte para transacciones SQL, pero la comunidad ahora está trabajando activamente en la implementación de MVCC, que agregará transacciones y, hasta donde yo sé, los cambios principales se introdujeron recientemente en master.


Para trabajar con datos de caché a través de SQL, debe especificar explícitamente en la configuración de caché qué campos del objeto se utilizarán en las consultas SQL. La configuración se escribe en el archivo XML, después de lo cual se especifica la ruta al archivo de configuración cuando se inicia el nodo:


 <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> 

El motor de Java analiza la configuración, por lo que los tipos base también deben especificarse para Java. Después de crear el archivo de configuración, debe iniciar el nodo, obtener la instancia de caché y puede comenzar a usar 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; } 

Del mismo modo, puede usar insert , update , create table y otras consultas. Por supuesto, las solicitudes de caché cruzado también son compatibles. Sin embargo, en este caso, el nombre del caché debe indicarse en la solicitud entre comillas como el nombre del esquema. Por ejemplo, en lugar de


 select * from Person inner join Profession 

debería escribir


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

Y así sucesivamente


Realmente hay muchas posibilidades en Apache Ignite y, por supuesto, en una publicación fue imposible cubrirlas todas. La API de C ++ se está desarrollando activamente ahora, por lo que pronto habrá más interesantes. Es posible que escriba algunas publicaciones más donde analizaré algunas características con más detalle.


PD: He sido un confirmador de Apache Ignite desde 2017 y estoy desarrollando activamente la API de C ++ para este producto. Si está bastante familiarizado con C ++, Java o .NET y desea participar en el desarrollo de un producto abierto con una comunidad activa y amigable, siempre encontraremos otras tareas interesantes para usted.

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


All Articles