Рискну предположить, что среднестатистический читатель этой статьи с продуктом Apache Ignite не знаком. Хотя, возможно, слышал или даже читал статью на Хабре, в которой описывается один из возможных сценариев использования этого продукта. О принудительном использовании Ignite в качесте L2 кэша для Activiti я писал недавно. Возможно, узнав о том, что это написанный на Java open source продукт, позиционирующий себя как «высокопроизводительная, интегрированная и распределённая in-memory платформа для вычисления и обработки больших объёмов данных в реальном времени», обладающая, помимо прочего возможностью автоматического деплоймента вашего проекта на все ноды сложной топологии, вам захочется с ним познакомиться. Испытав такое желание, вы обнаружите, что Ignite документирован не то, чтобы совсем плохо, но и не очень хорошо. Есть туториал, кое-какой javadoc, но полного и целостного впечатления от ознакомления с этими источниками не возникает. В настоящей статье я попытаюсь восполнить этот пробел на основе собственного опыта познания Ignite, полученного преимущественно путём дебага. Возможно, в своих выводах и впечатлениях я буду не всегда прав, но таковы издержки метода. От читателя и тех, кто захочет повторить мой путь, требуется не так много, а именно знание Java 8 core, multithreading и Spring core.

В статье будет рассмотрен и препарирован пример класса «Hello World!» с использованием данной технологии.

Установка и запуск


Последней версией Ignite на момент написания статьи являлась 1.7.0 и исследовалась именно она (хотя на GitHub уже есть 1.8.0-SNAPSHOT). Получить Ignite можно двумя способами. Во-первых, в приложение следует добавить Maven зависимость на org.apache.ignite:ignite-core;LATEST и дополнительно на org.apache.ignite:ignite-spring:LATEST. Также можно скачать с сайта производителя собраный релиз, который состоит преимущественно из тех же библиотек, которые подключает Maven, или образ Docker. Поскольку я провожу свои исследования на Windows 7, мне вариант с докером не доступен, и я скачал бинарный дистрибутив. Его надо скачать и распаковать, папка, куда распаковывали, будет называться IGNITE_HOME. Далее я буду в целом следовать порядку изложения оригинального туториала, местами его неизбежно дублируя, но исключительно с целью удобства читателя.

Прежде всего надо отметить, что топология Ignite состоит из узлов двух типов, клиентов и серверов. В типовом случае нагрузка выполняется на серверах, а работающие на слабых машинах клиенты к ним подключаются и инициируют задачи. Клиентские и серверные узлы могут быть запущены внутри одной JVM, однако чаще всего узлы относятся к JVM 1:1. На одной физической (или виртуальной машине) можно запустить любое количество узлов. Далее мы проанализируем это отличие глубже. В этой терминологии наше «Hello World!»-приложение будет состоять из сервера и клиента, который пошлёт на сервер своё знаменитое сообщение.

Для получения узла Ignite используется утилитный класс Ignition. Из множества его методов нас пока интересуют пять перегруженные метода start. Один из них без параметров и запускает узел с параметрами по-умолчанию, нам он не подходит. Второй получает на вход сформированный конфигурационный объект типа IgniteConfiguration, а три других хотят получить спринговый кофигурационный файл, описывающий всё тот же объект IgniteConfiguration, в виде пути к ресурсу с xml-конфигурацией, URL на xml-конфигурацию или он же в виде InputStream. Из личного опыта не рекомендую использовать вариант с ручным формированием конфигурации через new IgniteConfiguration. Дело в том, что объект IgniteConfiguration является составным, у него много всяких вложенных объектов, которые тоже надо проинициализировать. И тут может скрываться подвох, поскольку кое-какие классы содержат приватные поля, инициализируемые исключительно путём инжекции. Например, в классе TcpDiscoveryJdbcIpFinder таким образом инжектируется логгер. Как известно, при создании объектов через new инжектирования не происходит, и логгер остаётся неинициализированным что, очевидно, приводит к NullPointerException в самый неподходящий момент. Так что не зависимо от ваших предпочтений надёжнее написать xml-конфигурацию и её использовать. Этот вариант хорош ещё тем, что этот конфиг можно использовать для запуска Ignite из командной строки. Примеры кофигов можно увидеть в дистрибутиве, в папке ${IGNITE_HOME}\examples\config\. Простейший конфиг приведён ниже:

