Рекомендации по оптимизации производительности 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 address phone firstName lastName
1000 Владимирская, 34 1-425-000-0002 Джон Кузнецов
8396 Владимирская, 54 1-230-555-0191 Виктор Игнатьев

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

rowkey address phone firstName lastName socialSecurityNum
1000 Владимирская, 34 1-425-000-0002 Джон Кузнецов 111
8396 Владимирская, 54 1-230-555-0191 Виктор Игнатьев 222

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

rowkey address phone firstName lastName socialSecurityNum
Kuznetsov-Artem-111 Владимирская, 34 1-425-000-0002 Джон Кузнецов 111
Ignatiev-Victor-222 Владимирская, 54 1-230-555-0191 Виктор Игнатьев 222

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

rowkey key значение
Kuznetsov-Artem-111 address Владимирская, 34
Kuznetsov-Artem-111 phone 1-425-000-0002
Kuznetsov-Artem-111 firstName Джон
Kuznetsov-Artem-111 lastName Кузнецов
Kuznetsov-Artem-111 socialSecurityNum 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. Этот вторичный индекс ускорит запросы, которые фильтруются по значениям socialSecurityNum, но получение других значений полей требует другого чтения в основной таблице.

rowkey address phone firstName lastName socialSecurityNum
Kuznetsov-Artem-111 Владимирская, 34 1-425-000-0002 Джон Кузнецов 111
Ignatiev-Victor-222 Владимирская, 54 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'

В этом плане обратите внимание на фразу FULL SCAN OVER FLIGHTS (ПОЛНОЕ СКАНИРОВАНИЕ РЕЙСОВ). Эта фраза указывает, что запрос выполняет 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 */.

Сценарии

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

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

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

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

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

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

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

Запросы только на добавление без изменения существующих записей

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

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

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

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