- PVSM.RU - https://www.pvsm.ru -

Измеряем производительность кэша Apache Ignite

После того, как в предыдущих статьях данной серии обзоров распределённого Java-фреймворка Apache Ignite [1] мы сделали первые шаги [2], познакомились с основными принципами построения топологии [3] и даже сделали стартер для Spring Boot [4], неизбежно встаёт вопрос о кэшировании, которое является одной из основных функций Ignite. Прежде всего, хотелось бы понять, нужно ли оно, когда библиотек для кэширования на Java и так полным-полно. Тем, что предоставляется реализация стандарта JCache (JSR 107) и возможность распределённого кэширования в наше время удивить сложно. Поэтому прежде чем (или вместо того чтобы) рассматривать функциональные возможности кэша Apache Ignite, мне бы хотелось посмотреть, насколько он быстр.

Измеряем производительность кэша Apache Ignite - 1

Для исследования применялся бенчмарк cache2k-benchmark [5], разработанный с целью доказательства того, что у библиотеки cache2k [6] кэш самый быстрый. Вот заодно и проверим. Настоящая статья не преследует цель всеобъемлющего тестирования производительности, или хотя бы научно достоверного, пусть этим занимаются разработчики Apache Ignite. Мы просто посмотрим на порядок величин, основные особенности и взаимное расположение в рейтинге, в котором будут ещё cache2k и нативный кэш на ConcurrentHashMap.

Методика тестирования

В части методики тестирования я не стал изобретать велосипед, и взял ту, которая описана для cache2k [7]. Она состоит в том, что с помощью основанной на JMH [8] библиотеки производится сравнение производительности выполнения ряда типовых операций:

  • Наполнение кэша в несколько потоков
  • Производительность в режиме read-only

В качестве эталона в методике рассматриваются значения, получаемые для реализации кэша на основе ConcurrentHashMap, поскольку предполагается, что быстрее некуда. Соответственно во всех номинациях борьба идёт за второе место. В cache2k-benchmark (далее CB) реализованы сценарии для cache2k и ряда других провайдеров: Caffeine, EhCache, Guava, Infinispan, TCache, а также нативная реализация на основе ConcurrentHashMap. В CB реализованы и другие бенчмарки, но мы ограничимся этими двумя.

Измерения производились в следующих условиях:

  • JDK 1.8.0_45
  • JMH 1.11.3
  • Intel i7-6700 3.40Ghz 16Gb RAM
  • Windows 7 x64
  • JVM flags: -server -Xmx2G
  • Apache Ignite 1.7.0

Работа кэша Apache Ignite исследовалась в нескольких режимах, различающихся по топологии (тут рекомендуется вспомнить базовые понятия о топологии Apache Ignite [3]) и распределению нагрузки:

  • Локальный кэш (cacheMode=LOCAL) на серверном узле;
  • Распределённый кэш на 1 машине (cacheMode=PARTITIONED, FULL_ASYNC), сервер-сервер;

Согласно требованиям CB был реализован класс IgniteCacheFactory (код доступен в GitHub [9], основан на форке CB). Сервер и клиент создаются со следующими настройками:

Конфигурация сервера

<?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-server" class="org.apache.ignite.configuration.IgniteConfiguration">
        <property name="gridName" value="testGrid"/>
        <property name="clientMode" value="false"/>
        <property name="peerClassLoadingEnabled" value="false"/>

        <property name="cacheConfiguration">
            <list>
                <bean class="org.apache.ignite.configuration.CacheConfiguration">
                    <property name="name" value="testCache"/>
                    <property name="cacheMode" value="LOCAL"/>
                    <property name="statisticsEnabled" value="false" />
                    <property name="writeSynchronizationMode" value="FULL_ASYNC"/>
                </bean>
            </list>
        </property>

        <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:47520..47529</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>

Важно, чтобы настройки кэша для клиента и сервера были одинаковыми.

Сервер будет создаваться из командной строки вне теста с помощью той же JVM с опциями -Xms1g -Xmx14g -server -XX:+AggressiveOpts -XX:MaxMetaspaceSize=256m, то есть я ему даю почти всю память. Запустим сервер и подключимся к нему визором (за подробностями отсылаю ко второй статье серии [3]). С помощью команды cache убеждаемся, что кэш существует и девственно чист:

Измеряем производительность кэша Apache Ignite - 2

Со CB подключаемся с помощью класса

Фабрика кэша для бенчмарка

