Знакомство с Apache Ignite: первые шаги +10


Рискну предположить, что среднестатистический читатель этой статьи с продуктом 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
-->


К сожалению, не доступен сервер mySQL