Spec-Zone.ru › MariaDB

HBase Storage Engine

Сопоставление данных из HBase в SQL

Задача Jira для этого — MDEV-122

В настоящее время никто не работает над этой функцией. См. Cassandra Storage Engine для связанной разработки, которая достигла стадии выпуска.

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

Модель данных HBase и операции

1.1 Модель данных HBase

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

1.2 Операции чтения HBase

API HBase определяет два способа чтения данных:

  • Поиск по точке: получение записи для заданного ключа строки.
  • Сканирование по точке: чтение всех записей в диапазоне [startRow, stopRow).

Оба типа сканирования позволяют указать:

  • Семейство столбцов, которое нас интересует
  • Конкретный столбец, который нас интересует

По умолчанию для столбцов с версиями возвращается только самая последняя версия. API HBase также позволяет запросить

  • версии столбцов, которые были действительны в определенный момент времени;
  • все версии, которые были действительны в указанном интервале [minStamp, maxStamp);
  • N последних версий. Мы будем ссылаться на вышеперечисленное как [VersionedDataConds].

Существуют два способа сопоставления таблиц HBase с таблицами SQL:

  • Сопоставление по ячейке
  • Сопоставление по строке

2. Сопоставление по ячейке

Утилита HBase имеет команду «scan», вот пример её вывода:

hbase(main):007:0> scan 'testtable'
 ROW COLUMN+CELL
  myrow-1 column=colfam1:q1, timestamp=1297345476469, value=value-1
  myrow-2 column=colfam1:q2, timestamp=1297345495663, value=value-2
  myrow-2 column=colfam1:q3, timestamp=1297345508999, value=value-3

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

2.1 Определение таблицы

Сопоставление можно определить следующим образом:

CREATE TABLE hbase_tbl_cells (
  row_id binary(MAX_HBASE_ROWID_LEN),
  column_family binary(MAX_HBASE_COLFAM_LEN),
  column_name binary(MAX_HBASE_NAME_LEN),
  timestamp TIMESTAMP,
  value BLOB,
  PRIMARY KEY (row_id, column_family, column_name, timestamp)
) ENGINE=hbase_cell;

В этом сопоставлении нет необходимости в динамических столбцах.

  • ПРИМЕЧАНИЕ: Хорошо иметь определения SQL таблиц, независимые от содержимого таблицы HBase на бэкенде. Это избавит нас от необходимости синхронизации определений таблиц между HBase и MySQL (кластер NDB должен был это делать, и в итоге они реализовали очень сложную систему для этого).

2.2 Запросы в сопоставлении по ячейке

# Point-select:
SELECT value 
FROM hbase_cell
WHERE 
  row_id='hbase_row_id' AND 
  column_family='hbase_column_family' AND column_name='hbase_column'
  ...
#  Range select:
#   (the example uses BETWEEN but we will support arbitrary predicates)
SELECT value 
FROM hbase_cell
WHERE 
  row_id BETWEEN 'hbase_row_id1' AND 'hbase_row_id2' AND 
  column_family='hbase_column_family' AND column_name='hbase_column'
# Update a value for {row, column}
UPDATE hbase_cell SET value='value' 
WHERE row_id='hbase_row' AND 
      column_family='col_family' AND column_name='col_name'
# Add a column into row
INSERT INTO hbase_cell values ('hbase_row', 'col_family','col_name','value');

Обратите внимание, что

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

2.3 Сопоставление SQL-операторов

Сопоставление для SELECT

Таблица определена как имеющая

  PRIMARY KEY (row_id, column_family, column_name, timestamp)

что позволяет использовать оптимизатор диапазонов для получения диапазонов по

  • rowid
  • rowid, column_family
  • rowid, column_family, column_name
  • ...

Если диапазон определяет одну строку, мы можем прочитать её с помощью HTable.get(), в противном случае нам придётся использовать HTable.getScanner() и воспользоваться полученным сканером.

Несколько условий неравенства

API HBase позволяет сканировать диапазон строк, получая только определённое имя столбца или определённые семейства столбцов. В нашем SQL сопоставлении это можно записать как:

SELECT value
FROM hbase_cell
WHERE
  row_id BETWEEN 'hbase_row_id1' AND 'hbase_row_id2' AND
  column_family='hbase_column_family'                           (*)

Если мы передадим это оптимизатору диапазонов, он сгенерирует диапазон:

  ('hbase_row_id1', 'hbase_column_family') <= (row_id, column_family) <=
  ('hbase_row_id2', 'hbase_column_family')

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

  'hbase_row_id1' < rowid < 'hbase_row_id2'

Это приведёт к чтению дополнительных данных.

Возможные решения:

  • Расширить интерфейс многодиапазонного чтения для обхода «графа SEL_ARG», а не списка диапазонов. Это позволит уловить точный вид условий, таких как (*).
  • Реализовать перенос условий таблицы и выполнить независимый анализ условий.
  • Определить больше индексов, чтобы диапазоны были «плотными». А как насчёт (row_id BETWEEN $X AND $Y) AND (timestamp BETWEEN $T1 AND $T2)? Независимо от того, какой индекс вы определите, список диапазонов не будет идентичен условию WHERE.

Сопоставление для INSERT

INSERT будет переведён в вызов HTable.checkAndPut(..., value=NULL). Таким образом, попытка вставить {rowid, column}, которая уже существует, завершится неудачей.

Сопоставление для DELETE

API хранилища MySQL/MariaDB обрабатывает DELETE так:

  • Используйте какой-то способ для чтения записи, которую нужно удалить
  • Вызовите handler->ha_delete_row(). Это удалит последнюю прочитанную строку.

ha_hbase_cell может запомнить {rowid, column_name} записи и затем использовать вызов HBase.checkAndDelete(), чтобы убедиться, что удаляется то, что мы прочитали.

Если мы получим оператор в виде

DELETE FROM hbase_cell 
WHERE rowid='hbase_row_id' AND column_family='...' AND column_name='...';

то чтение записи избыточно (мы могли бы просто выполнить один вызов HBase.checkAndDelete()). Однако для этого потребуется некоторая форма переноса запросов.

Сопоставление для UPDATE

UPDATE подобны DELETE, если поля row_id, column_family и column_name не изменяются (т. е. изменяется только значение column_value). Как и с DELETE:

  • Вызов HBase.checkAndPut() можно использовать для обеспечения обновления того, что мы прочитали.
  • для одноточечных UPDATE может потребоваться обходной путь, чтобы нам не нужно было читать значение перед обновлением.

Если оператор UPDATE изменяет поля row_id, column_family или column_name, ситуация становится совершенно другой. HBase не позволяет изменять rowid записи. Мы можем только удалить запись с old rowid и вставить запись с новым rowid. HBase не поддерживает транзакции на нескольких строках, поэтому нам нужно вставить новую версию записи, прежде чем удалять старую (я предполагаю, что дублирование данных лучше, чем потеря данных).

Для первого этапа мы можем запретить UPDATE, которые изменяют row_id, column_family или column_name.

3. Сопоставление по строке

Пусть каждая строка в таблице HBase отображается в строку с точки зрения SQL:

SELECT * FROM hbase_table;

row-id column1 column2  column3  column4  ...
------ ------- -------  -------  -------  
row1    data1   data2
row2                     data3    
row3    data4                      data5

