Доступ к памяти намного быстрее, чем дисковый ввод-вывод, поэтому многие из нас ожидают получить поразительные преимущества в производительности, просто развернув распределенный кластер в памяти и начав читать данные с него. Однако иногда мы упускаем из виду тот факт, что сеть связывает узлы кластера с нашими приложениями, и это может быстро уменьшить положительный эффект наличия кластера в памяти, если много данных передается непрерывно по проводам.
При этом использование надлежащих шаблонов доступа к данным, обеспечиваемых технологиями распределенной памяти, может свести на нет эффект задержки в сети. В этой статье мы используем API-интерфейсы вычислительной платформы Apache Ignite в памяти, чтобы увидеть, как изменяется производительность нашего приложения, если мы оказываем меньшее давление на каналы связи. Конечная цель заключается в том, чтобы иметь возможность развертывать горизонтально масштабируемые кластеры в памяти, которые могут подключаться к пулу оперативной памяти и процессорам, распределенным по всем машинам, с минимальным влиянием сети.
Проведение простого эксперимента
Для простоты предположим, что в нашем кластере Apache Ignite в памяти хранятся тысячи записей, и приложению необходимо рассчитать самую высокую температуру и самое длинное расстояние в наборе данных. Необходимо сравнить три API-интерфейса, чтобы показать, как изменяется производительность при минимальном использовании сети: отдельные вызовы значения ключа, массовые чтения значения ключа и совместные вычисления.
Для этого эксперимента подходит местный ноутбук. Таким образом, я развернул на своем компьютере двухузловой кластер Ignite и запустил этот пример с более чем 200 000 записей, предварительно загруженных на узлы (MacBook Pro начала 2015 года с двухъядерным процессором Intel Core i5 с тактовой частотой 2,7 ГГц и DDR3 с частотой 867 ГБ 1867 МГц и 8 ГБ). Эти два узла кластера и приложение взаимодействовали через петлевой сетевой интерфейс и конкурировали за ресурсы ОЗУ и ЦП. Если мы проведем тот же тест в действительно распределенной среде, то разница между сравниваемыми API будет более заметной. Я рекомендую вам поэкспериментировать с другими конфигурациями развертывания, следуя инструкциям, приведенным в примере.
Вам также может понравиться:
Oracle In-Memory на практике .
Подчеркивая сеть с тысячами вызовов по значению
Мы начинаем с API-интерфейсов ключ-значение, которые извлекают каждую запись по одной из двух узлов. Ignite предоставляет стандартный cache.get(key)
метод API для этого (проверьте calcByPullingDataFromCluster
на полную реализацию):
Джава
1
for (int i = 0; i < RECORDS_CNT; i++) {
2
SampleRecord record = recordsCache.get(i);
3
if (record.getDistance() > longestDistance)
5
longestDistance = record.getDistance();
6
if (record.getTemperature() > highestTemp)
8
highestTemp = record.getTemperature();
9
// Running other custom logic...
11
}
То, что мы делаем здесь, можно рассматривать как грубый подход, если приложение считывает все 200 000 записей и выполняет те же или более сетевых обходов. Неудивительно, что приложению потребовалось ~ 35 секунд для завершения расчета. Если этот метод доступа к данным выбран для аналогичных вычислений, мы можем вообще не выиграть, сохраняя данные в ОЗУ и на диске, так как множество записей передается по сети.
Ускорение за счет сокращения числа сетевых обращений
Первая очевидная оптимизация для нашего эксперимента заключается в сокращении числа сетевых обращений между узлами сервера Ignite и приложением. У Ignite есть cache.getAll(keys)
версия API-интерфейсов ключ-значение, которая осуществляет массовый запрос данных. В следующем фрагменте кода показано, как использовать API для нашей задачи (полную реализацию можно найти в calcByPullingDataInBulkFromCluster
методе):
Джава
xxxxxxxxxx
1
Collection<SampleRecord> records = recordsCache.getAll(keys).values();
2
Iterator<SampleRecord> recordsIterator = records.iterator();
4
while (recordsIterator.hasNext()) {
6
// Calculating highest temperature and longest distance
7
}
При таком подходе наши вычисления завершаются за ~ 5 секунд, что в 5 раз быстрее по сравнению с отдельными вызовами ключ-значение, которые использовались ранее. Приложение по-прежнему считывает все 200 000 записей с узлов сервера, но делает это за несколько сетевых обращений. Ignite cache.getAll(keys)
делает эту оптимизацию для нас - когда мы передаем первичные ключи, Ignite сначала сопоставляет ключи с узлами сервера, на которых хранятся записи, а затем подключается к узлам, считывающим данные в большом количестве.
Устранение влияния сети с помощью совмещенных вычислений
Наконец, давайте посмотрим, что произойдет, если мы прекратим тянуть 200 000 записей с узлов сервера в приложение. С помощью API вычислений Ignite мы можем объединить наши вычисления в задачу вычислений Ignite, которая будет выполняться на узлах сервера поверх их локального набора данных. Приложение только получает результаты от серверов и больше не извлекает фактические данные; таким образом, использование сети практически не используется (полная реализация может быть найдена в calcByComputeTask
методе):
Джава
xxxxxxxxxx
1
// Step #1: Application sends the compute task
2
Collection<Object[]> resultsFromServers = compute.broadcast(new IgniteCallable<Object[]>() {
4
5
public Object[] call() throws Exception {
6
// Step #2: Calculating the highest temperature
7
// and longest distance on each server node
8
}
9
}
10
// Step #3: Application selects the highest temperature
12
// and longest distance by checking results from servers
13
Этот совместный подход, основанный на вычислениях, выполняется за ~ 1 секунду, что в 5 раз быстрее, чем cache.getAll(keys)
решение, и в 35 раз быстрее, чем выдача отдельных запросов значения ключа. Более того, если мы загрузим в кластер в X раз больше данных, подход, основанный на вычислениях, будет продолжать линейно масштабироваться, в то время как cache.getAll(keys)
будет замедляться.
Должны ли мы придерживаться совместного размещения вычислений?
Цель этого эксперимента состояла в том, чтобы показать, что с распределенными системами в памяти производительность наших приложений может сильно различаться в зависимости от того, как мы обращаемся к распределенным наборам данных, пока сеть склеивает кластер и приложения.
Этот эксперимент не лишает вас возможности использовать API-интерфейсы со значением ключа или придерживаться только совмещенных вычислений. Если приложению необходимо прочитать отдельную запись и вернуть ее конечному пользователю или объединить два набора данных, продолжайте вызовы значения ключа или запросы SQL.
Если логика более сложная или требует большого объема данных, рассмотрите вычислительные API для устранения или уменьшения сетевого трафика. Короче говоря, знайте API-интерфейсы, которые предоставляет технология в памяти, и выберите те, которые будут наиболее эффективными для конкретной задачи.