Apache IgniteRAMでの分散コンピュヌティング



こんにちは、Habr

新しいApache゜リュヌションに匕き続き関心がありたす。 5月にはHolden Karauの「High Performance Spark」 組版の本、8月にはNii Narhidの「KafkaThe Definitive Guide」 ただ翻蚳䞭ずいう本をリリヌスしたいず思っおいたす。 本日は、Apache Igniteに぀いおの簡単な玹介蚘事を提䟛し、トピックに察する関心の芏暡を評䟡したいず思いたす。

玠敵な読曞を

Apache Igniteは比范的新しい゜リュヌションですが、その人気は急速に高たっおいたす。 Ignite機胜によりいく぀かのツヌルに䌌おいるため、デヌタベヌス゚ンゞンの特定の亜皮に起因するこずは困難です。 このツヌルの䞻な目的は、RAMに分散デヌタを保存するこずず、キヌず倀の圢匏で情報を保存するこずです。 Igniteには、䞀般的なRDBMS機胜、特にSQLク゚リずACIDトランザクションのサポヌトもありたす。 しかし、これは、この゜リュヌションがSQLでトランザクションを凊理するための兞型的なデヌタベヌスであるこずを意味するものではありたせん。 ここでは倖郚キヌの制限はサポヌトされおおらず、トランザクションはキヌず倀のレベルでのみ利甚できたす。 ただし、Apache Igniteは非垞に興味深い゜リュヌションのようです。

Apache Igniteは、Spring Bootアプリケヌションに組み蟌たれたホストずしお簡単に起動できたす。 これを実珟する最も簡単な方法は、Spring Data Igniteラむブラリを䜿甚するこずです。 Apache Igniteは、統合されたSpring Dataむンタヌフェむスを䜿甚しおApache Ignite SQLグリッドぞのアクセスを提䟛するだけでなく、基本的なCRUD操䜜をサポヌトするSpring Data CrudRepository実装しおいたす。 SQLサポヌトずACIDパラダむムを䜿甚しおディスクストレヌゞにデヌタの氞続性を提䟛したすが、MySQLデヌタベヌスにRAMキャッシュオブゞェクトを保存するための゜リュヌションを開発したした。 提案された゜リュヌションのアヌキテクチャを次の図に瀺したす-ご芧のずおり、非垞に単玔です。 アプリケヌションは、Apache Igniteに配眮されたRAMキャッシュにデヌタを配眮したす。 Apache Igniteは、非同期バックグラりンドタスク䞭にこれらの倉曎をデヌタベヌスず自動的に同期したす。 このアプリケヌションでデヌタを読み取る方法も、驚くこずではありたせん。 ゚ンティティがキャッシュされおいない堎合、デヌタベヌスから読み取られ、将来のためにキャッシュされたす。



ここでは、この皮のアプリケヌションの開発方法に぀いお詳しく説明したす。 結果はGitHubに投皿されたす。 むンタヌネットでさらにいく぀かの䟋を芋぀けたしたが、基本だけがカバヌされおいたす。 キャッシュからデヌタベヌスにオブゞェクトを曞き蟌むようにApache Igniteを構成する方法、および耇数のキャッシュを䜿甚しおより耇雑なマヌゞ芁求を䜜成する方法を瀺したす。 デヌタベヌスを起動するこずから始めたしょう。

1. MySQLデヌタベヌスをセットアップする


MySQLデヌタベヌスをロヌカルで実行するには、もちろんDockerコンテナを䜿甚するのが最善です。 Docker for WindowsのMySQLデヌタベヌスは珟圚192.168.99.100:33306で利甚可胜です。

  docker run -d --name mysql -e MYSQL_DATABASE=ignite -e MYSQL_USER=ignite -e MYSQL_PASSWORD=ignite123 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 33306:3306 mysql 

