Рекомендации по оптимизации производительности Apache Phoenix

Наиболее важным аспектом производительности Apache Phoenix является оптимизация базовой реализации Apache HBase. Phoenix создает реляционную модель данных поверх HBase, которая преобразует запросы SQL в операции HBase, например проверки. Структура схемы таблицы, выбор и упорядочение полей в первичном ключе и использование индексов влияют на производительность Phoenix.

Структура схемы таблицы

При создании таблицы в Phoenix она сохраняется в таблице HBase. Таблица HBase содержит группы столбцов (семейства столбцов), используемые вместе. Строка в таблице Phoenix - это строка в таблице HBase, где каждая строка состоит из версионных ячеек, связанных с одним или несколькими столбцами. Логически, одна строка в HBase представляет собой коллекцию пар "ключ — значение", каждая из которых имеет одно и то же значение rowkey. То есть каждая пара "ключ — значение" имеет атрибут rowkey, а его значение одинаково для определенной строки.

Структура схемы таблицы Phoenix включает в себя структуру первичного ключа, семьи столбцов, отдельного столбца и способ секционирования данных.

Структура первичного ключа

Первичный ключ, определенный в таблице Phoenix, определяет, как данные хранятся в rowkey базовой таблицы HBase. В HBase получить доступ к определенной строке можно только через rowkey. Кроме того, данные, хранящиеся в таблице HBase, сортируются по rowkey. Phoenix создает значение rowkey, объединяя значения всех столбцов в строке записи в том порядке, в котором они определены в первичном ключе.

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

rowkey адрес телефон Имя lastName
1000 1111 Сан Габриэль Др. 1-425-000-0002 Джон Доул
8396 5415 Сан-Гэбриел Др. 1-230-555-0191 Калвин Raji

Однако, если часто запрашивать по фамилии (lastName), первичный ключ может работать неправильно, так как для каждого запроса требуется полное сканирование таблицы для чтения значения каждой фамилии. Вместо этого можно определить первичный ключ для столбцов фамилии (lastName), имени (firstName) и номера социального обеспечения (socialSecurityNum). Последний столбец позволяет различать двух жителей с одним адресом и одним именем, например отца и сына.

rowkey адрес телефон Имя lastName СНИЛС
1000 1111 Сан Габриэль Др. 1-425-000-0002 Джон Dole 111
8396 Улица Сан Габриэль, д. 5415 1-230-555-0191 Калвин Раджи 222

При использовании нового первичного ключа ключи строк, созданные Phoenix, будут такими:

rowkey адрес телефон имя lastName номер социального страхования
Dzhon-Dol-111 1111 Сан Габриэль Др. 1-425-000-0002 Джон Dole 111
Раджи-Кальвин-222 5415 Сан Габриэль Др. 1-230-555-0191 Кельвин Раджи 222

В первой строке данной таблицы данные для ключа строки представлены, как показано.

rowkey ключ значение
Dole-John-111 адрес 1111 Сан Габриэль Др.
Доу-Джон-111 телефон 1-425-000-0002
Dole-John-111 имя Джон
Dole-John-111 lastName Dole
Dole-John-111 номер социального страхования 111

Теперь этот rowkey хранит дубликат данных. Учитывайте размер и количество столбцов, включенных в первичный ключ, так как это значение добавлено в каждую ячейку в базовой таблице HBase.

Кроме того, если первичный ключ содержит значения, которые монотонно возрастают, необходимо создать таблицу с предварительным разделением на группы, чтобы избежать точек перезагрузки записи (см. раздел Секционирование данных).

Структура семейства столбцов

Если доступ к некоторым столбцам осуществляется чаще, чем к другим, нужно создать несколько семейств столбцов для разделения часто и редко используемых столбцов.

Кроме того, если к определенным столбцам осуществляется совместный доступ, их следует поместить в одно семейство столбцов.

Структура столбца

  • Держите столбцы типа VARCHAR размером до 1 МБ из-за затрат на операции ввода-вывода, связанных с большими столбцами. При обработке запросов HBase материализует ячейки полностью перед отправкой их клиенту, а клиент получает их в полном объеме, прежде чем передавать их в код приложения.
  • Храните значения столбцов в компактном формате, например Protobuf, Avro, MsgPack или BSON. Формат JSON не рекомендуется (размер файла в таком формате будет больше).
  • Рассмотрите возможность сжатия данных перед сохранением для сокращения задержки и стоимости операций ввода и вывода.

