如何高效地將SQL數據映射到NoSQL存儲系統中

jopen 9年前發布 | 15K 次閱讀 NoSQL數據庫 NOSQL

通常來說,我們都知道:

  • SQL數據庫只限在單機上運行,但它提供了更強的事務管理、schema與查詢功能。
  • NoSQL數據庫為了伸縮性與容錯性的目的,放棄了事務管理與schema。

而FoundationDB的SQL層結合了這兩個方面:它首先是一個開源的SQL數據庫,能夠線性地伸縮與提升容錯性,并且還具有真正的ACID事務功能。曾經互不相容的兩種特性,現在已融合在一個統一的系統中。

對于處于以下幾種情況的公司來說,這一特性是非常重要的:

  • 新的項目要為大規模的伸縮性進行計劃。
  • 現有的項目遇到了數據庫伸縮性的瓶頸。
  • 現有的許多項目希望能用一個唯一的、容錯性強的數據庫抽象層統一工作模式。

在本文中,我將為讀者介紹FoundationDB,并解釋FoundationDB的SQL層是怎樣將SQL數據映射到FoundationDB中的鍵-值存儲后臺系統中的。

NoSQL數據庫 ——FoundationDB的鍵-值存儲系統

FoundationDB是一個分布式的鍵-值存儲系統,支持全局ACID事務操作,并且性能出眾。在安裝系統時,可以指定數據分發的級別。數據分發為容錯性提供了支持:當某個服務器或網絡的某部分產生故障時,數據庫仍然可以正常操作,你的應用也不會受到影響。

鍵-值與SQL架構

我們開發的這套架構能夠在鍵-值存儲系統上支持多個層,每個層都能夠在FoundationDB的基礎上提供一套不同的數據模型,例如SQL數據庫、文檔數據庫或圖形數據庫。許多使用者也自行創建了自定義的層。

下圖中列出架構中的了關鍵部分。處于最底層的是FoundationDB集群,無論集群的實際大小如何,對它的操作與一個單獨的邏輯數據庫并沒有分別。SQL層則以一種無狀態的中間層方式運行在鍵-值存儲系統之上。這一層通過SQL與應用程序進行通信,并使用FoundationDB的客戶端API 與鍵-值存儲系統進行通信。由于SQL層是無狀態的,因此可以并行地運行任意數據的SQL層。

SQL層為鍵-值存儲系統帶來了如Google的F1般的能力

SQL層是對SQL與鍵-值存儲API進行轉換的一套邏輯嚴密的層。首先,SQL層會從一條SQL語句開始,將其轉換為最高效地鍵-值操作。這種方式類似于編譯器將代碼轉換為低級別的執行格式。并且,這種轉換是完全符合ANSI SQL 92標準的。開發者可以將該功能與ORM、REST API進行接合,或者直接使用SQL層的命令行界面進行調用。從代碼的角度來說,SQL層與鍵-值存儲是完全分離的,它是通過FoundationDB的 Java綁定方式與鍵-值存儲進行通信的。感興趣的讀者可以查看FoundationDB的SQL層在GitHub上的代碼庫,其代碼是完全開源的。眼下唯一能夠和這套系統進行比較的是Google的F1,后者是一套基于該公司的Spanner技術所創建的SQL引擎。

如以下的簡單圖例所示,SQL層是由一系列組件所組成的。應用程序通過某種受支持的SQL客戶端向SQL層發送查詢語句,在解析之后轉換為一棵計劃節點樹。優化器(Optimizer)會計算最佳的執行計劃,并以一棵操作符樹的方式表現出來,隨后由執行框架(Execution Framework)運行。在執行階段,對數據的請求將被發送到存儲虛擬(Storage Abstraction)層,這一層通過使用Java的鍵-值API在數據與FoundationDB集群之間進行傳輸。數據庫模型將存放在 Information Schema層中,這一層將被其它多個組件所調用。

將SQL數據映射到鍵-值存儲系統