Конфиг клиента
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
		
    <bean id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
        <property name="gridName" value="testGrid-client"/>
        <property name="clientMode" value="true"/>

        <property name="discoverySpi">
            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
                <property name="ipFinder">
                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
                        <property name="addresses">
                            <list>
                                <value>127.0.0.1:47500..47509</value>
                            </list>
                        </property>
                    </bean>
                </property>
				<property name="localAddress" value="localhost"/>
            </bean>
        </property>
		
        <property name="communicationSpi">
            <bean class="org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi">
				<property name="localAddress" value="localhost"/>
            </bean>
        </property>

	</bean>
</beans>


Здесь мы говорим о том, что создаём узел с именем «testGrid-client», что это клиент, и что он будет искать сервер в диапазоне адресов 127.0.0.1:47500..47509, то есть локально. Для сервера мы подготовим похожий конфиг:

Конфиг сервера
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
		
    <bean id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
        <property name="gridName" value="testGrid-server"/>
        <property name="clientMode" value="false"/>

        <property name="discoverySpi">
            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
                <property name="ipFinder">
                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
                        <property name="addresses">
                            <list>
                                <value>127.0.0.1:47500..47509</value>
                            </list>
                        </property>
                    </bean>
                </property>
				<property name="localAddress" value="localhost"/>
            </bean>
        </property>
		
        <property name="communicationSpi">
            <bean class="org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi">
				<property name="localAddress" value="localhost"/>
            </bean>
        </property>

	</bean>
</beans>


Сохраним серверный конфиг в файл test.xml и поместиим его в ${IGNITE_HOME}\examples\config. Чтобы запустить сервер, перейдём в папку ${IGNITE_HOME}\bin и выполним команду ignite.(bat|sh) examples\config\test.xml. Если не случится ексепшенов, то конфиг годный, и в конце должно появиться что-то вроде:



Выполненный командный файл полезно изучить. Помимо стандартных возможностей по установке переменных JVM, из него можно узнать о существовании системной переменной IGNITE_QUIET, управляющей подробностью логирования. Полный перечень системных переменных приводится в классе IgniteSystemProperties с расшифровкой; имеет смысл ознакомиться (оказывается, Ignite даже умеет проверять появление своих новых версий). Далее можно узнать, что за запуск из командной строки отвечает класс CommandLineStartup. Он тоже небезынтересен. Можно увидеть, что если вы работаете на OSX, то вам при старте за это выскочит попап-окошко. Мелочь, а не приятно — за что это только им такое счастье? Из интересного видно, что если в этот класс попасть без параметров, то включится интерактивный режим и вам будут предложены на выбор доступные конфиги, которые отыщет GridConfigurationFinder; он умеет искать в ${IGNITE_HOME}. Поскольку через командный файл мы без параметров стартовать не можем, то тут нам эта возможность не доступна. Но не расстраивайтесь, можно выполнить команду ${IGNITE_HOME}\bin\ignitevisorcmd.bat — это интерактивный мониторинг Ignite, в нём выполните команду open, и он выведет что-то такое:



Тут мы можем указать наш конфиг, в этом списке он под цифрой 6. Введя 6, мы подключимся к нашему серверу и увидим



Далее, мы можем ввести в консоль команду top и увидить нашу топологию:



Смотрим глубже


Возвращаясь к классу CommandLineStartup, можно обнаружить тоску разработчиков по алиасам для классов, которые им так нравятся в Scala: для краткости вызовов они создали класс G, пустой наследник класса Ignition. Ну ок, мы стартовали сервер, что дальше? Дальше запустим клиент. Типовой код для запуска инстанса выглядит примерно так:

Конфигурирование узла
@Configuration
public class IgniteProvider {
    private Log log = LogFactory.getLog(IgniteCacheAdapter.class);
    private final Ignite ignite;
    private boolean started = false;

    public IgniteProvider() {
        try {
            Ignition.ignite("testGrid-client");
            started = true;
        } catch (IgniteIllegalStateException e) {
            log.debug("Using the Ignite instance that has been already started.");
        }
        if (started)
            ignite = Ignition.ignite("testGrid-client");
        else {
            ignite = Ignition.start("ignite/example-hello.xml");
            ((TcpDiscoverySpi) ignite.configuration().getDiscoverySpi())
                    .getIpFinder()
                    .registerAddresses(Collections.singletonList(new InetSocketAddress("localhost", DFLT_PORT)));
        }
    }

