Cassandra數據模型設計最佳實踐
本文是Cassandra數據模型設計第一篇(全兩篇),該系列文章包含了eBay使用Cassandra數據模型設計的一些實踐。其中一些最佳實踐我們是通過社區學到的,有些對我們來說也是新知識,還有一些仍然具有爭議性,可能在要通過進一步的實踐才能從中獲益。
本文中,我將會講解一些基本的實踐以及一個詳細的例子。即使你不了解Cassandra,也應該能理解下面大多數內容。
說說Cassandra在ebay的使用情況
我們嘗試使用Cassandra已經超過1年時間了。Cassandra現在正在服務一些用例,涉及到的業務從大量寫操作的日志記錄和跟蹤,到一些混合工 作。其中一項服務是我們的“Social Signal”項目,支撐著ebay的pruduct pages里like/own/want特性。我們開發的一些用例已經上線運行,但更多的還是處于開發階段。
我們的Cassandra集群規模并不龐大,但正在穩步的增長中。在過去幾個月里,我們共部署了幾十個節點,它們分布在幾個跨機房的小型集群中。你可能會 問,為什么要多個集群?我們通過的職能部門和業務來劃分集群。相同職能部門的相同業務的用例共享一個集群,但它們存在于不同的keyspaces中。
RedLaser, Hunch和其它ebay的合作伙伴也在嘗試cassandra解決現實中各種問題。除了Cassandra,我們也在使用MongoDB和Hbase,本文中我不會討論它們,但我相信它們都有各自的優點。
我相信此時你一定有很多問題,在這篇文章里暫時不會一一說明。在即將到來的Cassandra Summit大會,我將更詳細的講解我們每個用例場景,數據模型和多數據中心部署,以及經驗教訓和其它知識。
本文重點講述我們在ebay應用的Cassandra數據模型設計最佳實踐。下面讓我們先看看這系列文章會用到的一些術語。
術語和約定
-
術語“Column Name” 和 “Column Key”被認為是一樣的。同樣的,“Super Column Name” 和 “Super Column Key”也認為是相同的。
-
下圖表示一個 Column Family (簡稱CF)中的一個row
-
下圖表示一個 Super Column Family (簡稱SCF)中的一個row
-
下圖表示一個Column Family中一個row,它包含Composite Columns。Composite Columns的屬性通過分隔符’|’連接。請注意,這里看到的只是數據的表現形式,Cassandra內置了Composite Column,它是一個對象,并不是使用’|’作為屬性分隔符的字符串。(順便說下,本文不要求你掌握Super Column和Composite Column方面知識。)
基于上面的內容,讓我們開始第一個實踐吧!
不要把Cassandra model想象成關系型數據庫table
取而代之,應該把它想象成事一個有序的map結構。
對于一個新手來說,下面關系型數據庫術語常常被對應到Cassandra模型
這種對比可以幫助我們從關系型數據庫轉換到非關系型數據庫。但是當設計Cassandra column famiy的時候請不要這樣去類比。取而代之,考慮它是一個map中嵌入另一個map:外部map的key為row key,內部map的key為column key,兩個map的key都是有序的。如下:
雙擊代碼全選1SortedMap<RowKey, SortedMap<ColumnKey, ColumnValue>>why?
將column family想象成嵌套的并排序的map比關系型數據庫table描述的更為準確,它將幫助你正確的進行Cassandra模型設計。
How?
-
Map可以進行高效查詢,同時排序的特性可以進行高效column掃描。在Cassandra中,我們可以使用row key和column key做高效查找和范圍掃描
-
Column key的數量是很龐大的(譯者注:目前譯者所使用的Cassandra1.2.5版本,每個row支持最多20億個columns)。換句話說你,你可以擁有一個wide rows。
-
Column key自身可以存儲值,即你可以擁有一個沒有值的column。
如果集群使用Order Preserving Partitioner (OOP)策略進行數據存儲,就可以對row key進行范圍查詢。但是OOP大多數情況都不推薦使用(譯者注:將rowkey按照順序存儲到節點上,如果分區不均勻,將導致數據讀寫不均衡),所以你 可以認為外部的map是不排序的,如下:
雙擊代碼全選1Map<RowKey, SortedMap<ColumnKey, ColumnValue>>上面提到的”Super Column”,認為它們是一組column,這樣的話,兩級嵌套map就會像下面展示的一樣變為三級嵌套map:
雙擊代碼全選1Map<RowKey, SortedMap<SuperColumnKey,雙擊代碼全選1SortedMap<ColumnKey, ColumnValue>>>注意:
-
你需要傳遞timestamp給每個column value,因為Cassandra使用它做內部的沖突處理機制。但在建模過程中你可以忽略它(譯者注:在操作column的時候timestamp信息 會自動添加到column)。同時,不要考慮在你的程序中使用column的timestamp,因為它不是為你設計的,與Hbase不同,它們不會生成 新的version數據(譯者注:在Hbase中相同rowkey和column key的數據會保存多個version,而Cassandra會將相同數據覆蓋,timestamp只保存最后一次更新的時間)。
-
因為Super Column的性能問題和缺乏二級索引支持問題,Cassandra社區對它的使用曾有過強烈爭議。所以,推薦使用Composite Columns代替Super Column實現功能。(譯者注:使用Super Column,如果你要獲取其中一個columnvalue,則要掃描整個Super Column,這會導致查詢性能很糟糕)
圍繞著查詢模式進行Column Family建模
建模盡量從實體和它們的關系開始
-
與關系型數據庫不同,在Cassandra中通過創建二級索引或者編寫復雜SQL(使用joins, order by, group by)來新建或修改查詢不是件容易的事情。因為Cassandra具有很高的分布式特性,所以要先考慮查詢模式,然后再設計column family。
-
牢記前面提到的嵌入排序map數據結構,在考慮如何組織你的數據到map,以滿足快速查詢/排序/分組/過濾/聚合的要求。
在大部分情況下,實體和它們的關系是很重要的(特殊用例除外,如日志存儲或者其它時間序列數據)。如果我給你一個查詢模式,用于為一個電子商務網站創建 Cassandra模型,但不告訴你任何實體和它們的關系。你會有意或者無意的從查詢模式或者從你之前領域對象的理解找出實體和它們之間的關系(因為我們 是通過實體和關系來描述真實世界)。在設計數據模型時最好從實體和關系開始,然后使用反范式化和冗余的方式繼續圍繞查詢模式建模。如果這聽起來有些讓人困 惑,通過后面的詳細例子就可以理解。
注意:在建模的時候考慮以下幾點會很有幫助。區分頻次大的查詢和頻次小的查詢,有些查詢可能只被查詢幾千次,其它可能被查詢數十億次;還要考慮哪些查詢對數據延遲是敏感的。確保你的模型優先滿足查詢頻次大的查詢和重要查詢。
為提升讀性能進行反范式化(De-normalize)和冗余
根據實際情況,如果不需要就不要反范式化。
在關系型數據庫的世界里,范式化的優點是顯而易見的:較少的數據冗余,較少的數據修改異常,概念更清晰,更容易維護等等;同樣,它的缺點也十分明顯:多表 join查詢會很慢等等。這兩方面也會體現在Cassandra中,但是缺點會更明顯,因為Cassandra數據是分布式存儲,當然它也并不支持 join操作。所以,對于一個完全范式化的schema,Cassandra讀操作性能可能比RDBMS更糟糕,所以我們通常通過反范式化來提升查詢性 能。(譯者注:Cassandra一次查詢可能會請求多個節點并將結果匯總到客戶端,而RDBMS查詢只需從本地查詢即可)。
這個實踐和上一個查詢建模實踐是非常重要的,我會在余下的文章中通過一個詳細的例子做進一步說明。
注意:下面我們要討論的例子只是個演示,它不代表eBayCassandra項目的數據模型。
實戰:User和Item中間的’Like’關系
這個示例是關于電子商務系統的一個功能,一個user可以喜歡多個item,同時一個item可以被多個user所喜愛,在關系型數據庫中這個關系是通過many-to-many實現的,如下圖所示:
通過上面的模型,我們可以進行如下查詢:
-
通過user id獲取user
-
通過item id獲取item
-
獲取指定user喜歡的所有item
-
查看指定item被那些user所喜愛
下面將介紹幾個通過Cassandra建模解決上面問題的現方案,反范式的順序從低到高。你會發現最佳方案依賴于查詢模式。
方案1:完全按照關系數據庫模型設計
這個模型支持通過user id查詢user和通過item id查詢item。但無法簡單查詢某個user喜愛的所有item或者某個item被那些user所喜愛。
對于這個用例來說,這是最糟糕的設計,主要是因為User_Item_Like沒有設計好。
注意:為了簡單起見,關系型數據模型中的timestamp字段沒有體現到Cassandra模型中(這個字段用于存儲user何時喜愛某個item),我會在后面介紹它。
方案2:使用自定義索引范式化實體
這個模型中User和Item是范式化實體,user id 和item id被映射存儲兩次,第一次是通過item id存儲user id(User_By_Item),第二次通過item id存儲user id(Item_By_User)。
這樣,我們很容易可以通過Item_By_User查詢某個user喜歡的全部item,還可以通過User_By_Item查詢某個item被哪些 user所喜愛。這里我們使用了,Item_By_User和User_By_Item這兩個column family作為自定義二級索引。(譯者注:Cassandra column family也有二級索引功能,它的作用是通過創建column key索引快速查詢到column value)。
有這樣一個場景,我們總是希望通過指定user查詢其喜愛的item,同時要獲取item title信息。在當前模型下,我們首先要通過Item_By_User獲取指定user關聯的item id,然后根據這些item id依次查詢Item模型獲取title信息,反之亦然。一個item有可能被幾百個user所喜愛,或者一個活躍user可能喜愛許多item,基于當 前的模型設計,將會導致很多額外的查詢。所以,最好通過反范式‘Item_by_User’ 中的itemtitle和’ User_by_Item’中的username信息來優化查詢,方案3將會向大家展示。
注意:即使你可以批量讀取(譯者注:在Cassandra Java客戶端hector中可以MultigetSliceQuery類實現一次查詢傳入多個rowkey),但它們將仍然很慢,因為 Cassandra底層仍然會單獨查詢每個rowkey,然后通過Coordinator 節點(譯者注:Coordinator 節點為Cassandra客戶端直接請求的節點,可以理解為它是一個代理)匯總到客戶端。批量讀取可以避免請求的往返耗時,它是個不錯的選擇,你可以去嘗 試它。
方案三:范式化實體,并將它們反范式化到自定義索引
在這個模型中,title和username被分別反范式到User_By_Item和Item_By_User。這樣將允許我們高效查詢指定user喜愛的所有item,以及喜愛指定item所有的user。這樣我們就為整個用例做了很大的反范式化工作。
問題又來了,如何獲取指定user喜愛item的具體信息(title,desc,price等等)?首先我們要問問自己我們是否真的需要這個查詢。還是 上面的例子,當用戶希望獲取item額外信息的時候,我們可以在頁面上展示所有的item title,當點擊item title時,在打開的新頁面顯示這個item的具體信息。所以,在這個用例中我們最好不要極端反范式化。(item title列表中通常還會顯示title和price信息,這也很容易實現,這個就留給大家做練習)
讓我們考慮下面兩個查詢:
-
通過所給item id,獲取具體item信息(title, desc等等),并一同查詢喜歡這個item的user name
-
通過所給的user id,獲取具體user信息,并一同查詢user喜歡的所有item titile
上面兩個查詢出現在查詢item和user的詳情頁面是很正常的,這些在當前模型中可以很好的實現。兩者都需要兩次查詢,一次查詢item(或者 user)信息,另一次查詢user name(或者item title)。User變得更加活躍的(喜歡上千個items)或者item變得很熱門(被幾百萬user喜愛),查詢的次數不會隨之增加,仍然為兩次。 這很好,當我們從方案2到方案3,反范式化并沒有讓我們變糟糕。讓我們看看方案4如何做更進一步的優化。
方案4:范式化部分實體
很明顯,方案4看起來有些凌亂。在數據存儲結構上,它與方案3也不同。
如果User和Item之間是高度關聯的實體(類似ebay),相比當前方案我將更傾向于方案3。
因為我們不打算反范式化所有item屬性到User實體或者反范式化所有user屬性到Item實體,所以這里我們使用了部分范式化。我不會打算進行極限 反范式化(讓所有time屬性到User實體和所有user屬性到Item實體),因為在這個用例中那樣做是沒有意義的。
注意:這里我使用Super Column只是為了給展示。大多情況,應該傾向于使用composite columns,而不是Super Column。
最佳模型
在本文的用例中方案3是優勝者。上面的方案中我們忽略了timestamp信息,下面我們將把它以timeuuid(type-1 uuid)形式添加到最終模型上。注意,在User_By_Item實體中timeuuid和userid合并為一個composite column key,在Item_By_user實體中timeuuid和item id合并為一個composite column key。
回想一下,column key是有序存儲的。這里我們的User_By_Item 和 Item_By_User兩個實體的column keys通過timeuid排序后被存儲到磁盤,這使得基于時間的范圍查詢非常高效。在這個模型中,我們不需要讀取一個row中所有column,就可以 高效的查詢某個item最近被哪些user所喜愛,以及某個用戶最近喜歡了哪些item。
最終模型如下:
總結
我們通過一些基本的實踐和詳細例子幫你開啟Cassandra數據建模之旅。下面是一些關鍵點:
-
當設計Cassandra列族時,不要把它想成是關系表,要把它想成是嵌套的、排序的map數據結構。
-
要圍繞著查詢來設計列族,從設計實體及其關系開始。
-
在需要的時候,通過反范式化和冗余來提升讀性能。
-
記住有多種方式創建模型,最佳的方式依賴于你的用例和查詢模式。
這里我沒有提到其它常用的用例,如日志記錄、監控、實時分析(rollups, counters),或者時間序列。但是,我們討論的實踐也適用于它們。此外,有些眾所周知的技術和模式用于時間序列的模型設計。在eBay,我們也使用 這些技術,也樂于在后續的文章中分享這些。關于時間序列數據建模,我推薦你閱讀 Advanced time series with Cassandra 和 Metric collection and storage,如果你是Cassandra新手,請先閱讀DataStax documentation。
1.2文檔
http://www.datastax.com/documentation/cassandra/1.2/pdf/cassandra12.pdf
原文鏈接:http://www.ebaytechblog.com/2012/07/16/cassandra-data-modeling-best-practices-part-1/
-