SQL層需要管理兩種類型的數據,首先是信息Schema的元數據,它負責描述所創建的表與可用的索引。其次,它還需要存儲實際的數據,包括表內容、索引及序列。我們首先來描述一下這些數據是如何保存在鍵-值存儲系統中的。

本質上講,每個鍵都是對應了某張表中的特定行的指針,而值則包含了該行的數據。鍵的分配是由Table-Group所決定的,它是包含了一個或多個表的組。稍后會對這個概念的細節進行更深入的講解。SQL層會通過使用鍵-值存儲目錄層為每個Table-Group創建一個目錄,存儲目錄層是為用戶管理鍵空間的一個工具,它為每個獨立的目錄分配一個簡短的字節數組,作為該目錄的唯一鍵。同時,它也維護著其它元數據,以實現通過名稱進行查找的功能。

下面這個例子演示了如何創建目錄的映射,通過以下語句分配鍵。

CREATE TABLE schema_a.table1(id INT PRIMARY KEY, c CHAR(10));
CREATE TABLE schema_a.table2(id INT PRIMARY KEY);

在鍵-值存儲系統中有一些預定義的目錄:

Directory

Tuple

Raw Key

sql/ (9) \x15\x09
sql/data/ (3) \x15\x03
sql/data/table/ (31) \x15\x1F
sql/data/table/schema_a/table1/ (215) \x15\xD7
sql/data/table/schema_a/table2/ (247) \x15\xF7

在存儲數據時,可以選擇使用以下三種格式中的一種:“元組(Tuple)”、“原始數據(Row_Data)”或者是“Protobuf”。如果使用默認的Tuple存儲格式,那么每一行內容都將保存為一個單獨的鍵-值對,鍵是通過連接以下字符串所生成的元組:目錄前綴、該表在Table- Group中的位置,以及主鍵。而值的內容則是由該行中的所有列所組成的一個元組。

舉例來說,以下代碼對之前創建的表進行操作,產生對應的鍵與值。

INSERT INTO schema_a.table1 VALUES (1, 'hello'), (2, 'world');
INSERT INTO schema_a.table2 VALUES (5);

Raw Key

Tuple Key

Raw Value

Tuple Value
\x15\xD7\x15\x01\x15\x01 (215, 1, 1) \x15\x01\x02hello\x00 (1, ‘hello’)
\x15\xD7\x15\x01\x15\x02 (215, 1, 2) \x15\x02\x02world\x00 (2, ‘world’)
\x15\xF7\x15\x01\x15\x05 (247, 1, 5) \x15\x05 (5)

了解了鍵-值存儲系統中鍵的結構之后,你就能夠從存儲系統中直接讀取數據了。我們將使用FoundationDB的Python API來演示這一功能。在SQL層中,鍵與值是通過“.pack()”方法進行編碼,并通過“.unpack()”方法進行解碼的。下面的示例為你演示如何獲取并解碼數據。

import fdb  fdb.api_version(200) 
db = fdb.open() 
directory = fdb.directory.open(db,('sql','data','table','schema_a','table1'))
for key, value in db[directory.range()]:         print fdb.tuple.unpack(key), ' --> ', fdb.tuple.unpack(value)

以上代碼會輸出類似下面的結果:

(215, 1, 1) --> (1, u'hello') 

(215, 1, 2) --> (2, u'world')

現在讓我們再來近距離觀察一下Table-Group。每個獨立的表都屬于一個單獨的組,如果某張額外的表能夠創建一個對第一張表的“組外鍵”引用,那么它也能夠加入到同一個組中。當我們為某張表創建組外鍵時,字表將與父表所在的目錄進行交互。字表將成為Table-Group的一部分,在源表之后進行命名。這兩張表的數據在將同一個目錄中進行交互,這保證了范圍掃描的高速,并且在Table-Group之內訪問對象及表連接的開銷極小。為了演示這一特性,我們將繼續之前的示例,這一次的SQL語句如下:

CREATE TABLE schema_a.table3(id INT PRIMARY KEY, id_1 INT, GROUPING FOREIGN KEY (id_1) REFERENCES schema_a.table1(id));
INSERT INTO schema_a.table3 VALUES (100, 2), (200, 2), (300, 1);