    public Ignite getIgnite() {
        return ignite;
    }
}


Здесь мы проверяем, не запущен ли в данном JVM уже узел с таким именем, если запущен, то он хранится в не абы в чём, не в java.util.concurrent.ConcurrentHashMap, как кто-то, наверное подумал, а в org.jsr166.ConcurrentHashMap8. В чём их отличие даже боюсь предположить, надеюсь, что кто-нибудь в комментах просветит. А если узла ещё нет, он создаётся на основе конфига. Поскольку мы подключаемся как клиент, нам нужно найти сервер. В качестве способа обнаружения в конфиге указан TcpDiscoverySpi и TcpDiscoveryMulticastIpFinder, инициализируются эти классы и совершают свои поисковые манипуляции. Основные из них следующие.

В соответствии с нашими указанями, выбор между двумя имплементациями интерфейса TcpDiscoveryImpl совершается в пользу ClientImpl. Затем, если бы указали конфигурацию ssl, был бы поднят ssl-контекст — он бы потом пригодился для создания сокетов. Объекту TcpDiscoverySpi очень важно самоидентифицироваться, для этого мы в конфиге установили свойство «localAddress». Если бы мы его не установили, то получили бы org.apache.ignite.spi.IgniteSpiException: Failed to resolve local host to addresses: 0.0.0.0/0.0.0.0 Далее для внутренней самодиагностики регистрируются MBean'ы, то есть их можно использовать для мониторинга продукта. Затем в методе spiStart стартует выбранная имплементация. И клиент и сервер должны подключиться к топологии, однако клиент при этом блокируется до устанолвки соединения. В конфиге мыуказали диапазон портов для локалхоста, и каждый из них Ignite пытается зарезолвить. На каждый из этих адресов-портов клиент шлёт joinRequest. Вот в этом месте меня лично поджидало разочарование, поскольку предусмотрено взаимодействие только через сокеты и, например на основе JMS топологию построить нельзя. Обидно. Но ладно, на порту 47500, который является для Ignite портом по-умолчанию, я отдискаверил сервер. В ответ мы получаем первый hearthbeat сервера и на его основе обновляем соответсвующие метрики диагностики. В дальнейшем этот процес — поиска сервера и получения hearthbeat'ов будет происходить непрерывно. Возвращаемся к нашему визору и спрашиваем о состоянии топологии, и ответ соответствует нашим ожиданиям:



Обратите внимание на вывод консоли сервера:

[15:36:11] Topology snapshot [ver=7, servers=1, clients=1, CPUs=8, heap=7.1GB]
[15:37:11] Topology snapshot [ver=8, servers=1, clients=0, CPUs=8, heap=3.6GB]
[15:42:15] Topology snapshot [ver=9, servers=1, clients=1, CPUs=8, heap=7.1GB]
[15:42:24] Topology snapshot [ver=10, servers=1, clients=0, CPUs=8, heap=3.6GB]

Видно, что в какой-то момент подключился клиент, а потом отвалился — это потому что я был в дебаге, и он отвалился по таймауту. Отлично. Теперь можно поздороваться с миром. Для этого гайд предлагает использовать код вида

JUnit тест
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {IgniteProvider.class})
public class IgniteHelloWorld {

    @Autowired
    private IgniteProvider igniteProvider;

    @Test
    public void sendHelloTest() {
        Ignite ignite = igniteProvider.getIgnite();

        while(true) {
            try {
                ignite.compute().broadcast(() -> System.out.println("Hello World!"));
                Thread.sleep(1000);
            }
            catch (Exception ex) {}
        }
    }
}


Что он делает? Объект ignite представляет наш узел. Метод compute() для нашего клиента, сообразно его знанию о топологии, и с учётом его присоединённости, создаёт объект для распределённого вычисления. Метод broadcast асинхронно выполняет job, который он сконструировал из команды System.out.println(«Hello World!»). Результат мы на это получим достаточно неожиданный:

Неожиданный exception
Caused by: class org.apache.ignite.binary.BinaryInvalidTypeException: ru.kmorozov.ignite.test.IgniteHelloWorld
	at org.apache.ignite.internal.binary.BinaryContext.descriptorForTypeId(BinaryContext.java:671)
	at org.apache.ignite.internal.binary.BinaryUtils.doReadClass(BinaryUtils.java:1454)
	at org.apache.ignite.internal.binary.BinaryUtils.doReadClass(BinaryUtils.java:1392)
	at org.apache.ignite.internal.binary.BinaryReaderExImpl.readClass(BinaryReaderExImpl.java:369)
	at org.apache.ignite.internal.binary.BinaryFieldAccessor$DefaultFinalClassAccessor.readFixedType(BinaryFieldAccessor.java:828)
	at org.apache.ignite.internal.binary.BinaryFieldAccessor$DefaultFinalClassAccessor.read(BinaryFieldAccessor.java:639)
	at org.apache.ignite.internal.binary.BinaryClassDescriptor.read(BinaryClassDescriptor.java:776)
	at org.apache.ignite.internal.binary.BinaryReaderExImpl.deserialize(BinaryReaderExImpl.java:1481)
	at org.apache.ignite.internal.binary.BinaryUtils.doReadObject(BinaryUtils.java:1608)
	at org.apache.ignite.internal.binary.BinaryReaderExImpl.readObject(BinaryReaderExImpl.java:1123)
	at org.apache.ignite.internal.processors.closure.GridClosureProcessor$C2V2.readBinary(GridClosureProcessor.java:2023)
	at org.apache.ignite.internal.binary.BinaryClassDescriptor.read(BinaryClassDescriptor.java:766)
	at org.apache.ignite.internal.binary.BinaryReaderExImpl.deserialize(BinaryReaderExImpl.java:1481)
	at org.apache.ignite.internal.binary.GridBinaryMarshaller.deserialize(GridBinaryMarshaller.java:298)
	at org.apache.ignite.internal.binary.BinaryMarshaller.unmarshal(BinaryMarshaller.java:109)
	at org.apache.ignite.internal.processors.job.GridJobWorker.initialize(GridJobWorker.java:409)
	... 9 more
Caused by: java.lang.ClassNotFoundException: ru.kmorozov.ignite.test.IgniteHelloWorld
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at org.apache.ignite.internal.util.IgniteUtils.forName(IgniteUtils.java:8350)
	at org.apache.ignite.internal.MarshallerContextAdapter.getClass(MarshallerContextAdapter.java:185)
	at org.apache.ignite.internal.binary.BinaryContext.descriptorForTypeId(BinaryContext.java:662)
	... 24 more


Этот же ексепшен мы увидим на стороне сервера. Это не совсем то, чего бы хотелось. Так произошло, потому что мы не включили удивительной крутизны фичу, P2P class loading или Zero Deployment. Этот момент хорошо разъяснён в аутентичном гайде, поэтому повторяться не буду. Смысл в том, что все наши классы, и lambda-замыкания тоже, должны быть пропагированы на все узлы. Альтернативой является подкладывания jar'а с классами в папку ${IGNITE_HOME}\libs. Но включим фичу, добавив в конфиги строку

<property name="peerClassLoadingEnabled" value="true"/>

Вносим изменение, перестартовываем сервер. И ура!

[16:21:11] Topology snapshot [ver=6, servers=1, clients=0, CPUs=8, heap=3.6GB]
[16:21:48] Topology snapshot [ver=7, servers=1, clients=1, CPUs=8, heap=7.1GB]
Hello World!
Hello World!
Hello World!

Выводы


Предсказуемым образом простенький пример выявил не то чтобы бездны, но интересные подробности. Я думаю, о них можно будет рассказать в следующих сериях, равно как других фичах Ignite, ещё незатронутых.

Ссылки


» Код тестового примера на GitHub
Поделиться с друзьями
-->

