2006年的OSDI有兩篇google的論文,分別是BigTable和Chubby。Chubby是一個分佈式鎖服務,基於Paxos算法;BigTable是一個用於管理結構化數據的分佈式存儲系統,構建在GFS、Chubby、SSTable等google技術之上。相當多的google應用使用了BigTable,比如Google Earth和Google Analytics,因此它和GFS、MapReduce並稱爲谷歌技術"三寶"。
與GFS和MapReduce的論文相比,我覺得BigTable的論文難懂一些。一方面是因爲自己對數據庫不太瞭解,另一方面又是因爲對數據庫的理解侷限於關係型數據庫。嘗試用關係型數據模型去理解BigTable就容易"走火入魔"。在這裏推薦一篇文章(需要翻牆):Understanding HBase and BigTable,相信這篇文章對理解BigTable/HBase的數據模型有很大幫助。
1 什麼是BigTable
Bigtable是一個爲管理大規模結構化數據而設計的分佈式存儲系統,可以擴展到PB級數據和上千臺服務器。很多google的項目使用Bigtable存儲數據,這些應用對Bigtable提出了不同的挑戰,比如數據規模的要求、延遲的要求。Bigtable能滿足這些多變的要求,爲這些產品成功地提供了靈活、高性能的存儲解決方案。
Bigtable看起來像一個數據庫,採用了很多數據庫的實現策略。但是Bigtable並不支持完整的關係型數據模型;而是爲客戶端提供了一種簡單的數據模型,客戶端可以動態地控制數據的佈局和格式,並且利用底層數據存儲的局部性特徵。Bigtable將數據統統看成無意義的字節串,客戶端需要將結構化和非結構化數據串行化再存入Bigtable。
下文對BigTable的數據模型和基本工作原理進行介紹,而各種優化技術(如壓縮、Bloom Filter等)不在討論範圍。
2 BigTable的數據模型
Bigtable不是關係型數據庫,但是卻沿用了很多關係型數據庫的術語,像table(表)、row(行)、column(列)等。這容易讓讀者誤入歧途,將其與關係型數據庫的概念對應起來,從而難以理解論文。Understanding
HBase and BigTable是篇很優秀的文章,可以幫助讀者從關係型數據模型的思維定勢中走出來。
本質上說,Bigtable是一個鍵值(key-value)映射。按作者的說法,Bigtable是一個稀疏的,分佈式的,持久化的,多維的排序映射。
先來看看多維、排序、映射。Bigtable的鍵有三維,分別是行鍵(row key)、列鍵(column key)和時間戳(timestamp),行鍵和列鍵都是字節串,時間戳是64位整型;而值是一個字節串。可以用 (row:string, column:string, time:int64)→string 來表示一條鍵值對記錄。
行鍵可以是任意字節串,通常有10-100字節。行的讀寫都是原子性的。Bigtable按照行鍵的字典序存儲數據。Bigtable的表會根據行鍵自動劃分爲片(tablet),片是負載均衡的單元。最初表都只有一個片,但隨着表不斷增大,片會自動分裂,片的大小控制在100-200MB。行是表的第一級索引,我們可以把該行的列、時間和值看成一個整體,簡化爲一維鍵值映射,類似於:
table{
"1" : {sth.},//一行
"aaaaa" : {sth.},
"aaaab" : {sth.},
"xyz" : {sth.},
"zzzzz" : {sth.}
}
列是第二級索引,每行擁有的列是不受限制的,可以隨時增加減少。爲了方便管理,列被分爲多個列族(column family,是訪問控制的單元),一個列族裏的列一般存儲相同類型的數據。一行的列族很少變化,但是列族裏的列可以隨意添加刪除。列鍵按照family:qualifier格式命名的。這次我們將列拿出來,將時間和值看成一個整體,簡化爲二維鍵值映射,類似於:
table{
// ...
"aaaaa" : { //一行
"A:foo" : {sth.},//一列
"A:bar" : {sth.},//一列
"B:" : {sth.} //一列,列族名爲B,但是列名是空字串
},
"aaaab" : { //一行
"A:foo" : {sth.},
"B:" : {sth.}
},
// ...
}
或者可以將列族當作一層新的索引,類似於:
table{
// ...
"aaaaa" : { //一行
"A" : { //列族A
"foo" : {sth.}, //一列
"bar" : {sth.}
},
"B" : { //列族B
"" : {sth.}
}
},
"aaaab" : { //一行
"A" : {
"foo" : {sth.},
},
"B" : {
"" : "ocean"
}
},
// ...
}
時間戳是第三級索引。Bigtable允許保存數據的多個版本,版本區分的依據就是時間戳。時間戳可以由Bigtable賦值,代表數據進入Bigtable的準確時間,也可以由客戶端賦值。數據的不同版本按照時間戳降序存儲,因此先讀到的是最新版本的數據。我們加入時間戳後,就得到了Bigtable的完整數據模型,類似於:
table{
// ...
"aaaaa" : { //一行
"A:foo" : { //一列
15 : "y", //一個版本
4 : "m"
},
"A:bar" : { //一列
15 : "d",
},
"B:" : { //一列
6 : "w"
3 : "o"
1 : "w"
}
},
// ...
}
查詢時,如果只給出行列,那麼返回的是最新版本的數據;如果給出了行列時間戳,那麼返回的是時間小於或等於時間戳的數據。比如,我們查詢"aaaaa"/"A:foo",返回的值是"y";查詢"aaaaa"/"A:foo"/10,返回的結果就是"m";查詢"aaaaa"/"A:foo"/2,返回的結果是空。
圖1是Bigtable論文裏給出的例子,Webtable表存儲了大量的網頁和相關信息。在Webtable,每一行存儲一個網頁,其反轉的url作爲行鍵,比如maps.google.com/index.html的數據存儲在鍵爲com.google.maps/index.html的行裏,反轉的原因是爲了讓同一個域名下的子域名網頁能聚集在一起。圖1中的列族"anchor"保存了該網頁的引用站點(比如引用了CNN主頁的站點),qualifier是引用站點的名稱,而數據是鏈接文本;列族"contents"保存的是網頁的內容,這個列族只有一個空列"contents:"。圖1中"contents:"列下保存了網頁的三個版本,我們可以用("com.cnn.www", "contents:", t5)來找到CNN主頁在t5時刻的內容。
再來看看作者說的其它特徵:稀疏,分佈式,持久化。持久化的意思很簡單,Bigtable的數據最終會以文件的形式放到GFS去。Bigtable建立在GFS之上本身就意味着分佈式,當然分佈式的意義還不僅限於此。稀疏的意思是,一個表裏不同的行,列可能完完全全不一樣。
3 支撐技術
Bigtable依賴於google的幾項技術。用GFS來存儲日誌和數據文件;按SSTable文件格式存儲數據;用Chubby管理元數據。
GFS參見谷歌技術"三寶"之谷歌文件系統。BigTable的數據和日誌都是寫入GFS的。
SSTable的全稱是Sorted Strings Table,是一種不可修改的有序的鍵值映射,提供了查詢、遍歷等功能。每個SSTable由一系列的塊(block)組成,Bigtable將塊默認設爲64KB。在SSTable的尾部存儲着塊索引,在訪問SSTable時,整個索引會被讀入內存。BigTable論文沒有提到SSTable的具體結構,LevelDb日知錄之四: SSTable文件這篇文章對LevelDb的SSTable格式進行了介紹,因爲LevelDB的作者JeffreyDean正是BigTable的設計師,所以極具參考價值。每一個片(tablet)在GFS裏都是按照SSTable的格式存儲的,每個片可能對應多個SSTable。
Chubby是一種高可用的分佈式鎖服務,Chubby有五個活躍副本,同時只有一個主副本提供服務,副本之間用Paxos算法維持一致性,Chubby提供了一個命名空間(包括一些目錄和文件),每個目錄和文件就是一個鎖,Chubby的客戶端必須和Chubby保持會話,客戶端的會話若過期則會丟失所有的鎖。關於Chubby的詳細信息可以看google的另一篇論文:The Chubby lock service for loosely-coupled distributed systems。Chubby用於片定位,片服務器的狀態監控,訪問控制列表存儲等任務。
4 Bigtable集羣
Bigtable集羣包括三個主要部分:一個供客戶端使用的庫,一個主服務器(master server),許多片服務器(tablet server)。
正如數據模型小節所說,Bigtable會將表(table)進行分片,片(tablet)的大小維持在100-200MB範圍,一旦超出範圍就將分裂成更小的片,或者合併成更大的片。每個片服務器負責一定量的片,處理對其片的讀寫請求,以及片的分裂或合併。片服務器可以根據負載隨時添加和刪除。這裏片服務器並不真實存儲數據,而相當於一個連接Bigtable和GFS的代理,客戶端的一些數據操作都通過片服務器代理間接訪問GFS。
主服務器負責將片分配給片服務器,監控片服務器的添加和刪除,平衡片服務器的負載,處理表和列族的創建等。注意,主服務器不存儲任何片,不提供任何數據服務,也不提供片的定位信息。
客戶端需要讀寫數據時,直接與片服務器聯繫。因爲客戶端並不需要從主服務器獲取片的位置信息,所以大多數客戶端從來不需要訪問主服務器,主服務器的負載一般很輕。
5 片的定位
前面提到主服務器不提供片的位置信息,那麼客戶端是如何訪問片的呢?來看看論文給的示意圖,Bigtable使用一個類似B+樹的數據結構存儲片的位置信息。
首先是第一層,Chubby file。這一層是一個Chubby文件,它保存着root tablet的位置。這個Chubby文件屬於Chubby服務的一部分,一旦Chubby不可用,就意味着丟失了root tablet的位置,整個Bigtable也就不可用了。
第二層是root tablet。root tablet其實是元數據表(METADATA table)的第一個分片,它保存着元數據表其它片的位置。root tablet很特別,爲了保證樹的深度不變,root tablet從不分裂。
第三層是其它的元數據片,它們和root tablet一起組成完整的元數據表。每個元數據片都包含了許多用戶片的位置信息。
可以看出整個定位系統其實只是兩部分,一個Chubby文件,一個元數據表。注意元數據表雖然特殊,但也仍然服從前文的數據模型,每個分片也都是由專門的片服務器負責,這就是不需要主服務器提供位置信息的原因。客戶端會緩存片的位置信息,如果在緩存裏找不到一個片的位置信息,就需要查找這個三層結構了,包括訪問一次Chubby服務,訪問兩次片服務器。
6 片的存儲和訪問
當片服務器收到一個寫請求,片服務器首先檢查請求是否合法。如果合法,先將寫請求提交到日誌去,然後將數據寫入內存中的memtable。memtable相當於SSTable的緩存,當memtable成長到一定規模會被凍結,Bigtable隨之創建一個新的memtable,並且將凍結的memtable轉換爲SSTable格式寫入GFS,這個操作稱爲minor compaction。
當片服務器收到一個讀請求,同樣要檢查請求是否合法。如果合法,這個讀操作會查看所有SSTable文件和memtable的合併視圖,因爲SSTable和memtable本身都是已排序的,所以合併相當快。
每一次minor compaction都會產生一個新的SSTable文件,SSTable文件太多讀操作的效率就降低了,所以Bigtable定期執行merging compaction操作,將幾個SSTable和memtable合併爲一個新的SSTable。BigTable還有個更厲害的叫major compaction,它將所有SSTable合併爲一個新的SSTable。
遺憾的是,BigTable作者沒有介紹memtable和SSTable的詳細數據結構。
7 BigTable和GFS的關係
集羣包括主服務器和片服務器,主服務器負責將片分配給片服務器,而具體的數據服務則全權由片服務器負責。但是不要誤以爲片服務器真的存儲了數據(除了內存中memtable的數據),數據的真實位置只有GFS才知道,主服務器將片分配給片服務器的意思應該是,片服務器獲取了片的所有SSTable文件名,片服務器通過一些索引機制可以知道所需要的數據在哪個SSTable文件,然後從GFS中讀取SSTable文件的數據,這個SSTable文件可能分佈在好幾臺chunkserver上。
8 元數據表的結構
元數據表(METADATA table)是一張特殊的表,它被用於數據的定位以及一些元數據服務,不可謂不重要。但是Bigtable論文裏只給出了少量線索,而對錶的具體結構沒有說明。這裏我試圖根據論文的一些線索,猜測一下表的結構。首先列出論文中的線索:
- The METADATA table stores the location of a tablet under a row key that is an encoding of the tablet's table identifier and its end row.
- Each METADATA row stores approximately 1KB of data in memory(因爲訪問量比較大,元數據表是放在內存裏的,這個優化在論文的locality groups中提到).This feature(將locality group放到內存中的特性) is useful for small pieces of data that are accessed frequently:
we use it internally for the location column family in the METADATA table.
- We also store secondary information in the METADATA table, including a log of all events pertaining to each tablet(such as when a server begins
serving it).
第一條線索,元數據表的行鍵是由片所屬表名的id和片最後一行編碼而成,所以每個片在元數據表中佔據一條記錄(一行),而且行鍵既包含了其所屬表的信息也包含了其所擁有的行的範圍。譬如採取最簡單的編碼方式,元數據表的行鍵等於strcat(表名,片最後一行的行鍵)。
第二點線索,除了知道元數據表的地址部分是常駐內存以外,還可以發現元數據表有一個列族稱爲location,我們已經知道元數據表每一行代表一個片,那麼爲什麼需要一個列族來存儲地址呢?因爲每個片都可能由多個SSTable文件組成,列族可以用來存儲任意多個SSTable文件的位置。一個合理的假設就是每個SSTable文件的位置信息佔據一列,列名爲location:filename。當然不一定非得用列鍵存儲完整文件名,更大的可能性是把SSTable文件名存在值裏。獲取了文件名就可以向GFS索要數據了。
第三個線索告訴我們元數據表不止存儲位置信息,也就是說列族不止location,這些數據暫時不是咱們關心的。
通過以上信息,我畫了一個簡化的Bigtable結構圖:
結構圖以Webtable表爲例,表中存儲了網易、百度和豆瓣的幾個網頁。當我們想查找百度貼吧昨天的網頁內容,可以向Bigtable發出查詢Webtable表的(com.baidu.tieba, contents:, yesterday)。
假設客戶端沒有該緩存,那麼Bigtable訪問root tablet的片服務器,希望得到該網頁所屬的片的位置信息在哪個元數據片中。使用METADATA.Webtable.com.baidu.tieba爲行鍵在root tablet中查找,定位到最後一個比它大的是METADATA.Webtable.com.baidu.www,於是確定需要的就是元數據表的片A。訪問片A的片服務器,繼續查找Webtable.com.baidu.tieba,定位到Webtable.com.baidu.www是比它大的,確定需要的是Webtable表的片B。訪問片B的片服務器,獲得數據。
參考文獻
[1] Bigtable: A Distributed Storage System for Structured Data. In proceedings of OSDI'06.