Schemaless架構(二):Uber的Trip資料庫

arron劉發表於2016-02-20

Uber的Schemaless資料庫是從2014年10月開始啟用的,這是一個基於MySQL的資料庫,本文就來探究一下它的架構。本文是系列文章的第二部分;第一部分是關於Schemaless的設計。

在《Mezzanine專案——Uber的超級大遷移》一文中,我們描述瞭如何將Uber的核心trip資料從一個單獨的Postgres例項遷移到Schemaless這個可擴充套件與高可用的資料庫中。然後對Schemaless進行了簡單介紹,包括其發展決策過程、整體資料模型,並介紹了Schemaless的trigger與索引等功能。 本文將概述Schemaless的架構。

Schemaless簡介

回顧一下,Schemaless是一個可擴充套件的容錯資料庫,其資料的基本單位被稱為單元(cell),它是不可變的,一旦寫入,便無法被覆蓋(在特殊情況下,我們可以刪除舊記錄);單元可以被行鍵(row key)、列名(column name)和引用鍵(ref key)引用;單元內容透過編寫引用鍵更高的新版來執行更新,但行鍵和列名保持不變。Schemaless不對其中儲存的資料執行任何操作(故而命名schemaless)。從Schemaless的觀點來看,它只負責儲存JSON物件。Schemaless有著獨特的模式,它支援最終在單元欄位保持一致的高效二級索引。

架構

Schemaless有兩種節點:工作節點和儲存節點,可以放在同一個物理/虛擬主機上,也可以放在分離的主機上。工作節點接收客戶端請求,將其分發到儲存節點中,再將結果聚合起來。儲存節點存放資料的方式使得在同一個儲存節點上進行單個或多個檢索速度很快。我們將這兩種節點型別分開,分別進行擴充套件。Schemaless的基本結構如下:

工作節點

Schemaless的客戶端與工作節點透過HTTP端點通訊。它們向儲存節點發出路由請求,並將從儲存節點獲得的結果進行聚合(在需要時),同時處理後臺任務。對於進展緩慢或出現故障的工作節點,客戶端資料庫將嘗試連線到其他主機並重試請求。對Schemaless的寫入請求是冪等的,因此每次請求重試都是安全的(這個效能真的很棒)。客戶端資料庫利用了這個功能。

儲存節點

我們將資料集劃分成固定數量的分片(一般配置為4096),然後將其對映到儲存節點上。根據單元的行鍵,將單元與分片一一對應。複製每個分片到儲存節點的可配置數量。總體來講,這些儲存節點構成儲存叢集,每個儲存叢集包含一個主(master)、兩個輔(minion)。Minion(也稱為副本)會分佈到多個資料中心中,提供資料冗餘,以防災難性的資料中心當機。

讀取和寫入請求

一旦Schemaless用作讀取,比如讀取單元或查詢索引時,工作節點能夠從叢集的任意儲存節點中讀取資料。每次請求是從master還是minion的儲存節點中讀取是可配置的;預設是讀取master儲存節點的資料,也就是說確保客戶端能夠看到寫入請求的結果。寫入請求(請求插入單元)必須要在單元叢集的master上執行。一旦master資料更新,儲存節點將更新非同步複製到叢集的minion上。

故障處理

分散式資料儲存系統有趣的一點在於它們處理故障的方式,比如在儲存節點未能響應請求時(無論master還是minion)。Schemaless在設計時,旨在將儲存節點無法響應讀取與寫入請求的失敗影響降到最低。

讀取請求

Master和minion的設定意味著:只要叢集中有一個節點可用,就能滿足讀取請求。如果master可用,Schemaless就總能在檢索時返回最新的資料。如果master不可用,一些資料可能還未傳到minion,因此Schemaless可能返回過期的資料。然而在生產環境中,複製的延遲通常是次秒級的,因此minion的資料往往是最新的。工作節點在與儲存節點的連線中使用斷路器模式,以檢測儲存節點是否出現問題。用這種辦法,在出現故障時將讀取任務轉移到另一節點上。

寫入請求

一個minion當機不會影響寫入;相應操作可以轉到master上去。不過如果master當機,Schemaless仍會接收寫入請求,但會將這些請求存入另一個master(隨機選擇)的磁碟。這與Dynamo或Cassandra系統中的暗示移交(hinted handoff)十分類似。向另一個master寫入意味著在master恢復或者minion升級為master前,隨後的讀取請求都無法讀取這些新的寫入請求。事實上,在非同步複製中Schemaless總是透過將寫入轉到另一個master的方式來處理故障;我們將這種技術稱為快取寫入(buffered writes,下面會詳細描述)。