Комментарии (14)


  1. A_Gura
    19.09.2016 20:26

    он хранится в не абы в чём, не в java.util.concurrent.ConcurrentHashMap, как кто-то, наверное подумал, а в org.jsr166.ConcurrentHashMap8. В чём их отличие даже боюсь предположить, надеюсь, что кто-нибудь в комментах просветит.


    Ignite написан на Java 7, тогда как класс ConcurrentHashMap в Java 8 получил ряд улучшений. Вот его и перенесли из jsr166 в кодовую базу Ignite и немного модифицировали. Благо лицензия CC0 это позволяет.


    1. kmorozov
      19.09.2016 20:44

      Спасибо!


  1. javax
    19.09.2016 20:53
    +2

    Не хватает объяснения или примера что вообще он умеет делать, для каких задач подходит


    1. kmorozov
      19.09.2016 21:20

      Этой статьёй, и если дело пойдёт, в последующих, я попытаюсь дать для себя и читателей информацию для ответа на этот вопрос. Сейчас этот ответ мне неизвестен. Рекламу ГридГейна цитировать не буду. Я не эксперт в этом продукте, я в нём копаюсь и изучаю в надежде, что не может же вся эта крутизна ни на что не быть годной.


    1. devozerov
      19.09.2016 22:10
      +1

      Начать можно с официального сайта: http://ignite.apache.org/

      На данный момент описать продукт в двух словах проблематично. Официальная формулировка — data fabric, то есть интегрированная платформа для распределенных вычислений и работы с данными.

      Многие (если не все) продукты такого класса лет 10 назад начинали с простого use case: распределенный кэш и map-reduce, горизонтальное масштабирование. За годы требования бизнеса и конкуренция возросли, поэтому они трансформировались в эдакие универсальные конструкторы для работы с данными.

      Ключевые фичи конкретно Ignite: distributed cache, распределенный SQL поверх данных в памяти (+ JDBC и ODBC драйвера), map-reduce, стриминг, распределенная файловая система, множество интеграций (web sessions, hibernate L2 cache, ...), API для .NET и C++, и т. д…


      1. javax
        19.09.2016 22:11

        Спасибо! Звучит интересно


      1. SerrNovik
        19.09.2016 23:36

        Computational grid — распределенные и масштабируемые вычисления. Не только map-reduce но и любые отдельные функции.


  1. Ermak
    20.09.2016 00:53

    Спасибо за статью. Ignite не пробовсал еще, но работаю со Spark. Пара вопросов:
    1. Apache Spark vs Apache Ignite — оба продукта предназначены для распределенных вычислений. В чем отличия, достоинства и недостатки?
    2. А в чем проблема запусакть докер на Win7? Пользуюсь Boot2Docker чтобы запускать докер на Win7.


    1. kmorozov
      20.09.2016 06:41

      Я думал, он только под Win10, попробую, спасибо.


    1. shamim
      23.09.2016 19:54

      Добрый вечер!
      >>> 1. Apache Spark vs Apache Ignite — оба продукта предназначены для распределенных вычислений. В чем отличия, достоинства и недостатки?

      Тут есть небольшое отличие, У Ignite распредленное вычислении на уровне сервисов (так называемое service/compute grid). В качестве примера можно вспомнить такие часто встречаемые задачи как генерирование/конвертация документов, криптография, конвертация изображений. Также типовой задачей является предоставление постоянно запущенных сервисов. Service grid apache ignite позволяет прозрачно для программиста и администратора запускать сервисы в кластере ignite, предоставляет эффективный протокол доступа, контролирует их работоспособность.


      1. shamim
        23.09.2016 19:58

        Можно прочитать небольшой обзор (sample chapter) из книги "High performance in-memory computing with Apache Ignite".


  1. ilshat_gainanov
    20.09.2016 16:26

    Хорошая статья, сейчас тоже занимаюсь изучением возможностей Apache Ignite. Для себя уяснил, что многие моменты, недосказанные в документации, можно узнать на форуме, ибо имеется довольно дружелюбное и активное комьюнити.


    1. devozerov
      21.09.2016 00:16

      Ильшат, не подскажите чего именно, на ваш взгляд, не хватало в документации?


      1. ilshat_gainanov
        21.09.2016 12:26

        Владимир, ну, по мелочам бывают недосказанности в основном.
        Сейчас разбираюсь с транзакциями, JTA, Affinity и Join`ами между кэшами, хотелось бы в принципе более подробного описания, примеров, может, более приближенных к реалиям.
        Ну, а может, у меня возникают трудности в силу неопытности:)