Проблема в том, что набор столбцов в таблице HBase не фиксирован и потенциально очень велик. Решением является размещение всех столбцов в одном столбце blob и использование функций Dynamic Columns (http://kb.askmonty.org/en/dynamic-columns) для упаковки/извлечения значений отдельных столбцов:

row-id dyn_columns
------ ------------------------------
row1   {column1=data1,column2=data2}
row2   {column3=data3}
row3   {column1=data4,column4=data5}

3.2 Определение сопоставления

Определение таблицы может выглядеть так:

CREATE TABLE hbase_tbl_rows (
  row_id BINARY(MAX_HBASE_ROWID_LEN),
  columns BLOB,  -- All columns/values packed in dynamic column format
  PRIMARY KEY (row_id)
) ENGINE=hbase_row;

(TODO: У HBase есть лимит MAX_HBASE_ROWID_LEN? Какой он? Мы можем его игнорировать. Пусть пользователь определяет столбец «row_id» с любым желаемым лимитом; не выполняйте операции со строками, у которых row_id длиннее предела)

Функции для чтения данных:

  COLUMN_GET(dynamic_column, column_nr as type)
  COLUMN_EXISTS(dynamic_column, column_nr);
  COLUMN_LIST(dynamic_column);

Функции для изменения данных:

  COLUMN_ADD(dynamic_column, column_nr,  value [as type], ...)
  COLUMN_DELETE(dynamic_column, column_nr, column_nr, ...);

3.2.1 Необходимые улучшения в Dynamic Columns

Функции динамических столбцов нельзя использовать напрямую:

  • Столбцы HBase имеют строковые имена, а динамические столбцы — числовые (см. параметр column_nr для вышеуказанных функций). Набор имён столбцов в HBase потенциально очень большой, нет способа получить список всех имён: мы не сможем решить эту проблему с сопоставлением в стиле перечислений, нам потребуется реальная поддержка строковых имён.
  • В HBase есть семейства столбцов, а в Dynamic Columns — нет. Семейство столбцов — это не просто «:» в имени столбца. Например, API HBase позволяет запросить «все столбцы из определённого семейства столбцов».
  • HBase поддерживает данные с версиями, а Dynamic Columns — нет. Возможным ограниченным решением является наличие глобальной/сессионной переменной @@hbase_timestamp, которая глобально укажет требуемую версию данных.
  • (См. также примечание ниже об эффективном выполнении)

Имена динамических столбцов рассматриваются в MDEV-377

3.3 Запросы в сопоставлении по строке

# Point-select, get value of one column
SELECT COLUMN_GET(hbase_tbl.columns, 'column_name' AS INTEGER)
FROM hbase_tbl
WHERE 
  row_id='hbase_row_id';
#  Range select:
#   (the example uses BETWEEN but we will support arbitrary predicates)
SELECT COLUMN_GET(hbase_tbl.columns, 'column_name' AS INTEGER)
FROM hbase_tbl
WHERE 
  row_id BETWEEN 'hbase_row_id1' AND 'hbase_row_id2';
# Update or add a column for a row
UPDATE hbase_tbl SET columns=COLUMN_ADD(columns, 'column_name', 'value') WHERE row_id='hbase_row_id1';

Использование COLUMN_ADD, как указано выше, не будет проверять, существовал ли столбец column_name=X для этой строки. Если он существовал, он будет безмолвно перезаписан.

ВНИМАНИЕ: Непонятно, как легко сделать то, что похоже на оператор SQL INSERT, т. е. который завершается ошибкой, если данные, которые вы изменяете, уже существуют.

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

ВНИМАНИЕ: Также можно поставить вопрос о том, имеет ли оператор со смыслом «сохранить эти данные независимо от того, что было раньше», какое-либо значение для «удалённого» хранилища, где вы не единственный, кто изменяет данные.

# Set all columns at once, overwriting the content that was there
UPDATE hbase_tbl SET columns=... WHERE row_id='hbase_row_id1';

UPDATE hbase_tbl SET columns=COLUMN_CREATE('column1', 'foo') WHERE row_id='row1';

Обратите внимание, что последний оператор приведёт к удалению всех столбцов, кроме «column1», для строки «row1». Это кажется логичным для SQL, но такой операции в HBase нет.

# Insert a new row with column(s)
INSERT INTO hbase_tbl (row_id, columns) VALUES
  ('hbase_row_id', COLUMN_CREATE('column_name', 'column-value'));

В: Непонятно, как получить доступ к данным с версиями? Можем ли мы обойтись без данных с версиями на первом этапе? (а затем использовать @@hbase_timestamp для второго этапа?)

В: Непонятно, как выбрать «все столбцы из семейства столбцов X».

3.4 Эффективное выполнение для сопоставления по строке

3.4.1 Анализ предикатов

Таблица объявляет:

  row_id BINARY(MAX_HBASE_ROWID_LEN),
  ...
  PRIMARY KEY (row_id)

что позволяет использовать оптимизатор диапазонов/ссылок для извлечения диапазонов по столбцу row_id.

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

SELECT column_get(columns, 'some_data') FROM hbase_tbl 
WHERE 
  row_id BETWEEN 'first_interesting_row' and 'last_interesting_row' AND 
  column_get(columns, 'attribute' as string)='eligible';

Оптимизатор диапазонов не может уловить условия в виде

column_get(columns, 'attribute' as string)='eligible'

Нам нужно либо расширить его, либо создать другой анализатор условий.

3.4.2 Оптимизации динамических столбцов

В настоящее время MariaDB работает с динамическими столбцами в этом сценарии:

  1. При чтении записи весь BLOB (=все столбцы) считывается в память
  2. Запрос обрабатывает BLOB с помощью функций Dynamic Columns (читает и обновляет значения некоторых столбцов и т. д.)
  3. [Если это UPDATE] весь BLOB записывается обратно в таблицу

Если использовать этот подход с HBase, это приведёт к большому объёму накладных расходов при чтении/записи ненужных данных.

Решение №1: чтение по запросу

  • При чтении записи в таблице не считывать никакие столбцы, возвращать дескриптор BLOB.
  • Функции Dynamic Column будут использовать дескриптор для чтения определённых столбцов. Столбец считывается из HBase только при запросе его значения.

Эта схема гарантирует отсутствие избыточных чтений данных, но при этом увеличивает количество дополнительных обращений mysqld<->HBase (что, скорее всего, будет дорогостоящим).

Решение №2: Список чтений

  • Пройти по запросу и найти все ссылки на hbase_table.columns.
  • Собрать имена столбцов, которые нужно прочитать, и извлечь только эти столбцы.

Это может привести к избыточным чтениям данных, например, для

  SELECT COLUMN_GET(hbase_tbl, 'column1' AS INTEGER) 
  FROM hbase_tbl
  WHERE 
    row_id BETWEEN 'hbase_row_id1' AND 'hbase_row_id2' AND 
    COLUMN_GET(hbase_tbl, 'column2' AS INTEGER)=1

столбец1 будет прочитан для строк, у которых столбец2≠1. Это всё ещё, кажется, лучше, чем дополнительные обращения.

Возникает вопрос, что делать, когда запрос содержит ссылки, такие как

  COLUMN_GET(hbase_tbl, {non-const-item} AS ...) 

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

  • считать все столбцы
  • считать столбцы по запросу
  • остановить запрос с ошибкой.

3.5 Сопоставление SQL-запросов

SELECT

См. вышеупомянутые разделы: мы сможем проанализировать условие по row_id и список столбцов, которые нам нужно прочитать. Это даст достаточно информации для вызова HTable.get() или HTable.getScanner() и использования сканера.

INSERT

INSERT должен гарантировать фактическое создание строки, а не перезапись существующих строк. Это нетривиально в HBase. Наиболее близкий к этому вариант — выполнить несколько вызовов HTable.checkAndPut() с проверками, что данные не перезаписываются.

Это позволит INSERT ('row_id', COLUMN_CREATE('column1', 'data')) завершиться успешно, даже если в таблице уже была строка ('row_id', COLUMN_CREATE('column2', 'data')).

Ещё одна потенциальная проблема заключается в том, что INSERT может завершиться неудачей на середине пути (будут вставлены только некоторые столбцы записи).

DELETE

DELETE, кажется, работает: можно удалить все комбинации {rowid, column_name} для данного row_id. Не уверен, возможно, это потребует нескольких обращений к HBase.

UPDATE

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

Ожидается, что наиболее частый тип UPDATE — это изменение значения столбца:

UPDATE hbase_tbl SET columns=COLUMN_ADD(columns, 'column_name', 'value') 
WHERE 
  row_id='hbase_row_id1' AND COLUMN_GET(columns, 'column_name')='foo';

Для этого нам понадобятся изменённые функции Dynamic Column, которые будут представлять *изменения* в наборе столбцов (а не *состояние*), чтобы избежать чтения и записи столбцов.

4. Сопоставление столбцов SELECT

Это упрощение сопоставления по строке. Предположим, пользователь заинтересован только в определённых столбцах с именами `column1` и `column2`. Он создаёт таблицу с таким определением:

CREATE TABLE hbase_tbl_cells (
  row_id binary(MAX_HBASE_ROWID_LEN),
  column1  TYPE,
  column2  TYPE,
  PRIMARY KEY (row_id),
  KEY(column1),
  KEY(column2)
) ENGINE=hbase_columns;

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

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

5. Сравнение сопоставлений

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

# Per-cell:
SELECT column_name, value 
FROM hbase_cell
WHERE 
  row_id='hbase_row_id1' AND 
  column_family='col_fam' AND column_name IN ('column1','column2')
+-------------+-------+
| column_name | value |
+-------------+-------+
| column1     | val1  |
| column2     | val2  |
+-------------+-------+
# Per row:
SELECT 
  COLUMN_GET(columns, 'col_fam:column1') as column1,  
  COLUMN_GET(columns, 'col_fam:column2') as column2,
FROM hbase_row
WHERE 
  row_id='hbase_row_id1' 
+---------+---------+
| column1 | column2 |
+---------+---------+
| val1    | val2    |
+---------+---------+

Сопоставление по ячейке:

  • Позволяет более тонко управлять выбором данных с версиями (легко указать [диапазон] версий для выбора), семействами столбцов и т. д.
  • Более подходит для случаев, когда нужно выбрать произвольный список столбцов.

Сопоставление по строке (или столбцам SELECT) проще, когда:

  • доступ ограничен к набору столбцов
  • нужно получить доступ к нескольким столбцам из нескольких строк (в сопоставлении по ячейке это потребует [неэффективного?] самосоединения).

6. Взаимодействие с HBase

HBase написан на Java, и его собственный API — это библиотека на Java. Нам нужно взаимодействовать с ним из кода движка хранилища C++. Возможные варианты:

6.1 Использовать Thrift

Это требует установки HBase для запуска сервера Thrift.

6.2 Реализовать сетевой протокол HBase

  • Кажется, это собственный протокол RPC.
  • Здесь есть независимая реализация: https://github.com/stumbleupon/asynchbase. Это 10 000 строк кода на Java, что даёт представление о сложности протокола HBase.
    • По-видимому, поддерживается только подмножество функций? То есть я не смог найти упоминания о поддержке условий, передаваемых вниз?
    • Посмотрите в HBaseRpc.java для "Unofficial Hadoop / HBase RPC protocol documentation"

6.3 Использовать JNI+протокол клиента HBase

  • Не уверен, насколько это сложно.
  • Марк упомянул, что это имеет неприемлемые накладные расходы?

7. Согласованность, транзакции и т. д.

  • HBase поддерживает транзакции для отдельных записей. Это означает, что движок хранилища HBase будет обладать характеристиками, похожими на MyISAM? Например, если произойдёт сбой в середине UPDATE для нескольких строк, нет способа вернуться назад.
  • В.: Важны ли вообще записи? (например, если у нас будет первая версия с предоставлением только чтения, это будет полезно?) О.: Да?

8. Упаковки

В.: Будут ли нужны объединения, то есть мне нужно сразу реализовать Multi-Range-Read и поддержку Batched Key Access?

9. Результаты обсуждения с Монти

  • Сопоставление по строке, похоже, намного полезнее, чем сопоставление по ячейке, так как у многих пользователей есть запросы, которые извлекают множество столбцов для множества строк (так ли это?)
  • Динамический формат столбцов будет поддерживать строковые имена столбцов (см. MDEV-377)
  • Для первого этапа забудьте о проблемах с динамическими столбцами, упомянутых в "Эффективное выполнение для сопоставления по строке". Достаточно, чтобы все столбцы возвращались как один BLOB, который физически содержит все столбцы.
Содержимое, воспроизведённое на этом сайте, является собственностью соответствующих владельцев, и это содержание не проходит предварительной проверки MariaDB. Мнения, информация и мнения, выраженные в этом содержании, не обязательно отражают точку зрения MariaDB или любой другой стороны.

© 2023 MariaDB
Licensed under the Creative Commons Attribution 3.0 Unported License and the GNU Free Documentation License.
https://mariadb.com/kb/en/hbase-storage-engine/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API