使用單獨的節點來接收寫入請求,優勢和劣勢都很多。一個優勢在於:寫入每個分片的請求可以整體進行排序。對於Schemaless trigger來說,這是很重要的效能,我們的非同步處理框架(系列文的第一部分中提到過)可以從任意節點為分片讀取資料,同時確保同樣的處理順序。在所有叢集的所有節點上負責寫入請求的單元都是一樣的。因此在某種意義上,Schemaless的分片可以看作是分割槽單元的修改日誌。

單master最突出的缺點在於,如果一個叢集的master當機,我們向別的master快取寫入命令,但這些新的寫入內容是無法讀取的。這種麻煩情況的優點在於:Schemaless可以在master當機時通知客戶端,因此客戶端會知道新寫入的單元不再是立即可讀的了。

快取寫入

由於Schemaless使用MySQL非同步複製,在master收到並留存寫入請求,然後還沒來得及將其複製到minion前,便出現了故障(比如硬碟驅動器故障),這個寫入請求就會丟失。為了解決這個問題,我們使用了一種技術,名叫快取寫入。透過寫入多個叢集,將資料丟失的風險減到最低。如果一個master當機,後續的讀取任務無法迅速執行,但請求存續卻不受影響。

透過快取寫入,當工作節點收到寫入請求時,會將請求寫入兩個叢集:次級叢集和主叢集(按次順序)。只有兩者都執行成功的情況下,系統才會通知客戶端寫入成功。見下圖:

在後續讀取中,資料應當在主叢集的master中。如果在非同步MySQL複製將單元複製到主叢集的minion前,主叢集的master就當機了,那麼就將次級叢集的master用作臨時資料備份。

次級叢集的master是隨機選擇的,轉移的寫入命令將進入特殊的快取表格。後臺job會監控主叢集的minion,檢視單元的出現時間;然後才會將相應單元從快取表格中刪除。設定次級叢集代表著需要將所有資料至少要寫入兩個主機。此外,次級叢集的數量也是可配置的。

快取寫入用到了冪等性;如果一個行鍵、列名和引用鍵相同的單元已經存在,寫入就會被拒絕。冪等性意味著只要單元的行鍵、列名和引用鍵不同,就會在主叢集的master恢復運作時寫入原master。另一方面,如果快取了多個行鍵、列名和引用鍵相同的寫入請求,那麼只有一個能夠成功;在主叢集恢復時,剩下的請求都會被拒絕。

將MySQL用作儲存後端

Schemaless的強大(與簡單)大多是因為我們在儲存節點中使用了MySQL。Schemaless本身是一個在MySQL之上相對較薄的層面,負責將路由請求傳送給正確的資料庫。透過使用MySQL索引,並將build快取到InnoDB中,單元和二級索引的查詢速度很快。

每個Schemaless分片都是獨立的MySQL資料庫,而每個MySQL資料庫伺服器包含一系列MySQL資料庫。每個資料庫包含一個單元的MySQL表格(叫做單元表),而每個二級索引也有一個MySQL表格,另有一組輔助表格。每個Schemaless的單元就是單元表中的一行,定義如下:

added_id列是一個自動遞增的整數列,也是單元表的MySQL主鍵。將added_id作為主鍵,可以讓MySQL在磁碟上線性寫入單元。此外,將added_id作為每個單元的獨特指標,Schemaless trigger可以按照插入的時間順序來有效地提取單元。

而row_key、column_name和ref_key分別代表Schemaless單元的行鍵、列名和引用鍵。為了透過這三欄進行有效地查詢,我們為這三列定義了一個複合MySQL索引。這樣一來,我們就能根據指定的行鍵和列名有效地找出所有單元了。

內容列中包含每個單元的JSON物件,以壓縮的MySQL blob(二進位制大物件)表示。我們嘗試了各種編碼和壓縮演算法,最終由於壓縮速度和大小選用了MessagePack和ZLib(在後面的文章中,我們會詳細進行描述)。最後,created_at列是單元插入的時間戳,可供Schemaless trigger用來查詢指定日期的單元。

透過這種設定,客戶端可以控制模式,而無需修改MySQL的佈局;查詢單元更有效率。此外,added_id列使得寫入命令以線性執行,因此我們能夠將資料視作分割槽日誌來訪問,達到高效。

總結

如今的Schemaless是Uber基礎架構大量服務的生產資料庫。我們的很多服務都極其依賴這個高可用性和可擴充套件的Schemaless。



原文連結: 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26916835/viewspace-1991651/,如需轉載,請註明出處,否則將追究法律責任。

相關文章