標籤:分布式系統 統計 img 簡曆 時間戳記 ODB bigtable 資料庫執行個體 基於
1.前言
為了適應大資料應用情境的要求,Hadoop以及NoSQL等與傳統企業平台完全不同的新興架構迅速地崛起。而下層技術基礎的革命必將影響上層建築:資料模型和演算法。簡單地將傳統基於第四範式結構化關係型資料庫的模型拷貝到新的引擎上,無異於削足適履,不僅增加了大資料應用開發的難度和複雜度,又無法發釋放新架構的潛能。
該如何構建基於NoSQL的資料模型?現在能供參考的公開知識積累要麼是空虛簡單的一句“去正常化“或粗暴的寬表化(將query和應用需要訪問的所有欄位“排排坐“,放在一個有很多列的結構化表中),要麼是針對具體工具或具體情境的實現細節,(如《HBase權威指南》中對於如何設計HBase主鍵的探討)。沒有一個像編程的設計模式一樣的,在模型架構層面可以遵循的方法論。
在比較不同的NoSQL資料庫時,通常使用功能以外其他各種指標,如可擴充性、效能和一致性。由於這些指標通常是使用NoSQL的初衷,所以無論從理論的角度還是實踐的角度被深入地研究了,而像CAP定理這樣的分布式系統基礎結論也同樣適用於NoSQL系統。另一方面,在NoSQL的資料模型領域,卻還沒有很好地研究過,也缺乏關聯式資料庫中那種系統性的理論。
我在這篇文章中從資料建模的角度對NoSQL家族系統做了比較簡單的比較,並簡要介紹幾種常見建模技術。
2.NoSQL資料模型視圖
要探索資料建模技術,必須先從系統性的NoSQL資料模型視圖著手,這多多少少能協助我們揭示其發展趨勢以及相互之間的關係。描繪了主要NoSQL家族系統的虛擬“進化”過程,即KVStore for Redis,BigTable類型的資料庫,文檔資料庫,全文檢索搜尋引擎,資料庫和圖形資料庫:
首先,我們應該注意到,一般意義上講,SQL和關係型模型都是在很久以前就被設計出來,目的是為終端使用者互動之用。這種面向使用者的性質有極深的影響:
終端使用者往往對匯總報表資訊感興趣而不是單獨的資料項目,因而SQL這方面做了大量的工作。
不能指望作為自然人的使用者能顯式地控制並發性、完整性、一致性或者資料類型有效性。這就是為什麼SQL竭力關注於事務保證、schema和參照完整性。
另一方面,軟體應用程式往往對在資料庫內部做彙總沒有太大的興趣,而且至少在許多情況下,程式能夠自己控制完整性和有效性。除此之外,剔除這些功能對於效能和可擴充性儲存的影響極其重要。
新資料模型的演變開始了:
鍵-值儲存是一個非常簡單,但非常強大的模型。下面所描述的許多技術都完全適用於這個模型。
索引值模型最致命的缺點之一就是不適合按範圍處理主鍵的情境。有序的鍵-值模型突破了這一限制,並顯著提高了彙總能力。
有序的鍵-值模型非常強大,但它不提供任何針對值(value)的建模架構。在一般情況下,值的建模可以由應用程式完成,但BigTable風格的資料庫想得更加周到,它可以將值按照映射的映射的映射(map-of-maps-of-maps)進行建模,說得明確點,分別是列簇(column family)、列(column)和時間戳記化的版本。
文檔資料庫對BigTable模式提出兩個明顯的改善。第一,值可以被聲明為任意複雜的schema,而不僅僅是一個映射的映射(map-of-maps)。第二,至少有一些產品實現了被資料庫管理的索引。就這個意義上來講,全文檢索搜尋引擎也可以同樣被認為提供了靈活的schema和自動化的索引。他們之間主要區別在於,文檔資料庫是根據欄位名對索引進行編組,而搜尋引擎是使用欄位值對索引編組。值得注意的是像Oracle Coherence這樣的鍵-值儲存系統增加了索引和內嵌入口處理器的功能,正逐步向檔案資料庫演化。
最後,圖形資料模型可以被視為有序的鍵-值模型朝另外一個方向的進化。圖形資料庫允許對業務實體進行非常透明的建模(這個東西取決於那個東西),而分層建模技術在這方面用的是另外的資料模型,但也可與之媲美。圖形資料庫和檔案資料庫息息相關,因為許多實現允許建模的值是映射或者文檔。
3.NoSQL資料建模的一般注意事項
與關係型建模不同,NoSQL資料建模往往是從特定查詢的應用開始:
關係型建模是典型地被手上可用資料的結構所驅動。設計主要圍繞著的是“我有什麼樣的答案?”
NoSQL資料建模通常由特定應用的訪問模式所驅動,比如需要支援的查詢類型。設計主要圍繞著的是“我有什麼問題?”
NoSQL資料建模往往比關聯式資料庫建模需要更加深入地瞭解資料結構和演算法。在這篇文章中,我介紹了幾個著名的資料結構,他們雖然非NoSQL所特有,但對於實際的NoSQL建模非常有用。
資料複製和去正常化是一等公民。
關聯式資料庫在對分層或圖形資料進行建模和處理時不是很方便。圖形資料庫顯然是這個領域的完美解決方案,但實際上大多數的NoSQL也都非常善於解決這樣的問題。這就是為什麼這篇文章為分層資料建模單獨寫了一個章節。
雖然資料建模技術基本上和具體實現無關,但我還是列出了在寫這篇文章時我能想到的產品:
KVStore for Redis:Oracle Coherence,Redis,Kyoto Cabinet
BigTable風格的資料庫: Apache HBase,Apache Cassandra
文檔資料庫: MongoDB,CouchDB
全文檢索搜尋引擎: Apache Lucene,Apache Solr
圖形資料庫:Neo4j,FlockDB
4.概念技術
本節專門介紹NoSQL資料建模的基本原則。
1、 去正常化(Denormalization)
可以將去正常化定義為把相同的資料複製到多個文檔或資料表中,這樣可以簡化/最佳化查詢處理,或者讓使用者資料能匹配一個特定的資料模型。在本文的大多數技術用到了這樣或那樣的去正常化。
一般來說,去正常化用於以下的折衷:
查詢的資料量或每次查詢IO**與總資料量的折衷。去正常化可以將一個查詢所需的所有資料群組合起來存放到同一個地方。這通常意味著對相同資料的不同的查詢會訪問不同的資料群組合。因此,資料需要被複製多份,也就意味著增加了總資料量。
處理複雜性與總資料量的折衷。建模時的正常化和相應查詢的串連(join)明顯增加了查詢處理器的複雜度,在分布式系統中尤為明顯。去正常化允許將資料按照查詢友好的方式儲存,從而簡化查詢的處理。
適用性:KVStore for Redis,文檔資料庫, BigTable風格的資料庫
2、 彙總(Aggregates)
所有主流NoSQL都提供了這樣或那樣的鬆散schema(soft schema)支援:
KVStore for Redis和圖形資料庫通常不對值進行約束,所以值可能是任意格式。另外,也可以通過使用按鍵組合將一個業務實體表示為多條記錄。例如,可以將一個使用者帳戶建模為UserID_name,UserID_email,UserID_messages等按鍵組合表示的一個實體集合。如果使用者沒有電子郵件或訊息,然後相應的實體不會被記錄。
BigTable模式也支援鬆散schema,因為一個列簇是可變的列集合,一個儲存格又能儲存不定數目的資料版本。
文檔資料庫天生就沒schema,雖然某些文檔資料庫允許在資料輸入時使用使用者定義的schema進行驗證。
鬆散schema允許使用複雜的內部結構(嵌套實體)構造實體的類,也允許改變特定實體的結構。這個更能帶來了兩個重要的便利:
通過嵌套的實體,最小化了一對多的關係,也因此減少了串連(join)。
異構業務實體的模型可以使用一個文檔集合或者一個資料表。鬆散schema掩藏了這種建模和業務實體之間“技術”上的差異。
我們用下面的圖來說明這些便利。該圖描繪了對電子商務領域中一個產品實體進行的建模。首先我們可以認為所有的產品都有一個ID、價格(Price)和描述(Deion)。進一步來看,我們發現不同類型的產品有不同的屬性,書包含作者資訊,而牛仔褲有長度屬性。這些屬性中間的某些屬性天生就有一對多或這多對多的特性,比如音樂唱片中的曲目。
更進一步來看,可能有些實體不可能使用固定的類型進行建模。例如,不同品牌的牛仔褲的屬性是不固定的,而每個製造商出產的牛仔褲的屬性也是不一致的。在正常化的關係型資料模型中雖然這些問題都可以解決,但方法很猥瑣。鬆散schema軟架構允許只使用一個彙總(Aggregation)(產品)就能對所有類型的產品及其屬性進行建模:
內嵌的去正常化會在效能和一致性上對更新操作造成很大的影響,所以要特別注意更新過程。
適用性:KVStore for Redis,文檔資料庫, BigTable的風格資料庫
3、 應用端串連(Application Side Joins)
很少有NoSQL解決方案支援串連。NoSQL“問題導向”性質的後果就是,通常在設計時處理join,而關係型模型是在執行查詢時處理join。查詢時處理join幾乎肯定會帶來效能上的損失,但在許多情況下,可使用去正常化和彙總,即嵌入嵌套實體來避免join。當然,join在許多情況下是不可避免的,而且應該由應用程式處理。主要的用例:
多對多關係往往是通過連結(link)建模的,這需要join。
彙總操作往往不適合內部實體會被頻繁修改的情境。通常更好的辦法是將發生的事情作為一條新的記錄保留,並在查詢的時候將所有記錄做join,而不是去更改值。例如,對於一個資訊系統而言,可以用嵌套包含了Message實體的User實體來建模。但是,如果會經常地添加訊息,更好的辦法可能是把Message提取出來作為獨立實體,並在查詢時再將其與User進行串連:
適用性:KVStore for Redis,文檔資料庫, BigTable風格資料庫,圖形資料庫
5.一般建模技術
在本節中,我們將討論適用於各種NoSQL實現的一般建模技術。
1、 原子彙總(Atomic Aggregates)
許多NoSQL解決方案提供了有限的事務支援,雖然有些NoSQL不支援。在某些情況下,人們還可以使用分布式鎖或應用程式管理的MVCC機制實現事務行為,但常見的是使用彙總技術來對資料建模,以保證一些ACID特性。
強大的交易處理機制對於關係型資料庫而言是不可或缺的,其中原因之一就是正常化的資料通常需要在多個地方進行更新。另一方面,彙總允許一個單個業務實體儲存為一個檔案,行或索引值對,從而可以對其進行原子性的更新:
當然,做為一種資料建模技術,原子彙總並不是一個完善的事務型解決方案,但如果儲存能提供原子性、鎖或者TAS(test-and-set,測試並設定)指令上的一些擔保,那原子彙總就是可行的。
(譯者註:即將需要事務性操作的業務資料彙總放在一起,儲存在一個NoQSQL提供或者應用能提供原子性操作的資料結構中。使用HBase時,將某個使用者某個業務的所有資料,如,用一行儲存就是這種模式的應用。)
適用性:KVStore for Redis,文檔資料庫, BigTable風格資料庫
2、 可枚舉主鍵(Enumerable Keys)
也許無序鍵-值資料模型最大的好處就是可以通過將主鍵雜湊的辦法把實體資料分別儲存在多個伺服器上。排序使事情變得更加複雜,但是即使儲存不提供這樣的功能,有時應用程式也能利用到有序主鍵的優勢。讓我們將對電子郵件建模作為一個例子:
某些NoSQL儲存提供原子計數器,能產生一個順序化的ID。在這種情況下,可以使用userID_messageID作為一個複合鍵來儲存訊息。如果最新的訊息ID是已知的,那就可以遍曆以前的訊息。另外,對於任何一個給定的訊息ID,也可以向前或向後進行遍曆。
也可以將訊息分桶(bucket),例如,每天的資料放到一個桶裡。這樣就允許從任何指定日期或當前日期開始,向前或向後遍曆一個郵箱。
適用性:KVStore for Redis
(譯者註:能利用主鍵的一些自然或業務維度特徵,將隨機讀寫轉換為順序讀寫能提高遍曆效能,同時能方便應用邏輯編寫。但需要注意對分布式部署時並發寫的影響以及對於業務的過度耦合。對於無序主鍵和有序主鍵的討論可以參見《HBase權威指南》中Schema設計章節。)
3、 降維(Dimensionality Reduction)
降維這種技術允許將一個多維資料模型映射到一個鍵-值模型或其他非多維模型。
傳統的地理資訊系統使用四叉樹(Quadtree)或R樹(R-tree)的某種變形來做索引。這些結構需要就地完成更新操作,因此在資料量很大時,維護開銷相當的大。另一種方法是對這個二維結構進行遍曆,並將其扁平化為一個普通的條目列表。使用這種技術的一個眾所周知的例子是Geohash。 Geohash使用類似Z形狀的路線來掃描整個二維空間,每次移動根據行進方向被編碼為0或1。交錯位的經度和緯度上的變更移動以及移動。編碼過程在中進行了說明,其中黑色和紅色位分別代表經度和緯度:
,Geohash的一個重要特性是能夠通過這種逐位編碼的近似程度來估計地區之間的距離。Geohash編碼允許使用簡單普通的資料模型來儲存地理資訊,比如用有序索引值儲存空間上的聯絡。[6.1]講述了BigTable中的降維技術。更多有關Geohash及其相關技術的資訊可以在[6.2]和[6.3]中找到。
適用性:KVStore for Redis,文檔資料庫, BigTable風格的資料庫
(譯者註:通過交織編碼方式來能將原本需要多維度標示的資料,如cube,儲存到一維的KVStore for Redis系統中,這是一種非常重要的建模模式:提供了不同縮放等級下在多維空間中鄰接的資料仍然順序儲存,遍曆高效;同時不同主鍵從前向後的相似性和空間距離的遠近相一致,能通過索引值的簡單順序比較判斷其位置“相似性”。
它的應用遠遠不只地理資訊的表示,有多個維度屬性不同粒度的資料表示都能用到這個技術,比如線下銷售交易資料通常都有時間和分支結構資訊,使用者查詢銷售資料時通常先查詢一個大的地區,而且使用的時間段跨度也比較大,需要查詢更具體的資訊時同時縮小地區和時間來逐步定位細節,這時分公司和時間就好比是經度維度兩個軸,通過交織時間和分支結構編碼的方式建立主鍵,能很好的滿足這種情境。而單純的使用時間或分公司做主鍵或索引卻都不能適應上述情境。)
4、 索引表(Index Table)
索引表是一個非常簡單的技術,它在內部不支援索引的儲存上提供索引的支援。這類儲存中最重要的一類就是BigTable風格的資料庫。索引表的想法是按照訪問模式所需要的鍵來建立和維護一個特殊的表。例如,有一個主表,儲存了可以通過使用者ID直接存取的使用者帳戶。查詢指定城市的所有使用者可以通過一個額外的用城市做主鍵的表來支援:
索引表可以在每一個主表記錄更新時更新或者使用批模式更新。無論哪種方式,它會導致額外的效能損失,並帶來資料一致性上的問題。
可以認為索引表是一種對關聯式資料庫執行個體化視圖的類比。
適用性: BigTable風格的資料庫
5、 組合主鍵索引(Composite Key Index)
複合主鍵是一個非常通用的技術,但尤其在主鍵有序儲存時極其有用。複合主鍵結合二次排序就能建立起一種多維索引,這和前面所述的降維技術在原理上是類似的。例如,假設我們有一組記錄,每個記錄是一個使用者統計資料。如果我們要按使用者來自的地區來彙總這些統計資料,我們可以使用這樣的主鍵格式(State:City:UserId)。如果主鍵的儲存支援通過部分匹配來選取範圍(如BigTable風格的資料庫),那就可以在特定的州(State)或者城市(City)的記錄上做遍曆:
適用性: BigTable風格的資料庫
(譯者註:在使用HBase這類BigTable風格的資料庫時,如果主鍵只使用一個欄位/域的資訊簡直是暴殄天物。使用多欄位組合主鍵不僅能解決主索引值不能重複的問題,還能提高對於資料子集二次尋找時的效能。)
6、 組合主鍵的彙總
複合主鍵不僅可用於作索引,還可以為不同類型分組。讓我們來看一個例子。有一個巨大的日誌數組,記錄了互連網使用者和他們訪問不同的網站(點擊流)的資訊。我們的目標是對於每個唯一使用者計算出每個網站的點擊數量。這類似於下面的SQL查詢:
我們可以使用將使用者名稱當首碼的組合主鍵來對這種情況進行建模:
我們的想法是將一個使用者的所有記錄放置在一起,這樣就可能將其全部載入到記憶體中(一個使用者不會產生太多的事件),並使用雜湊表或其他方法消除掉重複的網站。另一種技術是將使用者做為主鍵,每次事件到達時將網站添加到這條資料的後部。然而,在大多數實現中,修改資料一般比插入資料的效率低。
適用性:有序KVStore for Redis,BigTable風格的資料庫
7、 倒排搜尋-直接彙總
這種技術更像是資料處理模式,而不是資料建模。然而,資料模型也受這種模式使用的影響。這種技術的主要思想是使用索引來找到滿足條件的資料,但彙總操作還是使用原來的方式或者全表掃描。讓我們來考慮一個例子。有一堆的日誌資料記錄了互連網使用者和他們訪問不同的網站(點擊流,click stream)的資訊。假設每條記錄都包括使用者ID、使用者所屬類別(男性、女性、博主(Blogger)等)、使用者來自的城市以及訪問的網址。我們的目標是找出滿足條件(網址、城市等)的觀眾,並將這堆觀眾(如符合標準的使用者集合)中出現的不同使用者按類別歸類。
很明顯,滿足條件的使用者可以通過像{類別->[使用者ID]}或{網站->[使用者ID]}這樣的倒排索引表非常高效地尋找到。使用這樣的倒排索引,可以得到所要的使用者ID的交集或者並集(如果使用者ID被儲存為排有序的列表或位元影像,這就可以非常高效地實現),從而獲得目標使用者。但如果目標使用者是使用類似這樣的聚集查詢描述的:
那如果類別的數量很大,就不能用倒排索引做有效處理。要解決這個問題,可以用{使用者名稱->[分類集合]}的形式建立直接索引(Direct Index),然後遍曆它來建立最終報表。此架構示意如:
最後需要提示的是,我們需要知道如果隨機地訪問目標使用者中每一個使用者ID所對應記錄,這樣做的效率可能很低。可以通過利用批量查詢處理解決這個問題。這意味著,一些數量的使用者集可以被預先計算(針對不同的報表條件),然後可以通過對直接索引表或倒排索引表進行一次全表掃描從而計算出這批目標使用者的所有報告。
適用性:KVStore for Redis,BigTable風格的資料庫,文檔資料庫
分層建模技術(Hierarchy Modeling Techniques)
(譯者註:如果需要通過屬性或者類別來快速尋找對應的實體,這樣的索引比不可少。現在很火的用大資料分析使用者畫像不就需要像{使用者標籤->使用者群}這樣的結構麼。)
8、 樹彙總(Tree Aggregation)
可以將一條單獨的記錄或者檔案的模型建成樹,甚至是任意的圖(通過去正常化)。
在樹會被一次性訪問的情境中(例如,部落格的整個評論樹會被讀取,並顯示在一篇文章的頁面中),這個技術很高效。
搜尋和訪問任意條目可能有問題。
在大多數NoSQL的實現中,更新操作的效率低下(同相互獨立的節點相比)。
適用性:KVStore for Redis,文檔資料庫
(譯者註:這是文檔型NoSQL的天下,對於無需跨樹join的情境,不僅讀寫效率高,而且能很好的支援局部性的事務應用。)
9、 鄰接列表(Adjacency Lists)
鄰接列表是一個簡單的圖型建模方法——每個節點作為一個單獨記錄建模,其中有包含直接祖先的數組或包含後代的數組。它允許通過其父母或子女的標識符來搜尋一個節點,當然也可以通過查詢一次前進一步的方式來遍曆一個圖。無論對於深度優先或廣度優先遍曆而言,要在整個子樹中找到一個給定的節點,這種方法的效率通常不高。
適用性:KVStore for Redis,文檔資料庫
10、 物化路徑
物化路徑是一種有助於避免在樹型結構上做遞迴遍曆的技術。也可以認為這是一種去正常化的技術。其設計思想是用一個節點所有的父節點或者子女節點來標識該節點,這樣就有可以不用遍曆而得到一個節點的所有祖先節點或者衍生節點:
因為這個技術可以將階層轉換成扁平化的文檔,所以它對於全文檢索搜尋引擎特別地有用。從中可以看出,在男鞋(Men’s Shoes)類別下所有的產品或者子類別可以簡單的通過查詢一個類別名稱而得到。這個查詢很短。
物化路徑的儲存方式可以是一個ID的集合,或者一個是包含級聯ID的字串。後一種方式允許使用Regex,來尋找那些指定部分的路徑符合某種條件的節點。此種方法如在(路徑也包括了節點自身)所示:
適用性:KVStore for Redis,文檔資料庫,搜尋引擎
11、 嵌套集合(Nested Sets)
在對類似樹型結構進行建模時,嵌套集合是個標準的做法。它在關聯式資料庫中廣泛被使用,然而它也完全適用於KVStore for Redis和文檔資料庫。其設計思想是在用數組來儲存樹的葉子節點(譯者:每個葉子節點對應數組中的一個位置下標),並將每個非葉結點映射為一個葉子節點的範圍,這個範圍就是開始葉子節點和結束葉子節點在數組中的位置下標。如所示:
這個結構對於不變資料來講非常有效,因為它佔用的記憶體小,並且可以在不遍曆樹的情況下得到一個給定節點的所有葉子節點。然而,因為增加一個葉子節點會帶來位置下標的大量更新,所以插入和更新的操作代價是相當的高。
適用性:KVStore for Redis,文檔資料庫
(譯者註:適用於字典表、按日期排序的曆史日誌資料表等。)
12、 扁平化嵌套檔案:欄位名稱編號
搜尋引擎通常工作在扁平化的文檔之上,即每個文檔由兩個扁平列表組成,這兩個列表分別記錄了欄位的名稱和它對應的值。資料建模的目標是將業務實體映射為簡單無結構的文檔,但如果實體內部的結構很複雜,這就難辦了。一個典型的困難就是階層模型,比如要將內部嵌套了文檔的文檔映射為簡單無結構的文檔。讓我們來考慮下面的例子:
每一個業務實體是一份某種形式的簡曆,其中包含了這個人的名字,和枚舉了他或她所具有的技能以及相應技能水平的列表。為這樣的實體建模的一個很顯然的方式就是是建立的簡單無結構文檔,裡麵包含Skill和Level欄位的列表。這種模式允許通過技能或水平來搜尋某個人,但將這兩個欄位聯合起來搜尋卻容易導致虛假匹配,正如所述。(譯者:簡單的AND操作不能感知技能以及其水平的對應關係。)
在[4.6]中提出了一種解決這個問題的方法。這項技術的主要思想是將每一項技能以及相應的水平聯合起來組成一個配對(pair),並使用下標標識成為Skill_i和Level_i。在搜尋時需要同時查詢所有這些對值(查詢中OR條件陳述式的個數是和一個人所能具有的技能的最大值相同):
這種方法實際上沒有可擴充性,因為隨著嵌套結構的數目的增長會迅速增加查詢的複雜度。
適用性:搜尋引擎
(譯者註:如果你對於使用索引來加速查詢無計可施,尤其對使用者將來如何查詢資料一無所知,無法在設計模型時進行最佳化時,這就是最後的稻草:使用全文檢索搜尋工具。它至少能在可接受的時間內能查出點東西來^o^。)
13、 扁平化嵌套檔案:近似查詢
在[4.6]中還描述了另一種可以解決嵌套檔案問題的技術。它的想法是使用近似的查詢,將文檔中單詞之間的距離限制在可以接受的距離範圍以內。在下面的圖中,所有的技能和水平都被索引到了一個叫SkillAndLevel的域。使用“Excellent”和“Poetry”進行查詢表示的話,就會尋找到這兩個單詞鄰接的條目:
[4.3]講述了在Solr之上使用這種技術的一個成功案例。
適用性:搜尋引擎
(譯者註:同上,但查詢召回率高,會出現假匹配,有髒資料。)
14、 批量圖處理
在瀏覽一個指定節點的相鄰節點或瀏覽兩個或幾個節點之間的關係時,像Neo4j這樣的圖形資料庫的效能出奇的好。然而,通用圖形資料庫對大圖做全域性處理不是很高效,因為擴充性不好。分布式圖形處理可以使用MapReduce或者訊息傳遞(Message Passing)模式來實現。我以前的一篇文章就介紹了一種這樣的模式。這種方法使用了KVStore for Redis、文檔資料庫和BigTable風格的資料庫,能處理大型的圖形。
大資料架構師必讀的NoSQL建模技術