該語句將返回以下結果:

directory = fdb.directory.open(db,('sql','data','table','schema_a','table1'))
for key, value in db[directory.range()]:     print fdb.tuple.unpack(key), ' --> ', fdb.tuple.unpack(value)
(215, 1, 1)          -->  (1, u'hello')
(215, 1, 1, 2, 300)  -->  (300, 1) 
(215, 1, 2)          -->  (2, u'world')
(215, 1, 2, 2, 100)  -->  (100, 2)
(215, 1, 2, 2, 200)  -->  (200, 2)

由于第三張表的鍵都處于第一張表中各行的命名空間范圍內,因此第三張表中所有插入的行都能夠與第一張表的行相關聯。鍵中的兩個額外的值分別對應了 Table-Group中的位置以及第三張表中的主鍵。對表1與表3通過引用鍵進行連接也無需通過標準的連接操作實現,直接通過線性掃描就語句了。這種排序方式比起傳統的關系型數據庫系統有著極大的優勢。

由于鍵都已經經過排序,因此索引可以直接利用這一點所帶來的便利性。所有的表索引只包含一個鍵值,其中包括兩部分內容。每個索引都創建于該表所屬的目錄之下,一個名為index的子目錄中,這是該鍵元組的第一部分內容。第二個部分是一個組合,首先是該索引所對應的各個列的值,之后則是指定這一行所必須的列的值。

舉例來說,我們可以為這張表的c列創建一個索引。

CREATE INDEX index_on_c ON schema_a.table1(c) STORAGE_FORMAT tuple;

接下來使用Python讀取這個索引的內容,我們需要在Python解釋器中加入以下內容:

directory = fdb.directory.open(db, ('sql', 'data', 'table', 'schema_a', 'table1', 'index_on_c'))
for key, value in db[directory.range()]:     print fdb.tuple.unpack(key), ' --> ', fdb.tuple.unpack(value)

這段代碼會輸入類似于下圖中的內容,顯示了鍵的兩個組成部分:即該索引所在的目錄的字節值,以及創建索引的c列的值加上主鍵的值。最后一個部分將被索引的值鏈接到某個特定的行,而該索引鍵所對應的值為空。

(20127, u'hello', 1) --> ()
(20127, u'world', 2) --> ()

如果要對SQL層的行為進行更多的控制調整,可以使用以下三種存儲格式:一是之前描述過的元組格式,一是列鍵格式,以及protobuf格式。列健格式會為某一行的每個列值創建一個獨立的鍵-值對。而protobuf存儲格式為會每一行創建一個protobuf消息。

接下來還需要對元數據進行存儲與組織。SQL層使用protobuf消息與基于SQL的數據的結構進行通信。這個結構是由schema、組、表、列、索引與外鍵等對象共同組成的。

SQL與NoSQL的混合模式

如果在應用程序級別使用只讀的鍵-值API,那么SQL層就能夠在客戶端進行直接訪問。可以通過鍵-值API直接訪問數據,但如果增加或改寫了 SQL層所用的關鍵數據,那就很可能破壞系統的運行。這里例舉一些可能會產生的問題:缺乏對索引的維護、缺乏應有的限定,以及忽略了對數據及元數據的版本維護。而這種方式的好處,哪怕是在進行數據讀取時也并不明顯,因為SQL層本身的額外開銷就非常小。因此總的來說,性能的開銷主要取決于網絡延遲。

結論

SQL與NoSQL的結合使用能夠相互利用兩者的優點。FoundationDB的鍵-值存儲系統為SQL層帶來的好處包括可伸縮性、容錯性及全局 ACID的事務屬性。你的應用程序同樣也能從中受益,因此趕緊嘗試一下吧!對應那些要執行大量的小批數據讀取及寫入的應用程序來說,FoundationDB提供了一個高伸縮并且安全的解決方案,并且可以任意使用SQL或NoSQL。

來源:InfoQ - 邵思華

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!