public class IgniteCacheFactory extends BenchmarkCacheFactory {

    static final String CACHE_NAME = "testCache";
    static IgniteCache cache;
    static Ignite ignite;

    static synchronized IgniteCache getIgniteCache() {
        if (ignite == null)
            ignite = Ignition.ignite("testGrid");

        if (cache == null)
            cache = ignite.getOrCreateCache(CACHE_NAME);

        return cache;
    }

    @Override
    public BenchmarkCache<Integer, Integer> create(int _maxElements) {
        return new MyBenchmarkCache(getIgniteCache());
    }

    static class MyBenchmarkCache extends BenchmarkCache<Integer, Integer> {

        IgniteCache<Integer, Integer> cache;

        MyBenchmarkCache(IgniteCache<Integer, Integer> cache) {
            this.cache = cache;
        }

        @Override
        public Integer getIfPresent(final Integer key) {
            return cache.get(key);
        }

        @Override
        public void put(Integer key, Integer value) {
            cache.put(key, value);
        }

        @Override
        public void destroy() {
            cache.destroy();
        }

        @Override
        public int getCacheSize() {
            return cache.localSize();
        }

        @Override
        public String getStatistics() {
            return cache.toString() + ": size=" + cache.size();
        }
    }
}

Здесь мы подключаемся в режиме клиента к нашему серверу и берём у него кэш. Важно по завершении теста остановить клиент, иначе JMH ругается на то, что по завершении теста остались работающие потоки — Ignite для своего функционирования создаёт их множество. Также прошу отметить, что в зачёт идёт время на удаление кэша после каждой итерации. Будем считать это издержками метода исследования, то есть мы смотрим не только производительность самого кэша, но и затраты на его администрирование.

Класс бенчмарка

@State(Scope.Benchmark)
public class IgnitePopulateParallelOnceBenchmark extends PopulateParallelOnceBenchmark {
    Ignite ignite;

    {
        if (ignite == null)
            ignite = Ignition.start("ignite/ignite-cache.xml");
    }

    @TearDown(Level.Trial)
    public void destroy() {
        if (ignite != null) {
            ignite.close();
            ignite = null;
        }
    }
}

Результаты

После сборки проекта через mvn clean install можно запускать тесты, например командой
java -jar <BENCHMARK_HOME>benchmarks.jar PopulateParallelOnceBenchmark -jvmArgs "-server -Xmx14G -XX:+UseG1GC -XX:+UseBiasedLocking -XX:+UseCompressedOops" -gc true -f 2 -wi 3 -w 5s -i 3 -r 30s -t 2 -p cacheFactory=org.cache2k.benchmark.thirdparty.IgniteCacheFactory -rf json -rff e:tmp1.json. Настройки JMH взяты из оригинального бенчмарка, мы их обсуждать тут не будем. Параметр "-t 1" указывает количество потоков, которыми мы работаем с кэшем. Памяти я указывал 14Gb, на всякий случай. "-f 2" означает, что для исполнения теста будет подниматься два форка JVM, это способствует резкому уменьшению доверительного интервала (столбец «error» в выводе JMH).

Наполнение кэша в несколько потоков

Сначала прогоним тест для Apache Ignite с cacheMode=LOCAL. Поскольку в этом случае во взаимодействии с сервером никакого смысла нет, узел для тестирования подымем в серверном режиме и не будем ни к кому подключаться. Измеряется время, которое потребовалось на то, чтобы закэшировать числа от 1 до 1млн, 2млн, 4млн, 8млн. Для количества потоков 1, 4 и 8 (у меня 8-ядерный процессор) результаты будут такими:

Измеряем производительность кэша Apache Ignite - 3 Измеряем производительность кэша Apache Ignite - 4 Измеряем производительность кэша Apache Ignite - 5

Видим, что если 4 потока быстрее 1 потока примерно вдвое, то добавление ещё 4 потоков даёт выигрыш примерно 20%. То есть масштабирование нелинейное. Для сравнения посмотрим, что покажут ConcurrentHashMap и cache2k.

ConcurrentHashMap:

Измеряем производительность кэша Apache Ignite - 6 Измеряем производительность кэша Apache Ignite - 7 Измеряем производительность кэша Apache Ignite - 8

cache2k:

Измеряем производительность кэша Apache Ignite - 9 Измеряем производительность кэша Apache Ignite - 10 Измеряем производительность кэша Apache Ignite - 11

