谷歌技術"三寶"之BigTable

2006年的OSDI有兩篇google的論文,分別是BigTable和Chubby。Chubby是一個分佈式鎖服務,基於Paxos算法;BigTable是一個用於管理結構化數據的分佈式存儲系統,構建在GFS、Chubby、SSTable等google技術之上。相當多的google應用使用了BigTable,比如Google Earth和Google Analytics,因此它和GFSMapReduce並稱爲谷歌技術"三寶"。

與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 片的存儲和訪問

片的數據最終還是寫到GFS裏的,片在GFS裏的物理形態就是若干個SSTable文件。圖5展示了讀寫操作基本情況。

當片服務器收到一個寫請求,片服務器首先檢查請求是否合法。如果合法,先將寫請求提交到日誌去,然後將數據寫入內存中的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論文裏只給出了少量線索,而對錶的具體結構沒有說明。這裏我試圖根據論文的一些線索,猜測一下表的結構。首先列出論文中的線索:

  1. 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.
  2. 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.
  3. 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的片服務器,獲得數據。

這裏需要注意的是,每個片實際都由若干SSTable文件和memtable組成,而且這些SSTable和memtable都是已排序的。這就導致查找片B時,可能需要將所有SSTable和memtable都查找一遍;另外客戶端應該不會直接從元數據表獲得SSTable的文件名,而只是獲得片屬於片服務器的信息,通過片服務器爲代理訪問SSTable。

參考文獻

[1] Bigtable: A Distributed Storage System for Structured Data. In proceedings of OSDI'06.

[2] Understanding HBase and BigTable.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章