Секционирование данных

Phoenix позволяет вам контролировать количество регионов, в которых распределяются ваши данные, что может значительно увеличить производительность при операциях чтения и записи. При создании таблицы Phoenix можно либо добавить соль, либо предварительно разделить данные.

Чтобы посолить таблицу во время создания, укажите число соляных контейнеров.

CREATE TABLE CONTACTS (...) SALT_BUCKETS = 16

При этом таблица разделяется по значениям первичных ключей, которые выбираются автоматически.

Чтобы контролировать, где происходит разделение таблицы, можно предварительно разбить таблицу, указав диапазон значений, по которому происходит разделение. Например, чтобы разделить таблицу по трем регионам:

CREATE TABLE CONTACTS (...) SPLIT ON ('CS','EU','NA')

Структура индекса

Индекс Phoenix — это таблица HBase, в которой хранятся копии некоторых или всех данных индексированной таблицы. Индекс повышает производительность отдельных типов запросов.

Если при наличии нескольких определенных индексов запросить таблицу, Phoenix автоматически выберет лучший индекс для запроса. Первичный индекс создается автоматически в зависимости от выбранных первичных ключей.

Для предполагаемых запросов можно также создать вторичные индексы путем указания их столбцов.

При проектировании индексов:

  • создавайте только необходимые индексы;
  • ограничьте число индексов в часто обновляемых таблицах. Обновления таблицы преобразуются в записи как в основной таблице, так и в таблицах индексов.

Создание вторичных индексов

Вторичные индексы могут повысить производительность операций чтения: вместо полного сканирования таблицы выполняется конкретный поисковой запрос. Однако при этом увеличивается пространство для хранения и ухудшается скорость записи. Вторичные индексы можно добавить или удалить после создания таблицы. Изменять текущие запросы не требуется — запросы просто выполняются быстрее. В зависимости от потребностей можно создать охватывающие и функциональные индексы.

Использование охватывающих индексов

Охватывающие индексы — это индексы, которые включают данные из строки, а также индексированные значения. Когда вы найдете нужную запись индекса, доступ к основной таблице не требуется.

Например, в примере таблицы с контактами можно создать вторичный индекс только по столбцу socialSecurityNum. Этот вспомогательный индекс ускорит запросы, которые фильтруются по значениям номера социального страхования, но получение других значений полей требует дополнительного чтения из основной таблицы.

rowkey адрес телефон Имя lastName номер социального страхования
Dole-John-111 Улица Сан Габриэль, 1111 1-425-000-0002 Джон Дол 111
Раджи-Калвин-222 Сан Габриэль Др., 5415 1-230-555-0191 Калвин Раджи 222

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

CREATE INDEX ssn_idx ON CONTACTS (socialSecurityNum) INCLUDE(firstName, lastName);

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

SELECT socialSecurityNum, firstName, lastName FROM CONTACTS WHERE socialSecurityNum > 100;

Использование функциональных индексов

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

Например, можно создать индекс, позволяющий выполнить поиск без учета регистра по сочетанию имени и фамилии человека:

CREATE INDEX FULLNAME_UPPER_IDX ON "Contacts" (UPPER("firstName"||' '||"lastName"));

Структура запросов

При проектировании запросов учитывайте следующие основные рекомендации:

  • разберитесь с планом запроса и проверьте ожидаемое поведение;
  • Присоединяйтесь эффективно.

Понять план запроса

В SQLLine используйте инструкцию EXPLAIN, за которой следует SQL-запрос, чтобы просмотреть план операций, выполняемых Phoenix. Убедитесь, что план:

  • Использует ваш первичный ключ, когда это необходимо.
  • использует соответствующие вторичные индексы, а не таблицу данных;
  • когда это необходимо, использует RANGE SCAN или SKIP SCAN вместо TABLE SCAN.

Примеры плана

Предположим, что у вас есть таблица с именем FLIGHTS, содержащая сведения о задержках рейсов.

Чтобы выбрать все рейсы с airlineid19805, где airlineid — это поле, которое не находится в первичном ключе или в каком-либо индексе:

select * from "FLIGHTS" where airlineid = '19805';

Выполните описанную команду следующим образом:

explain select * from "FLIGHTS" where airlineid = '19805';

План запроса выглядит следующим образом:

CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER FLIGHTS
   SERVER FILTER BY AIRLINEID = '19805'

В этом плане обратите внимание на фразу «ПОЛНОЕ СКАНИРОВАНИЕ ВСЕХ РЕЙСОВ». Эта фраза указывает, что запрос выполняет TABLE SCAN по всем записям в таблице вместо использования более эффективного параметра RANGE SCAN или SKIP SCAN.

Теперь предположим, что необходимо выполнить запрос рейсов на 2 января 2014 года для компании-перевозчика AA, где номер рейса больше 1. Предположим, что в примере таблицы есть столбцы year (год), month (месяц), dayofmonth (день месяца), carrier (перевозчик) и flightnum (номер рейса), и все они являются частью составного первичного ключа. Запрос будет выглядеть следующим образом:

select * from "FLIGHTS" where year = 2014 and month = 1 and dayofmonth = 2 and carrier = 'AA' and flightnum > 1;

Давайте рассмотрим план этого запроса:

explain select * from "FLIGHTS" where year = 2014 and month = 1 and dayofmonth = 2 and carrier = 'AA' and flightnum > 1;

Ниже приведен результирующий план:

CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER FLIGHTS [2014,1,2,'AA',2] - [2014,1,2,'AA',*]

Значения в квадратных скобках — диапазон значений для первичных ключей. В этом случае значения диапазона фиксированы 2014 годом, месяцем 1 и днем месяца 2, но допускаются номера рейса, начинающиеся с 2 и более (*). Этот план запроса подтверждает, что первичный ключ используется должным образом.

Затем создайте индекс с именем carrier2_idx для таблицы FLIGHTS, который находится только в поле перевозчика. Этот индекс также включает в себя flightdate, tailnumoriginи flightnum как покрытые столбцы, данные которых также хранятся в индексе.

CREATE INDEX carrier2_idx ON FLIGHTS (carrier) INCLUDE(FLIGHTDATE,TAILNUM,ORIGIN,FLIGHTNUM);

Предположим, что вы хотите получить носитель вместе с flightdate и tailnum, как показано в следующем запросе.

explain select carrier,flightdate,tailnum from "FLIGHTS" where carrier = 'AA';

Вы должны увидеть, что этот индекс используется:

CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER CARRIER2_IDX ['AA']

Полный список элементов, которые могут присутствовать в результатах объяснения плана, см. в разделе объяснения планов в руководстве по настройке Apache Phoenix.

Присоединяйтесь эффективно

Как правило, следует избегать объединения таблиц, если только одна из таблиц не является большой, особенно для частых запросов.

Если необходимо, можно сделать большие соединения с указанием /*+ USE_SORT_MERGE_JOIN */, однако большие соединения являются ресурсоемкими операциями с огромным числом записей. Если общий размер всех таблиц с правой стороны превышает объем доступной памяти, используйте указание /*+ NO_STAR_JOIN */.

Сценарии

Ниже приведены рекомендации, в которых описаны некоторые распространенные шаблоны.

Нагрузки с преобладанием операций чтения

В случае с вариантами использования с интенсивными операциями чтения нужно обязательно использовать индексы. Кроме того, чтобы оптимизировать скорость чтения, рассмотрите возможность создания охватывающих индексов.

Рабочие нагрузки с интенсивными операциями записи

Для рабочих нагрузок с интенсивными операциями записи, в которых первичный ключ монотонно возрастает, создайте дополнительные сегменты (salt buckets), чтобы избежать горячих точек записи, что может снизить общую пропускную способность чтения из-за необходимости дополнительных сканирований. Кроме того, при использовании операции UPSERT для добавления большого количества записей отключите автоматическое подтверждение и объедините записи в пакет.

Массовое удаление

При удалении большого набора данных включите autoCommit перед отправкой запроса DELETE, чтобы клиенту не нужно было запоминать ключи всех удаленных записей. Автокоммит не позволяет клиенту буферизовать строки, затронутые запросом DELETE, поэтому Phoenix может удалить их непосредственно на региональных серверах без необходимости возвращать их клиенту.

Невозможно изменить и только добавление

Если в сценарии скорость записи важнее целостности данных, рассмотрите возможность отключения упреждающего протоколирования при создании таблиц:

CREATE TABLE CONTACTS (...) DISABLE_WAL=true;

Дополнительные сведения об этом и других вариантах см. в статье о грамматике Apache Phoenix.

Следующие шаги