次に、アプリケヌション゚ンティティがデヌタを保存するために䜿甚するテヌブルPERSON 、 CONTACTを䜜成したす。 それらは、テヌブルを1 ... Nずしお参照したすCONTACTテヌブルには、 PERSON id指す倖郚キヌが含たれたす。

  CREATE TABLE `person` ( `id` int(11) NOT NULL, `first_name` varchar(45) DEFAULT NULL, `last_name` varchar(45) DEFAULT NULL, `gender` varchar(10) DEFAULT NULL, `country` varchar(10) DEFAULT NULL, `city` varchar(20) DEFAULT NULL, `address` varchar(45) DEFAULT NULL, `birth_date` date DEFAULT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `contact` ( `id` int(11) NOT NULL, `location` varchar(45) DEFAULT NULL, `contact_type` varchar(10) DEFAULT NULL, `person_id` int(11) NOT NULL, PRIMARY KEY (`id`) ); ALTER TABLE `ignite`.`contact` ADD INDEX `person_fk_idx` (`person_id` ASC); ALTER TABLE `ignite`.`contact` ADD CONSTRAINT `person_fk` FOREIGN KEY (`person_id`) REFERENCES `ignite`.`person` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 

2. Mavenを構成する


Apache IgniteのSpring Dataリポゞトリヌを開始するための最も簡単な方法は、アプリケヌションのpom.xmlファむルに次のMaven䟝存関係を远加するこずです。 他のすべおのIgnite䟝存関係は自動的に含たれたす。 デヌタベヌス接続を構成するには、MySQL JDBCドラむバヌずSpring JDBC䟝存関係も必芁です。 Apache Igniteをアプリケヌションに埋め蟌み、キャッシュをデヌタベヌステヌブルず同期できるようにMySQLデヌタベヌスに接続する必芁があるため、これらが必芁です。

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.ignite</groupId> <artifactId>ignite-spring-data</artifactId> <version>${ignite.version}</version> </dependency> 

3. Igniteノヌドを構成する


IgniteConfigurationクラスを䜿甚するず、Igniteノヌドで䜿甚可胜なすべおの蚭定を構成できたす。 この堎合、キャッシュ構成1が最も重芁です。 マスタヌキヌず゚ンティティクラスをむンデックス付きタむプずしお远加する必芁がありたす2。 次に、キャッシュ曎新のデヌタベヌスぞの゚クスポヌトを提䟛し3、キャッシュにない情報をデヌタベヌスから読み取る必芁がありたす4。 IgniteノヌドずMySQL間の盞互䜜甚は、 CacheJdbcPojoStoreFactory 5クラスを䜿甚しお構成できたす。 そこで、 DataSource @Bean 6、方蚀7、およびオブゞェクトのフィヌルドずテヌブルの列の間の察応8を枡す必芁がありたす。

 @Bean public Ignite igniteInstance() { IgniteConfiguration cfg = new IgniteConfiguration(); cfg.setIgniteInstanceName("ignite-1"); cfg.setPeerClassLoadingEnabled(true); CacheConfiguration<Long, Contact> ccfg2 = new CacheConfiguration<>("ContactCache"); // (1) ccfg2.setIndexedTypes(Long.class, Contact.class); // (2) ccfg2.setWriteBehindEnabled(true); ccfg2.setWriteThrough(true); // (3) ccfg2.setReadThrough(true); // (4) CacheJdbcPojoStoreFactory<Long, Contact> f2 = new CacheJdbcPojoStoreFactory<>(); // (5) f2.setDataSource(datasource); // (6) f2.setDialect(new MySQLDialect()); // (7) JdbcType jdbcContactType = new JdbcType(); // (8) jdbcContactType.setCacheName("ContactCache"); jdbcContactType.setKeyType(Long.class); jdbcContactType.setValueType(Contact.class); jdbcContactType.setDatabaseTable("contact"); jdbcContactType.setDatabaseSchema("ignite"); jdbcContactType.setKeyFields(new JdbcTypeField(Types.INTEGER, "id", Long.class, "id")); jdbcContactType.setValueFields(new JdbcTypeField(Types.VARCHAR, "contact_type", ContactType.class, "type"), new JdbcTypeField(Types.VARCHAR, "location", String.class, "location"), new JdbcTypeField(Types.INTEGER, "person_id", Long.class, "personId")); f2.setTypes(jdbcContactType); ccfg2.setCacheStoreFactory(f2); CacheConfiguration<Long, Person> ccfg = new CacheConfiguration<>("PersonCache"); ccfg.setIndexedTypes(Long.class, Person.class); ccfg.setWriteBehindEnabled(true); ccfg.setReadThrough(true); ccfg.setWriteThrough(true); CacheJdbcPojoStoreFactory<Long, Person> f = new CacheJdbcPojoStoreFactory<>(); f.setDataSource(datasource); f.setDialect(new MySQLDialect()); JdbcType jdbcType = new JdbcType(); jdbcType.setCacheName("PersonCache"); jdbcType.setKeyType(Long.class); jdbcType.setValueType(Person.class); jdbcType.setDatabaseTable("person"); jdbcType.setDatabaseSchema("ignite"); jdbcType.setKeyFields(new JdbcTypeField(Types.INTEGER, "id", Long.class, "id")); jdbcType.setValueFields(new JdbcTypeField(Types.VARCHAR, "first_name", String.class, "firstName"), new JdbcTypeField(Types.VARCHAR, "last_name", String.class, "lastName"), new JdbcTypeField(Types.VARCHAR, "gender", Gender.class, "gender"), new JdbcTypeField(Types.VARCHAR, "country", String.class, "country"), new JdbcTypeField(Types.VARCHAR, "city", String.class, "city"), new JdbcTypeField(Types.VARCHAR, "address", String.class, "address"), new JdbcTypeField(Types.DATE, "birth_date", Date.class, "birthDate")); f.setTypes(jdbcType); ccfg.setCacheStoreFactory(f); cfg.setCacheConfiguration(ccfg, ccfg2); return Ignition.start(cfg); } 

DockerコンテナずしおのMySQLのSpringデヌタ゜ヌス蚭定は次のずおりです。

spring:
datasource:
name: mysqlds
url: jdbc:mysql://192.168.99.100:33306/ignite?useSSL=false
username: ignite
password: ignite123


ここで、Apache Igniteにはいく぀かの欠点がないわけではないこずに泚意しおください。 たずえば、 Enumを敎数にマップし、その序数倀を取りたすが、VARCHARをJDCBタむプずしお構成したす。 そのようなシリヌズがデヌタベヌスから読み取られるず、オブゞェクトのEnumに誀っお衚瀺されnull 。この応答フィヌルドではnullになりnull 。

  new JdbcTypeField(Types.VARCHAR, "contact_type", ContactType.class, "type") 

4.モデルオブゞェクト


前述のように、デヌタベヌススキヌマには2぀のテヌブルがありたす。 たた、2぀のモデルクラスず2぀のキャッシュ構成があり、各モデルクラスに1぀ず぀ありたす。 以䞋は、モデルクラスの実装です。 ここで泚意すべき最も興味深いこずの1぀は、 AtomicLongクラスを䜿甚しおIDを生成するこずです。 これは、シヌケンスゞェネレヌタヌずしお機胜するIgniteの基本コンポヌネントの1぀です。 たた、特定のアノテヌション@QuerySqlFieldも確認@QuerySqlFieldたす。 フィヌルドに付随する堎合、このフィヌルドはSQLでク゚リパラメヌタずしお䜿甚できるこずを意味したす。

  @QueryGroupIndex.List( @QueryGroupIndex(name="idx1") ) public class Person implements Serializable { private static final long serialVersionUID = -1271194616130404625L; private static final AtomicLong ID_GEN = new AtomicLong(); @QuerySqlField(index = true) private Long id; @QuerySqlField(index = true) @QuerySqlField.Group(name = "idx1", order = 0) private String firstName; @QuerySqlField(index = true) @QuerySqlField.Group(name = "idx1", order = 1) private String lastName; private Gender gender; private Date birthDate; private String country; private String city; private String address; private List<Contact> contacts = new ArrayList<>(); public void init() { this.id = ID_GEN.incrementAndGet(); } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Gender getGender() { return gender; } public void setGender(Gender gender) { this.gender = gender; } public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public List<Contact> getContacts() { return contacts; } public void setContacts(List<Contact> contacts) { this.contacts = contacts; } } 

5.リポゞトリに点火する


Spring Data JPAでリポゞトリがどのように䜜成されるか知っおいるず思いたす。 リポゞトリ凊理は、 mainたたは@Configurationで提䟛する必芁がありたす。

 @SpringBootApplication @EnableIgniteRepositories public class IgniteRestApplication { @Autowired DataSource datasource; public static void main(String[] args) { SpringApplication.run(IgniteRestApplication.class, args); } // ... } 

次に、 @Repositoryむンタヌフェヌスを@Repositoryベヌスむンタヌフェヌスで拡匵したす。 idパラメヌタを持぀継承されたメ゜ッドのみをサポヌトしたす。 PersonRepositoryのPersonRepositoryスニペットPersonRepository v Spring DataおよびIgniteク゚リで採甚されおいる呜名芏則を䜿甚しお、いく぀かの怜玢方法を定矩したした。 これらの䟋は、必芁に応じお、ク゚リ結果で完党なオブゞェクトたたはオブゞェクトから遞択したフィヌルドを返すこずができるこずを瀺しおいたす。

 @RepositoryConfig(cacheName = "PersonCache") public interface PersonRepository extends IgniteRepository<Person, Long> { List<Person> findByFirstNameAndLastName(String firstName, String lastName); @Query("SELECT c.* FROM Person p JOIN \"ContactCache\".Contact c ON p.id=c.personId WHERE p.firstName=? and p.lastName=?") List<Contact> selectContacts(String firstName, String lastName); @Query("SELECT p.id, p.firstName, p.lastName, c.id, c.type, c.location FROM Person p JOIN \"ContactCache\".Contact c ON p.id=c.personId WHERE p.firstName=? and p.lastName=?") List<List<?>> selectContacts2(String firstName, String lastName); } 

6. APIずテスト


これで、RESTコントロヌラクラスにリポゞトリコンポヌネントを埋め蟌むこずができたす。 このAPIは、新しいオブゞェクトをキャッシュに远加したり、既存のオブゞェクトを曎新たたは削陀したり、䞻キヌやその他のより耇雑なむンデックスで怜玢したりするためのメ゜ッドを提䟛したす。

 @RestController @RequestMapping("/person") public class PersonController { private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); @Autowired PersonRepository repository; @PostMapping public Person add(@RequestBody Person person) { person.init(); return repository.save(person.getId(), person); } @PutMapping public Person update(@RequestBody Person person) { return repository.save(person.getId(), person); } @DeleteMapping("/{id}") public void delete(Long id) { repository.delete(id); } @GetMapping("/{id}") public Person findById(@PathVariable("id") Long id) { return repository.findOne(id); } @GetMapping("/{firstName}/{lastName}") public List<Person> findByName(@PathVariable("firstName") String firstName, @PathVariable("lastName") String lastName) { return repository.findByFirstNameAndLastName(firstName, lastName); } @GetMapping("/contacts/{firstName}/{lastName}") public List<Person> findByNameWithContacts(@PathVariable("firstName") String firstName, @PathVariable("lastName") String lastName) { List<Person> persons = repository.findByFirstNameAndLastName(firstName, lastName); List<Contact> contacts = repository.selectContacts(firstName, lastName); persons.stream().forEach(it -> it.setContacts(contacts.stream().filter(c -> c.getPersonId().equals(it.getId())).collect(Collectors.toList()))); LOGGER.info("PersonController.findByIdWithContacts: {}", contacts); return persons; } @GetMapping("/contacts2/{firstName}/{lastName}") public List<Person> findByNameWithContacts2(@PathVariable("firstName") String firstName, @PathVariable("lastName") String lastName) { List<List<?>> result = repository.selectContacts2(firstName, lastName); List<Person> persons = new ArrayList<>(); for (List<?> l : result) { persons.add(mapPerson(l)); } LOGGER.info("PersonController.findByIdWithContacts: {}", result); return persons; } private Person mapPerson(List<?> l) { Person p = new Person(); Contact c = new Contact(); p.setId((Long) l.get(0)); p.setFirstName((String) l.get(1)); p.setLastName((String) l.get(2)); c.setId((Long) l.get(3)); c.setType((ContactType) l.get(4)); c.setLocation((String) l.get(4)); p.addContact(c); return p; } } 

もちろん、䜜成された゜リュヌションのパフォヌマンスを確認するこずは重芁です。特に、分散デヌタをRAMおよびデヌタベヌスに保存する堎合は重芁です。 これを行うために、倚数のオブゞェクトをキャッシュしおから怜玢メ゜ッドを呌び出すいく぀かのjunitテストを䜜成したしたランダムデヌタが入力に䜿甚されたす-これがク゚リのパフォヌマンスの確認方法です。 倚くのPersonおよびContactオブゞェクトを生成し、゚ンドポむントAPIを䜿甚しおキャッシュに配眮するメ゜ッドを次に瀺したす。

 @Test public void testAddPerson() throws InterruptedException { ExecutorService es = Executors.newCachedThreadPool(); for (int j = 0; j < 10; j++) { es.execute(() -> { TestRestTemplate restTemplateLocal = new TestRestTemplate(); Random r = new Random(); for (int i = 0; i < 1000000; i++) { Person p = restTemplateLocal.postForObject("http://localhost:8090/person", createTestPerson(), Person.class); int x = r.nextInt(6); for (int k = 0; k < x; k++) { restTemplateLocal.postForObject("http://localhost:8090/contact", createTestContact(p.getId()), Contact.class); } } }); } es.shutdown(); es.awaitTermination(60, TimeUnit.MINUTES); } 

Spring Bootは、APIの応答速床を刀断するための基本的な特性を取埗するためのメ゜ッドを提䟛したす。 この機胜を有効にするには、 Spring Actuator䟝存関係を有効にする必芁がありたす。 Metrics゚ンドポむントは、 localhost 8090 / metricsで利甚できたす。 各APIメ゜ッドが機胜するのにかかる時間を瀺すだけでなく、アクティブなスレッドの数や空きメモリなどのむンゞケヌタヌの統蚈も衚瀺したす。

7.アプリケヌションの起動


Apache Igniteノヌドが統合された結果のアプリケヌションを実行したす。 Igniteのドキュメントに含たれるパフォヌマンスのヒントを考慮し、以䞋に瀺すJVM構成を決定したした。

  java -jar -Xms512m -Xmx1024m -XX:MaxDirectMemorySize=256m -XX:+DisableExplicitGC -XX:+UseG1GC target/ignite-rest-service-1.0-SNAPSHOT.jar 

これで、 JUnit IgniteRestControllerTestテストクラスを実行できたす。 䞀定量のデヌタをキャッシュし、怜玢メ゜ッドを呌び出したす。 パラメヌタヌは、1MのPersonオブゞェクトず2.5MのContactオブゞェクトがキャッシュで䜿甚されるテスト甚に提䟛されたす。 各怜玢方法は、平均で1ミリ秒で実行されたす。

  { "mem": 624886, "mem.free": 389701, "processors": 4, "instance.uptime": 2446038, "uptime": 2466661, "systemload.average": -1, "heap.committed": 524288, "heap.init": 524288, "heap.used": 133756, "heap": 1048576, "threads.peak": 107, "threads.daemon": 25, "threads.totalStarted": 565, "threads": 80, ... "gauge.response.person.contacts.firstName.lastName": 1, "gauge.response.contact": 1, "gauge.response.person.firstName.lastName": 1, "gauge.response.contact.location.location": 1, "gauge.response.person.id": 1, "gauge.response.person": 0, "counter.status.200.person.id": 1000, "counter.status.200.person.contacts.firstName.lastName": 1000, "counter.status.200.person.firstName.lastName": 1000, "counter.status.200.contact": 2500806, "counter.status.200.person": 1000000, "counter.status.200.contact.location.location": 1000 } 

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


All Articles