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

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

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

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


В части методики тестирования я не стал изобретать велосипед, и взял ту, которая описана для cache2k. Она состоит в том, что с помощью основанной на JMH библиотеки производится сравнение производительности выполнения ряда типовых операций:
  • Наполнение кэша в несколько потоков
  • Производительность в режиме 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×64
  • JVM flags: -server -Xmx2G
  • Apache Ignite 1.7.0

Работа кэша Apache Ignite исследовалась в нескольких режимах, различающихся по топологии (тут рекомендуется вспомнить базовые понятия о топологии Apache Ignite) и распределению нагрузки:
  • Локальный кэш (cacheMode=LOCAL) на серверном узле;
  • Распределённый каш на 1 машине (cacheMode=PARTITIONED, FULL_ASYNC), сервер-сервер;

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



		
    
        
        
        

        
            
                
                    
                    
                    
                    
                
            
        

        
            
                
                    
                        
                            
                                127.0.0.1:47520..47529
                            
                        
                    
                
				
            
        

        
            
                
            
        
	



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

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

1b33f52403424a6987f0bd345b7d4367.JPG

Со 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 create(int _maxElements) {
        return new MyBenchmarkCache(getIgniteCache());
    }

    static class MyBenchmarkCache extends BenchmarkCache {

        IgniteCache cache;

        MyBenchmarkCache(IgniteCache 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 \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:\tmp\1.json. Настройки JMH взяты из оригинального бенчмарка, мы их обсуждать тут не будем. Параметр »-t 1» указывает количество потоков, которыми мы работаем с кэшем. Памяти я указывал 14Gb, на всякий случай.»-f 2» означает, что для исполнения теста будет подниматься два форка JVM, это способствует резкому уменьшению доверительного интервала (столбец «error» в выводе JMH).

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


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

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

ConcurrentHashMap:

ba8d51f1c2ae401da4253ee223cfaf54.JPG 78e835c0a0894735a1a5ed9e34aaae72.JPG 5c9c9f3dff3d43b7b25bffd0225a81b0.JPG

cache2k:
3874f5b4fbe64bd2aefa324e9467e448.JPG c944a6568b794c8d9e95413fdccfb1b7.JPG 2aed09a5a21e45c89a1b8cf3df75da91.JPG

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

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

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

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


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

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

0794d07736af4ca1b3e79d5816c76461.JPG 3140f0dbff7a4f558cea8e61acce4fdb.JPG 1216fafb4cb749efad705f03d3859cb2.JPG

То есть, 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.

Ссылки


  • Оригинальная методика тестирования
  • Фреймворк тестирования на GitHub

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

© Habrahabr.ru