Таким образом, в локальном режиме при вставке кэш Ignite примерно в 10 раз медленнее ConcurrentHashMap и в 4-5 раз медленнее cache2k. Далее попробуем оценить, какой оверхед даёт партиционирование кэша между двумя серверными узлами на одной машине (то есть кэш будет делиться пополам) — разработчики Ignite предприняли шаги, чтобы он не был гигантским. Они, например, используют собственную сериализацию, которая по их словам в 20 раз быстрее родной. Во время исполнения теста можно посмотреть визор, теперь в этом есть смысл, у нас топология:

Измеряем производительность кэша Apache Ignite - 12

По окончании мы видим вот такие душераздирающие цифры:

Измеряем производительность кэша Apache Ignite - 13

То есть партиционирование кэша нам обошлось весьма не дёшево, раз в 10 стало хуже. Режим кэша REPLICATED не исследовался, в нём данные бы хранились в обоих узлах.

Только чтение

Чтобы не усложнять картину множеством параметров, этот тест проведём в 4 потока, Ignite запустим только локально. Здесь используем ReadOnlyBenchmark [10]. Кэш наполняется 100k записями и различным случайным образом из него выбираются значения, с различным hit rate. Измеряется число операций в секунду.

Вот данные Cache2k/ConcurrentHashMap/Ignite:

Измеряем производительность кэша Apache Ignite - 14 Измеряем производительность кэша Apache Ignite - 15 Измеряем производительность кэша Apache Ignite - 16

То есть, Cache2k в 1.5-2.5 раза хуже ConcurrentHashMap, а Ignite ещё в 2-3 раза хуже.

Выводы

Таким образом, Ignite мягко говоря не потрясает скоростью своего кэширования. Попытаюсь заранее ответить на возможные упрёки:

  • Я просто не умею его готовить, и если Ignite оттюнить, то будет лучше. Что ж всё, если оттюнить, будет лучше. Исследовалась работа в дефолтной конфигурации, в 90% случаев она и в продакшене будет такая же;
  • Яблоки и бананы, продукты разного класса, микроскопом гвозди и т.п. Хотя, возможно, следовало сравнивать с чем-то более навороченным типа Inifinispan, от Ignite в данном исследовании никто не требовал невозможного;
  • Устранить overhead, вынести за скобки дорогие операции поднятия узла и создания/удаления кэша, уменьшить частоту hearthbeat и т.п. Но мы же не коня в вакууме меряем?;
  • Этот продукт не предназначен для локального использования, нужно enterprise-оборудование. Возможно, но это только размажет весь overhead по топологии, а тут мы его увидели весь разом. Во время тестирования %% CPU и памяти ни разу не достигали 100%;
  • Такова специфика продукта. Можно посмотреть на приведённые результаты как на очень достойные, учитывая, невероятную мощь Ignite. Необходимо учитывать, что кэширование осуществляется в другой поток, через сокеты и т.д. С другой стороны, Ignite по-другому и не имеет.

Ну и так далее. В общем, по моему впечатлению, Ignite следует использовать как архитектурный каркас для распределённых приложений, а не как источник получения производительности. Хотя, возможно, он способен ускорить что-то ещё более тормознутое. IMHO, разумеется.

Приглашаю делиться своими наблюдениями о производительности Ignite.

Ссылки

Автор: kmorozov

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/193467

Ссылки в тексте:

[1] Apache Ignite: https://ignite.apache.org/

[2] первые шаги: https://habrahabr.ru/post/310334/

[3] построения топологии: https://habrahabr.ru/post/310464/

[4] сделали стартер для Spring Boot: https://habrahabr.ru/post/310672/

[5] cache2k-benchmark: https://github.com/cache2k/cache2k-benchmark

[6] cache2k: http://cache2k.org/

[7] описана для cache2k: http://cruftex.net/2016/03/16/Java-Caching-Benchmarks-2016-Part-1.html

[8] JMH: http://openjdk.java.net/projects/code-tools/jmh/

[9] код доступен в GitHub: https://github.com/kvmorozov/cache2k-benchmark

[10] ReadOnlyBenchmark: https://github.com/cache2k/cache2k-benchmark/blob/fa38ccfa43a7b83bcb4e4e8f28b2ba293fc30747/jmh-suite/src/main/java/org/cache2k/benchmark/jmh/suite/noEviction/symmetrical/ReadOnlyBenchmark.java

[11] Источник: https://habrahabr.ru/post/311140/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best