Mappatura dei dati da HBase a SQL

Il modello dei dati e le operazioni di HBase

1.1 Il modello dei dati di HBase

  • Una tabella HBase è formata da righe, che sono identificate dalla chiave di riga.
  • Ogni riga ha un numero di colonne arbitrario, potenzialmente molto grande.
  • Le colonne sono suddivise in gruppi di colonne, questi gruppi determinano in che modo esse vengono registrate (un'ottimizzazione consiste nel non leggere alcuni gruppi).
  • Ogni combinazione (riga, colonna) ha diverse versioni dei dati, identificate da un timestamp.

1.2 Le operazioni di lettura di HBase

La API di HBase definisce due modi per leggere i dati:

  • Ricerca di un punto: seleziona un record per una data row_key.
  • Scansione tra i punti: legge tutti i record nell'intervatto [rigaIniziale, rigaFinale).

Entrambi i tipi di letture permettono di specificare:

  • Una famiglia di colonne alla quale si è interessati
  • Una determinata colonna

Il comportamento predefinito per le colonne con versione è restituire solo la versione più recente. L'API di HBase API permette anche di ottenere:

  • le versioni delle colonne che erano attuali ad un certo timestamp;
  • tutte le versioni che sono state attuali in un certo intervallo [tstampInizio, tstampFine).
  • N versioni più recenti. Queste condizioni da qui in avanti verranno chiamate [VersionedDataConds].

Esistono due modi per mappare le tabelle HBase alle tabelle SQL:

2. Mappatura per riga

Se ogni riga in una tabella HBase è mappata ad una riga SQL in questo modo:

SELECT * FROM hbase_table;

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

Il problema è che l'insieme delle colonne della tabella HBase non è fisso ed è potenzialmente molto grande. La soluzione è immettere tutte le colonne in un unico campo blob e utilizzare le funzioni per le Colonne Dinamiche (http://kb.askmonty.org/en/dynamic-columns) per registrare/estrarre i valori delle singole colonne:

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

2.2 Definizione della mappatura

Il DDL della tabella assomiglia al seguente:

CREATE TABLE hbase_tbl_rows (
  row_id BINARY(MAX_HBASE_ROWID_LEN),
  columns BLOB,
  PRIMARY KEY (row_id)
) ENGINE=hbase_row;

(TODO: Does Hbase have MAX_HBASE_ROWID_LEN limit? What is it?)

Il campo blob `columns` conterrà i valori (e i nomi) di tutte le colonne.

L'accesso al nome/valore delle singole colonne deve essere effettuato tramite le funzioni per le Colonne Virtuali (see http://kb.askmonty.org/en/dynamic-columns).

Funzioni per leggere i dati:

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

Funzioni per modificare i dati:

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

2.2.1 Miglioramenti necessari nelle Colonne Dinamiche

Le funzioni per le Colonne Dinamiche non possono essere utilizzate così come sono:

  • Le colonne HBase hanno nomi letterali, le Colonne Dinamiche hanno i numeri (si veda il parametro column_nr per le funzioni sopra). L'insieme delle colonne di HBase è potenzialmente molto grande, non c'è modo di ottenere un elenco di tutti i nomi: non si potrà risolvere questo problema con una mappatura stile enum, occorre un vero supporto ai nomi di colonna.
  • HBase ha le famiglie di colonne, le Colonne Dinamiche no. Una famiglia di colonne non è solo un ':' nel nome delle colonne. Per esempio, l'API di HBase permette di richiedere "tutte le colonne che appartengono a una data famiglia".
  • HBase supporta i dati con versione, le Colonne Dinamiche no. Una possibile semplice soluzione è avere una variabile global/session @@hbase_timestamp che specifica globalmente la versione dei dati richiesta.
  • (Si veda anche la nota sulle prestazioni)

2.3 Le query nella mappatura per riga

# Point-select:
SELECT COLUMN_GET(hbase_tbl.columns, 'column_name' AS INTEGER)
FROM hbase_tbl
WHERE 
  row_id='hbase_row_id';


#  Intervallo:
#   (l'esempio usa BETWEEN ma saranno supportati predicati arbitrari)
SELECT COLUMN_GET(tab_hbase.colonne, 'nome_colonna' AS INTEGER)
FROM hbase_tbl
WHERE 
  row_id BETWEEN 'riga_hbase_id1' AND 'riga_hbase_id2';

# Aggiorno una colonna
UPDATE hbase_tbl SET columns=COLUMN_ADD(colonne, 'nome_colonna', 'valore');

# Aggiungo una colonna
UPDATE hbase_tbl SET columns=COLUMN_ADD(colonne, 'nome_colonna', 'valore');

# Inserisco una riga con una colonna
INSERT INTO hbase_tbl (id_riga, colonne) VALUES 
  ('hbase_row_id', COLUMN_CREATE('column_name', 'column-value'));

Q: Non è chiaro: come si accede alle versioni dei dati? Si possono tralasciare nella prima milestone? 
   (e poi, usare global @@hbase_timestamp per la seconda milestone?)

Q: Non è chiaro come si seleziona "tutte le colonne dalla famiglia X".

2.4 Esecuzione efficiente della mappatura per riga

La tabella è dichiarata così:

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

ciò permette all'ottimizzatore dell'intervallo e della referenza di costruire ricerche di un punto e scansioni di intervalli su row_id.

Q: Ci sarà bisogno di join, ovvero: occorre implementare le ottimizzazioni Multi-Range-Read e Batched Key Access?

Attualmente MariaDB lavora con le Colonne Virtuali in questo scenario:

  1. Per leggere un record, viene letto in memoria l'intero blob
  2. Le funzioni sulle Colonne Dinamiche operano sui dati in memoria (leggono e modificano singole colonne nella memoria)
  3. [Se è un UPDATE] l'intero blob viene riscritto nella tabella

Se MariaDB userò questo approccio con HBase, leggerà molte colonne non necessarie.

Soluzione #1: letture su richiesta

  • Per leggere un record, non leggere alcuna colonna, restituire un blob handle.
  • Le funzioni per le Colonne Dinamiche useranno l'handle per leggere le singole colonne. Una colonna viene letta da HBase solo quando il suo valore viene richiesto.

Questo schema previene le letture ridondanti dei dati, provocando però di maggiori comunicazioni tra mysqld e HBase (che probabilmente sono costose)

Soluzione #2: Lista di letture

  • Traversare la query e trovare tutti i riferimenti a tab_hbase.colonne.
  • Ricordare i nomi delle colonne lette, ed estrarre solo queste colonne.

Questo metodo potrebbe causare letture ridondanti, per esempio in:

  SELECT COLUMN_GET(tab_hbase, 'colonna1' AS INTEGER) 
  FROM hbase_tbl
  WHERE 
    row_id BETWEEN 'hbase_riga_id1' AND 'hbase_riga_id2' AND 
    COLUMN_GET(tab_hbase, 'colonna2' AS INTEGER)=1

colonna1 viene letta in tutte quelle righe che hanno column2!=1. Tuttavia, questo problema sembra essere preferibile alle troppe comunicazioni.

Occorre decidere cosa fare se la query ha riferimenti come

  
  COLUMN_GET(tab_hbase, {oggetto-non-costante} AS ...) 

dove non c'è modo di sapere in anticipo quali sono le colonne da leggere. I possibili approcci sono:

  • estrarre tutte le colonne
  • estrarre le colonne su richiesta
  • fermare la query restituendo un errore.

3. Mappatura per cella

La shell di HBase ha il comando 'scan', ecco un esempio del suo output:

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

Qui una riga di HBase produce diverse righe di output. Ognuna di esse rappresenta una combinazione (id_riga, colonna), pertanto le righe con colonne multiple (e revisioni multiple dei dati delle colonne) possono essere rappresentate facilmente.

3.1 Definizione della mappatura

La mappatura può essere definita come segue:

CREATE TABLE hbase_tbl_cells (
  id_riga binary(MAX_HBASE_ROWID_LEN),
  famiglia_colonne binary(MAX_HBASE_COLFAM_LEN),
  nome_colonna binary(MAX_HBASE_NAME_LEN),
  timestamp TIMESTAMP,
  valore BLOB,
  PRIMARY KEY (riga_id, famiglia_colonne, nome_colonna, timestamp)
) ENGINE=hbase_cell;

Non c'è bisogno di usare le Colonne Dinamiche in questa mappatura.

  • NOTA: E' desiderabile che le definizioni delle tabelle SQL siano indipendenti dal contenuto della tabella hbase backend. In questo modo non è necessario sincronizzare la definizione della tabella tra hbase e mysql (NDB cluster ha dovuto farlo, e per farlo hanno dovuto implementare un sistema molto complesso).

3.2 Query nella mappatura per cella

# Point-select:
SELECT value 
FROM hbase_cell
WHERE 
  row_id='id_riga' AND 
  column_family='famiglia_colonne' AND column_name='colonna'
  ...

#  Select di un intervallo:
#   (the example uses BETWEEN but we will support arbitrary predicates)
SELECT value 
FROM hbase_cell
WHERE 
  row_id BETWEEN 'id_riga1' AND 'id_riga2' AND 
  column_family='famiglia_colonne' AND column_name='colonna'


# Update su una colonna
UPDATE cella SET value='valore' 
WHERE row_id='hbase_row' AND 
      column_family='famiglia_colonne' AND column_name='colonna'


# Aggiungere una colonna (aggiunge anche una riga, se non ce ne sono)
INSERT INTO cella values ('riga_hbase', 'famiglia_col','nome_col','valore');

Si noti che

  • accedere alle versioni dei dati è facile: si può leggere una particolare versione, versioni in un intervallo, etc
  • è facile anche selezionare tutte le colonne di una certa famiglia .

3.3 Esecuzione efficiente della mappatura per cella

Nella definizione della tabella c'è:

  PRIMARY KEY (row_id, column_family, column_name, timestamp)

Questo permette rapide ricerche di un punto su tuple (id_riga, famiglia_colonne, nome_colonna).

L'ordine delle colonne nell'indice permette inoltre di leggere tutte le colonne di una certa famiglia.

3.3.1 Scansire alcune colonne all'interno di un intervallo

L'API di HBase permette di scansire un intervallo di righe, estraendo solo certe colonne o certe famiglie. In SQL, si scrive così:

SELECT valore
FROM hbase_cell
WHERE
  row_id BETWEEN 'id_riga1' AND 'id_riga2' AND
  famiglia='nome_famiglia'                           (*)

Elaborato dall'ottimizer, viene prodotto un range:

  ('id_riga1', 'nome_famiglia') <= (row_id, nome_famiglia) <=
  ('id_riga2', 'nome_famiglia')

che racchiude tutte le famiglie delle colonne che soddisfano

  'id_riga1' < rowid < 'id_riga2'

In questo modo vengono però letti più dati.

Possibili soluzioni:

  • Estendere l'interfaccia di multi-range-read per attraversare il 'grafo SEL_ARG' invece di una lista di intervalli. Questo permette di catturare l'esatta forma delle condizioni simili a (*).
  • Usare il condition pushdown e analizzare la condizione.
  • Definire più indici, in modo che gli intervalli siano "densi". può andare (row_id BETWEEN $X AND $Y) AND (timestamp BETWEEN $T1 AND $T2) ? Indipendentemente da quali indici si definiscono, la lista degli intervalli non sarà identica alla clausola WHERE.

4. Comparazione delle due mappature

Se si selezionano due colonne da una riga, la mappatura per cella produce risultati "verticali", mentre la mappatura per riga produce risultati "orizzontali".

# Per cella:
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 riga:
SELECT 
  COLUMN_GET(columns, 'col_fam:column1') as col1,  
  COLUMN_GET(columns, 'col_fam:column2') as col2,
FROM hbase_row
WHERE 
  row_id='hbase_row_id1' 
+------+------+
| col1 | col2 |
+------+------+
| val1 | val2 |
+------+------+

Mappatura per cella:

  • Consente un controllo più preciso della selezione delle versioni dei dati, delle famiglie di colonne, etc.
  • Produce un set di risultati "migliore" quando si selezionano diverse colonne arbitrarie (il client deve solo iterare sul resultset, senza dover decomprimere i blob delle Colonne Dinamiche).

Mappatura per riga:

  • più semplice da usare se si seleziona un set di colonne predefinito
  • permette join su diverse colonne (con la mappatura per cella occorre eseguire una [inefficiente?] auto-join se si desidera ottenere una join tra righe in una tabella hbase e alcune altre cose).

5. Interfacciarsi con HBase

HBase è in Java, e la sua API client nativa è una libreria Java. Occorre interfacciarla con uno Storage Engine in C++. Le possibilità sono:

5.1 Usare Thrift

Occorre installare HBase per eseguire un server Thrift

5.2 Reimplement il protocollo di rete di HBase

  • Sembra essere un protocollo RPC creato appositamente.
  • Ecco una reimplementazione indipendente: https://github.com/stumbleupon/asynchbase. Sono 10K righe di codice Java, il che dà un'idea della complessità del protocollo di HBase
    • Sembra supportare solo un sottoinsieme delle sue funzionalità? Ad esempio, non è menzionato il supporto alle push down condition?
    • Guardare in HBaseRpc.java per una "Documentazione non ufficiale del protocollo RPC di Hadoop / HBase"

5.3 Usare il protocollo client JNI+HBase

  • non so quanto sia complesso
  • Mark ritiene che il carico di lavoro sia accettabile?

6. Integrità, transazioni, etc

  • HBase usa transazioni di un singolo record. Questo significa che lo Storage Engine HBase avrà caratteristiche simili a MyISAM? Per esempio, se una UPDATE multi-riga fallisce a metà, non c'è modo di tornare indietro.
  • Come devono essere fatte le scritture dei dati? Le UPDATE/INSERT dovrebbero usare checkAndPut per non sovrascrivere i dati già presenti?
  • Q: La scrittura è importante? (ad esempio, se la primissima versione ha un accesso in sola lettura, è utile?) A: Sì?

Commenti

Sto caricando i commenti......