天天看點

Uber是如何使用MySQL設計可擴充性資料存儲的?

在mezzanine項目中我們描述了我們是如何将uber的核心行程資料從單個的postgres節點遷移到schemaless,這是我們開發的一個容錯性很高、可用的資料存儲。

根據uber工程師的習慣使用mysql設計的資料存儲,使我們可以從2014 擴容到更高。本文分成三部分對schemaless進行闡述。

一、schemaless的總體設計

這一部分我們将講述schemaless的架構它在uber基礎結構中的角色以及他是如何成為該角色的。

1.我們對新資料庫的迫切需求

2014年初,由于出行業務的迅猛增長,資料庫空間即将耗盡。每次入住新的城市以及行程的裡程碑都會把我們推向危險的境地,直到我們發現到年末時uber的基礎架構将無法繼續發揮效用:postgre并不能存儲如此多的行程資料。我們的任務是實作uber的下一代資料庫技術,一個耗時數月甚至幾乎整年的任務,大量的來自于我們世界各地研究所的工程師參與進來。

但是首先,在商業與開源選擇如此之多的當下,為什麼要自己建構一個可擴充的資料庫。我們對我們新的行程資料存儲有5點關鍵的需求:

我們新的解決方案需要可以通過增加伺服器線性擴容,這是原有的postgre所缺乏的。添加伺服器應該能在增加硬碟存儲的同時減少系統的響應時間。

我們需要寫入的能力。我們之前通過redis實作了一套簡單的緩沖機制,是以如果postgre寫入失敗,我們可以稍後重試,因為行程已經在中間層存入了redis當中。但是當行程資料在redis當中時,是不能從postgre中讀取的,然後一些功能就挂了,比如計費。很煩,不過至少我們沒有丢失行程資料。随着時間流逝,uber逐漸成長,我們基于redis的解決方案不能擴容。schemaless需要支援一種類似redis的機制,但最好還是寫完即時可讀。

我們需要一種機制通知下遊依賴。在現有系統當中,我們同時處理多個行程元件(比如計費,分析等)。這種處理方式很容易出錯:如果任何一步失敗了,我們就比如從頭重試,即使一些元件處理已經成功了。這就不能擴容了,是以我們想把這些步驟打碎成獨立的步驟,由資料變更發起。我們曾經确實有一個異步事件系統,但是它是基于 kafka 0.7的。我們沒法讓它無損運作,是以我們希望新系統有一些類似的機制,但是可以無損運作。

我們需要副索引。由于我們是從postgre遷移的,那新的存儲系統需要支援postgre的索引,會按照習慣用副索引搜尋行程資料。

我們需要運維夠信賴的可靠系統,因為其中包含了行程資料的關鍵任務。如果淩晨3點我們接到叫車請求,但是這時資料存儲無法響應查詢,導緻業務當機,我們是否有相關操作知識可以快速解決這個問題。

鑒于以上種種,我們分析了幾種常用的選擇的優勢和潛在的限制,比如cassandra、riak、mongodb等。出于說明的目的,我們提供了如下圖表,展示了不同系統選擇下的不同功能組合:

Uber是如何使用MySQL設計可擴充性資料存儲的?

所有的三個系統都可以通過線上增加節點線性擴容,隻有一對系統可以在當機時收到寫操作。所有的解決方案中都沒有内置的方式将變化通知下遊依賴,是以可能需要在應用層實作該功能。它們都有索引功能,但是如果你想索引多個不同的值,查詢會變慢,因為他們使用分散查詢并聚合結果的方式查詢了所有節點。最後對于其中的一些系統有過單叢集的使用經驗,但不提供面向使用者的線上流量,而且在我們的服務連接配接的時候有各種各樣的運維問題。

最終我們的确定運維信賴為主要因素,因為它包含了行程資料的關鍵任務。可供選擇的解決方案理論上可能都是可靠的,但是我們是否有運維的知識來立即發揮其最大功效,基于此我們決定基于uber的使用情況開發自己的解決方案。這不僅基于我們使用的技術,而且根據成員的經驗。

需要注意的是我們對這些系統的研究持續了兩年,沒有發現适合行程資料存儲的,但是我們已經在其它領域成功接受了cassandra與riak作為我們的基礎服務,而且在生産環境使用這些為數百萬級的使用者提供服務。

2.在schemaless中我們相信

由于以上的所有選擇在規定的時間内都不能完全滿足自己的需求,我們決定建構我們自己的系統使運維盡量簡單,也參考了其它廠擴容的經驗。這個設計的靈感來自于friendfeed,運維的方面則參考了pinterest。

最後我們決定建構一個鍵值存儲,允許存儲任何json資料而不需要嚴格的格式驗證,一個非結構化的模式(命名的由來)。這是一個隻擴充分片的mysql, master節點都帶有寫緩沖在應對mysql當機,資料變更通知是一個訂閱-釋出的功能,我們稱之為trigger。最後,schemaless支援全局資料索引。

下面我們将讨論一下資料模型概覽以及一些關鍵特性,包括剖析uber的一份行程資料,更深入的例子保留在接下來的文章中。

3.schemaless的資料模型

schemaless是一個隻擴充的稀疏三維持久化哈希表,非常類似google的 bigtable。schemaless中的最小資料被稱作cell,不可更改;一次寫入後不可被覆寫或删除。cell是一個json blob通過一個rowkey和一個columnname引用,還有一個referencekey叫做ref key。rowkey是一個uuid,column name是一個字元串,reference key是一個整型。

你可以将row key看作是關系型資料庫的主鍵,column name看作是關系型資料庫的列。無論如何,在schemaless中沒有預定義或強制模式而且每行不需要共享column name;事實上,columnname完全由應用層定義。ref key用于給一個指定row key和列加版本。

是以如果一個cell需要更新,你隻需寫一個新的cell附帶一個更大的ref key (最新的cell是那個有最大的ref key的)。ref key也可以用作标記一個清單中的實體,但主要用作标記版本。具體哪種形式由應用本身決定。

應用通常把相關的資料組織進同一列,然後每列的所有cell在應用側的結構都大緻相同。這種分組方式很好的把一起修改的資料很好的組織到了一起,這樣應用程式就可以在資料庫不停機的情況下迅速修改結構。下面的例子進行了更詳細的叙述。

執行個體:schemaless中的行程資料存儲

在深入了解我們如何在schemaless中對行程資料模組化之前,讓我們先剖析一下一個uber的行程。行程資料在不同的時間點産生,從上車下車到付費,這許多資訊伴随着使用者在行程中的回報以及背景程序處理異步到達。下圖簡要說明了一個uber行程的不同階段是何時發生的:

Uber是如何使用MySQL設計可擴充性資料存儲的?

這個圖表展示了一個我們行程流的簡化版。*标志的部分是可選的且可能發生多次。

一個行程是由乘客發起,由司機結束,包含開始與結束的時間戳。這些資訊構成了行程的基礎,我們據此計算出該次行程的費用,由司機來收費。行程結束後,我們可能要調整跟收取或發放的費用。我們也可能給行程資料添加備注,從乘客或司機出發出回報(上圖中星号部分标出)。在第一張信用卡超期或禁用的情況下,我們不得不嘗試用多張信用卡付款。

uber行程流是一個資料驅動的過程。随着資料變得有效或添加,特定的一組處理會在該行程上執行。這些資訊中的一部分,比如乘客或司機的評級(上圖中note部分),可能在行程結束後幾天處理。

好了,那我們如何把上述的行程模型映射到schemaless?

4.行程資料模型

使用斜體字标注uuid,大寫字母表示column name,下表展示了我們行程資料存儲的簡化版的資料模型。我們有兩個行程(uuidstrip_uuid1 和 trip_uuid2) 以及四列(base, status, notes, and fare adjustment)。一個格子表示一個cell,帶有一個數字以及一個json的 (以{…}縮寫)。格子的覆寫代表不同版本 (也就是不同的ref keys)。

Uber是如何使用MySQL設計可擴充性資料存儲的?

trip_uuid1 有三個cell:一個在base列,兩個在status列,fare adjustment列沒有内容。trip_uuid2的base列有兩個格子,notes列有一個,同樣的fare adjustments列也沒有内容。在schemaless中,列沒什麼不同;每列的語義都由應用層定義,本例中是 mezzanine。

在mezzanine中,base列的cell包含了行程的基礎資訊,比如司機的uuid和行程的時間。status列包含行程現在的支付狀态,每次我們嘗試對行程支付的時候都會插入一個cell (由于信用卡額度不足或者逾期等問題嘗試可能失敗)。如果司機或者uber的dops(司機排程員)有行程相關的備注,會在notes列添加一個cell。最後的fare adjustment列的cell記錄了行程價格的調整。

我們如此劃分列是為了避免資料沖突 而且最小化更新時需要寫的資料量。base列在行程結束時寫入,基本隻會寫一次。當行程開始嘗試支付的時候開始嘗試寫status列,此時base已經寫好了,如果支付失敗可能會寫多次。相似的notes列在base列寫過後的一些節點可能會寫多次,但是與status列的寫操作完全獨立。類似的fare adjustments列隻在行程費用變更時會寫,例如路況不好等原因。

5.schemaless觸發器

schemaless的一個重要特性是觸發器,提供了在schemaless執行個體變更時可獲得通知的能力。由于cell是不可變的,以及新的版本是追加的,是以每個cell都代表了一個修改或者一個版本,這允許一個執行個體的值變更可以像日志一樣檢視變化。

對于一個給定執行個體,可以監聽這些變化以及觸發基于這些變化的函數,非常像類似kafka這種事件總線系統。schemaless的觸發器使schemaless成為一個完美的真實來源的資料存儲,因為除了随機通路資料,下遊的依賴可以運用觸發器功能來監聽并觸發任何應用側特定的代碼(與linkedin’s的 databus類似),進而實作資料建立與資料處理的解耦。

在其它用例中,uber在base列寫入mezzanine執行個體後,使用schemaless的觸發器來進行結賬操作。針對上面的例子,當trip_uuid1的base列被寫入後,我們的支付服務被base列的觸發器觸發,擷取這個cell,然後嘗試用信用卡支付該行程。

無論成功與否,信用卡支付的結果都會回寫如mezzanine的status列。通過這種方式實作了支付服務于行程建立的解耦,schemaless扮演了一個異步事件總線的角色。

Uber是如何使用MySQL設計可擴充性資料存儲的?

6.索引的易用性

最後,schemaless支援在json blob中的字段上定義索引。當這些預定義的用于找到cell的字段與查詢的參數相比對時,就會用到索引。索引查詢效率很高,因為索引查詢隻需要通路一個單一的分片來找到需要傳回的cell的集合。

事實上,查詢還可以更深度的優化,因為schemaless允許非标準化的cell資料直接加入索引中。索引中含有非标準化資料意味着索引查詢在查詢和取資訊操作一起隻需要查詢一個分片。

其實,我們通常推薦schemaless使用者在可能需要的地方都把非标準資料加到索引當中,除非隻需要直接用row key查詢并取回cell。在某種意義上,這樣一來我們用存儲交換來了快速查詢的性能。

作為mezzanine的一個例子,我們定義個了一個副索引來查詢指定司機的所有行程。我們将行程的建立時間以及行程發生的城市非标準話加入到索引中。這樣就可以查詢一個司機在一個城市中一段時間中的所有行程。

下面我們給出了driver_partner_index yaml 格式的定義,這是行程資料存儲的一部分,定義在base列上 (這個例子用标準#符号添加了注釋)。

Uber是如何使用MySQL設計可擴充性資料存儲的?

使用這個索引,通過篩選city_uuid或者trip_created_at,我們能夠找出指定driver_partner_uuid的行程。在這個例子中我們隻用到base列的中的字段,但是schemaless支援從多個列中非标準化資料,相當于上面column_def清單中的多個執行個體。

像上文提到的schemaless高效的索引得益于基于分片字段将索引分片。是以一個索引的唯一需求是索引中有一個字段是分片字段(例如上例中最先給出的driver_partner_uuid)。該分片字段決定了索引實體應該在哪個分片寫入或者讀取。原因是我們在查詢索引的時候需要提供分片字段。這意味着在查詢時,我們隻需要查詢一個分片來擷取索引實體。關于分片字段有一點需要注意的是要選一個分布好的字段。uuid最佳,其次是city ids,不要選狀态字段(枚舉值)。

除分片字段外,schemaless還支援相等、不等以及範圍查詢的過濾器,同時支援隻查詢索引字段的一個子集以及根據索引實體指向的row key擷取特定列或所有列。現在分片字段必須是不可修改的,是以schemaless隻需跟一個分片互動,但是我們正在探尋如何在沒有太大性能開銷的情況下讓他成為可變的。

索引具備最終一緻性,無論何時我們寫入一個cell,我也更新這個索引實體,但是這不發生在同一個事務中。cell與索引實體通常不在同一個分片上,是以如果我們想要提供一緻的索引,就需要在寫入操作中引入 2pc ,這會明顯加大開銷。

通過最終一緻性的索引,我們避免了這項開銷,但是schemaless的使用者可能會看到過期的資料。多數情況下cell變化與相關索引變化之間的延遲能控制在20ms之内。

7.小結

我們給出了一個資料模型、觸發器以及索引的概覽,這些都是schemaless的關鍵功能,我們行程存儲引擎的主要組成部分。在後續的文章中,我們将看到一些schemaless的其它特性來闡明在uber的基礎設施中,它是如何成為服務的好夥伴的:更多的架構,使用mysql作為一個存儲節點,以及我們如何使觸發器在用戶端成為可容錯的。

二、schemaless的架構

在 mezzanine項目: uber偉大的遷移,我們講述了我們是如何将uber的核心行程資料從一個單一的postgres執行個體遷移到schemaless的,這是我們開發的可擴容、高可用的資料存儲。

然後我們給出了一個schemaless的概覽—它的開發的決定過程,以及資料模型概覽—而且介紹來一些特征比如schemaless的觸發器和索引。本段介紹覆寫了schemaless的架構。

1.schemaless概要

回顧一下,schemaless是一個可擴充且能容錯的資料存儲。基礎的資料實體稱作cell,是不可變的,一次寫入之後不可被覆寫(在一些特殊的情況下,可以删除舊的記錄) 。一個格子由一個row key,columnname,以及ref key定位。一個cell通過寫入一個帶有更大的ref key和相同的row key和column name的新版本來更新。

schemaless對内部存儲的資料的結構沒有強制要求,并以此得名。從schemaless的觀點看,他隻存儲json 對象。schemaless獨特的支援基于cell的字段建構的高效的具備最終一緻性的副索引。

2.架構

schemaless的節點分為兩類:工作節點和存儲節點,既可以在同一個實體機/虛拟機上,也可以分開。工作節點接受到用戶端的請求,把請求分散到存儲節點,并把結果聚合。存儲節點存儲資料的方式使單個存儲節點上的單個或多個cell的查詢非常快。我們把兩種節點分開來讓兩部分各自獨立的擴容。下圖是schemaless架構概覽:

Uber是如何使用MySQL設計可擴充性資料存儲的?

3.工作節點

schemaless的用戶端通過http協定與工作節點互動。工作節點将請求路由到存儲節點,并根據需要将存儲節點傳回的結果聚合,并處理一些背景工作。為了解決運作慢或當機節點的問題,用戶端的庫檔案會顯示的嘗試其它主機,以及重試失敗的請求。寫操作對schemaless是幂等的,是以所有的請求都可以安全的重試 (這真是個不錯的特性)。這個特性被用戶端類庫充分的利用了。

4.儲存節點

我們将資料集合劃分入一個固定數量的分片(通常設定為4096),然後映射到存儲節點上。一個cell根據它的row key映到到一個分片。每個分片都會按配置的數量複制到多個存儲節點上。這些存儲節點共同組成一個存儲叢集,每個節點都有一個主節點與兩個從節點。從節點(也稱為副本)分布在多個資料中心為機房故障提供備援。

5.讀寫請求

當schemaless處理一個讀請求時,比如讀取一個cell或者請求一個索引,工作節點可以從叢集内的任何存儲節點讀取資料,具體從主節點還是從節點讀取資料是在一個的底層配置的,預設讀取主節點,這意味着用戶端可以看到它寫請求的結果。寫請求(插入cell的請求),隻能操作這個cell所屬叢集的主節點。當更新完主節點的資料,存儲節點會異步的将這個更新複制到叢集的從節點。

 錯誤處理 

分布式資料存儲一個有趣的方向是如何處理故障,比如請求傳回失敗(主節點或從節點)。schemaless設計的目的就是最小化存儲節點讀寫請求失敗造成的影響。

 讀請求 

主節點與從節點的設定意味着隻要叢集裡有一個節點可用就可以提供服務。如果主節點可用,schemaless總是可以通過查詢傳回最新的資料。如果主節點當機了,有些資料可能還沒有複制到從節點,是以schemaless傳回得可能不是最新的資料。

在生産環境中,複制的延時基本都是亞秒級的,是以從節點的資料基本都是最新的。工作節點對存儲節點連接配接的管理采用 斷路器模式,當一個存儲節點當機時,會自動尋找新的節點。通過這種方式,讀取任務在故障時轉移到了另一個節點。

 寫請求 

從節點當機并不影響寫操作,寫請求發往主節點,但是如果主節點當機,schemaless同樣接受寫請求,但是他們會在其它(随機選擇的)主節點上執行個體化。這與dynamo 或 cassandra的hinted handoff機制很像。寫往其它的主節點意味着随後的讀請求在主節點恢複或者從節點更新為主節點前讀不到這些寫入結果。事實上,schemaless在處理異步故障的時候都是通過寫其它主節點解決的,我們稱之為技術緩沖寫入(将會在下節中詳述)。

使用單一節點負責寫入會産生一些優點與缺點。一個優勢是對于每個分片的寫入操作構成一個 全序,這對schemaless的觸發器來說非常重要,我們的異步處理處理架構(這在schemaless系列文章中的第一篇有提及),因為這樣它可以從任何一個節點讀取該分片的資料,并能保證處理的順序。叢集中所有節點的cell的寫順序都是一緻的,是以在一些情景下schemaless的分片可以看作一份分區cell的修改日志。

單主節點最突出的缺點是:如果叢集中主節點當機了,我們将資料緩沖寫入别的地方,但是不可讀。這種麻煩情況的優勢在于:schemaless可以告知用戶端,master節點當機了,是以用戶端知道剛剛寫入的cell不是馬上可以讀取的。

6.緩沖寫入

由于schemaless使用mysql異步複制,如果一個主節點收到一個寫請求,并将寫請求執行個體化,但是在複制到其它從節點的時候當機了(比如硬體故障)。未解決這個問題,我們使用一項稱作緩沖寫入的技術。緩沖寫入通過将資料寫入多個叢集來最小化資料丢失的機率。如果一個主節點當機了,資料對接下來的讀請求是不可用,但是還是先被執行個體化下來。

通過緩沖寫入,當一個工作節點收到一個寫入請求,他将請求寫入兩個叢集:一個副叢集和一個主叢集(按這個順序)。僅當兩個寫入都成功了時才會告知用戶端寫入成功。請看下表:

Uber是如何使用MySQL設計可擴充性資料存儲的?

主叢集的主節點是接下來的讀請求期望讀取資料的地方。如果主機群主節點在異步mysql複制将cell複制到主叢集從節點之前當機了了,副叢集主節點暫時充當資料備份。

副叢集主節點是随機選擇的,寫操作寫入一個特殊的緩沖表。一個背景的程序監控主叢集的從節點來檢視何時cell出現,僅當那是從緩沖表删除該cell。存在副叢集意味着資料至少寫入了兩台主機。附帶一下,副叢集主節點的數量是配置的。

緩沖寫入利用幂等性,如果一個帶有指定row key,column name和ref key的cell已經存在,這個寫入會被拒絕。幂等性意味着如果緩沖的cell有不同的row key,columnname和ref key,當主叢集主節點恢複時會寫入主叢集。從另一方面說,如果多個帶有相同的row key,columnname以及ref key的寫操作被緩沖,他們中的隻有一個可以成功,當主叢集恢複的時候,其它的會被拒絕。

7.使用mysql作為存儲後端

schemaless的強大(與簡單)來源于存儲節點中使用了mysql。schemaless本身隻是在mysql之上加了一層薄的封裝用于将請求路由到正确的資料庫。通過使用mysql innodb内置的索引和緩存,我們獲得了cell及副索引查詢的高性能。

每個schemaless分片是一個獨立的mysql資料庫,每個mysql資料庫伺服器包括一系列mysql資料庫。每個資料庫包含一個盛放cell的mysql表(稱作實體表)以及每個副索引各有一張表,另外還有一組輔助表。每個schemaless的cell是實體表中的一行,并有如下mysql表定義:

Uber是如何使用MySQL設計可擴充性資料存儲的?

added_id 列是一個自增的整數列,而且是實體表的mysql主鍵。使用added_id作為主鍵使mysql在磁盤上線性寫入cell。此外added_id為每個cell提供了一個唯一的指針,是以schemaless的觸發器可以有效地使用它按插入順序提取資料。

而 row_key, column_name, 和ref_key 三列即是每個schemalesscell的row key、columnname和ref key。為了通過這三列高效地查找cell,我們在這三列上定義了一個mysql聯合索引。是以我們可以高效根據給定row key和column name找到所有cell。

body列使用壓縮過的mysqlblob格式存儲了cell的json對象。

我們試驗了各種編碼和壓縮算法,最後出于速度和體積的考慮決定使用messagepack 和zlib (詳細内容将會在後面的文章中讨論)。最後,created_at列用于存儲cell插入的時間戳,因為schemaless的觸發器會查詢一個給定時間節點之後的cell。

基于這些配置,我們使用用戶端控制結構,而無需修改mysql的中的表結構;而且可以高效的尋找cell。此外added_id列使插入線性寫入到磁盤上,據此我們可以像分區日志一樣高效的操作資料。

8.小結

schemaless如今是uber底層一大批服務的生産中的資料存儲。我們很多服務高度依賴schemaless高可用及可擴容的特性。

三、schemaless triggers的細節及案例

schemaless triggers是一種具有可伸縮性、容錯性和無損性的技術,它可以監聽schemaless執行個體的變化。它是隐藏在行程背後的流程管理引擎,從司機的領航員按下“結束行程”并支付了行程費用開始,所有相關的資料都進去我們的資料倉庫等待我們分析。在schemaless系列的最後一篇中,我們将深入探讨schemaless triggers的相關功能,以及我們如何讓這個系統具備可伸縮性和容錯性。

總的來說,在schemaless中最基本的單中繼資料被稱為單元(cell),它是不可變的,一旦寫入就不能再重寫(當然在特殊情況下,我們可能會删除舊記錄)。一個單元可能被行鍵(row key)、列名稱(column name)及引用鍵(ref key)所引用,單元的内容通過寫入一個更高版本的引用鍵來進行更新,其中行鍵和列名稱保持不變。schemaless不對存儲在其内部的資料執行任何的類資料庫的schema操作,是以這也正是為什麼叫schemaless的原因,從schemaless的觀點來看,它僅僅存儲json對象。

1.schemaless triggers案例

讓我們看一下實際中schemaless trigger是如何工作的,下面的代碼展示了我們是如何異步計費的一個簡化版本,大寫表示schemaless 列名稱。案例中使用的是python語言:

我們通過添加一個注解符@trigger定義一個trigger,這個注解可以加在一個函數上,也可以添加在schemaless 指定的列上。這樣的做法讓schemaless triggers去調用指定的函數,在本例中是指bill_rider,當一個單元被寫入一個指定的列時将被觸發調用。

這裡的base是一個列,當一個新的單元寫入到base時表明行程已經結束。然後就會觸發trigger,這時行鍵(這裡是行程的uuid)就會被傳遞到函數中,如果需要更多的資料,程式員必須從schemaless 執行個體中擷取其他的實時資料,本例是從行程存儲系統mezzanine中擷取資料。

下面的圖檔展示了bill_rider的trigger相關的資訊流(乘客結賬部分),箭頭指向表明了調用方和被調用方,旁邊的數字表示流程的順序:

Uber是如何使用MySQL設計可擴充性資料存儲的?

首先行程進狀态寫入到mezzanine,這會使 schemaless trigger架構調用bill_rider,在調用時,函數要求行程存儲擷取status列的最新版本資訊,在本例中is_completed字段不存在,也就是意味着乘客還未結賬,接着再base列的行程資訊将被擷取并通過函數調用信用卡provider進行結賬。在本例中,我們成功地使用信用卡進行付款,是以我們将成功狀态寫回到mezzanine中,然後将status列中的is_completed字段設定為true。

trigger架構能保證bill_rider被每個schemaless 執行個體的每個單元至少調用一次。一般而言trigger函數隻會被觸發一次,但在某些出錯的情況下可能會被調用若幹次,這個錯誤可能是trigger函數本身的錯誤也可能是trigger函數外部的錯誤。這也就意味着trigger函數需要被設計成具備幂等性,在本例中,幂等性可以通過檢查單元是否已經被處理完畢來實作,函數檢測到如果已經處理完畢了則可以直接傳回。

當你在檢視schemaless 如何支援類似本案例的流程時,請記住這個案例。我們将會解釋schemaless 如何被當做變更日志來使用的,并且讨論schemaless 相關的一些api,最後還會分享我們是通過什麼技術讓流程變得可伸縮和具備容錯能力。

2.把schemaless 當做日志

schemaless 包含所有單元,這也就意味着它包含了指定的行鍵、列鍵對的所有版本。也真是因為它擁有單元所有的曆史版本,schemaless 除了可以作為随機通路的key-value存儲外,它還可以作為變更日志。事實上,它是一個分區日志,每個切片都是自己的日志,如下面圖所示:

Uber是如何使用MySQL設計可擴充性資料存儲的?

每個單元都通過指定的行鍵(這裡是指uuid)進行切片映射後寫入特定的切片,在每個切片中,所有單元都會被賦予一個唯一的辨別符,這個辨別符叫已添加id。已添加id是一個自動遞增的字段,它代表單元插入的順序,越是新的單元就會有一個越新的已添加id。除了剛剛提到的已添加id外,每個單元還會有單元寫入時間字段。在所有分片副本中已添加id具備唯一性,這個特性對于提供failover能力是非常重要的。

schemaless 的api既支援随機通路也支援日志類型通路,随機通路api是相對于單元而言的,它由row_key、column_key和ref_key三者共同辨別。

Uber是如何使用MySQL設計可擴充性資料存儲的?

schemaless 還包含這些api終端的批處理版本,這裡我們省略它。早前說過的trigger函數bill_rider就是使用這些函數去擷取和操作一個單元的。

對于日志類型通路api,我們主要關心單元的切片數字、時間戳和已添加id,這三者合起來稱定位的位置。

Uber是如何使用MySQL設計可擴充性資料存儲的?

與随機通路api類似,日志通路api擁有更多方法讓我們一次性從不同的切片中去批量擷取多個單元。其中的location可以使時間戳timestamp或已添加id added_id。調用get_cells_for_shard除了傳回指定的單元外還會傳回下一個已添加id。例如,如果你通過指定location為1000去調用get_cells_for_shards請求了10個單元,那麼傳回回來的下一個location的位置偏移量就是1010。

3.追蹤日志

通過日志類型通路api你可以追蹤schemaless 執行個體,這個看起來就像在你的作業系統中通過tail -f指令去追蹤一個檔案,或者像kafka這樣最新的變更會被輪詢的事件通知隊列。用戶端然後通過維護保持跟蹤位置偏移量進而使用它們去輪詢。想要開啟一個跟蹤你要指定起始入口,例如location為0,或者任意的時間戳,或者某位置偏移量。

schemaless triggers通過使用日志類型通路api實作了相同機制的跟蹤,它保持跟蹤位置偏移量,通過輪詢該api方式的最直接的好處就是schemaless triggers使處理過程具有容錯性和可擴充性。通過配置schemaless執行個體及配置哪些列去輪詢資料,然後就可以通過用戶端程式去連接配接schemaless triggers架構。所有的函數和回調函數都被綁定到架構的數字流上,在适當的時刻将被schemaless triggers調用,或者說被觸發,而這個适當的時刻就是當一個新的單元被插入到schemaless執行個體時。

作為回報,架構會将增加運作在主機上的程式需要的工作程序編号。架構優雅地通過可用程序和不可用程序進行分工,将失敗的程序上面的工作傳播到健康程序去處理。這種分工模式意味着程式員隻需編寫好處理者即可,例如trigger函數,隻要保證這個函數是幂等性的就可以了,剩下的就交給schemaless triggers來處理。

4.架構

在這部分中,我們将讨論schemaless triggers是如何做到可擴充,如何做到讓故障影響最小化。下面的圖從一個高層次的角度展示了其架構,拿了前面結賬服務的例子:

Uber是如何使用MySQL設計可擴充性資料存儲的?

結算服務使用了運作在三個不同主機上的schemaless triggers,為簡單起見,我們假設每個主機上有一個工作程序,schemaless triggers架構将切片從多個工作程序中分開,是以每個工作程序負責一個特定的切片。注意到,工作程序1從切片1拉取資料,而工作程序2則則負責切片2和切片5,最後工作程序3負責切片3和切片4。

一個工作程序隻處理指定切片的單元,通過擷取新的單元去調用這些切片上注冊上來的回調函數。其中一個工作程序被指定為leader,它負責配置設定切片給各個工作程序。如果有一個工作程序挂了,leader将失敗程序的切片重新配置設定給其他工作程序。

在一個切片中,單元都是按照寫入的順序被進行觸發的,這也就意味着如果某個觸發單元總是因為程式錯誤而總是失敗,那它就會在相應的切片上阻礙單元處理。為了避免這種延遲,你可以配置schemaless triggers标記多次失敗的單元,并将他們放到單獨的隊列中。這樣做以後schemaless triggers就會跳過出錯的單元接着處理下一個單元。如果被标記的單元超過了某一閥值,trigger就會停止,這通常表明是系統錯誤,需要人工進行修複。

schemaless triggers通過儲存每個切片最新成功被觸發的單元的已添加id去跟蹤整個觸發過程,架構将這些位置偏移儲存在一個共享存儲中,例如zookeeper或者schemaless 執行個體本身。這也就意味着如果程式被重新開機了,trigger将會從公共存儲中讀取擷取位置偏移量後繼續運作,公共存儲同樣用于儲存一些meta資訊,例如協調leader選舉的工作,工作程序的發現及移除。

5.可擴充性和容錯性

schemaless triggers在剛開始設計時就充分考慮其可擴充性,對于任意用戶端程式,我們可以在被追蹤的schemaless 執行個體中添加最多與切片數量相等的工作程序,通常這個數量為4096。除此之外,我們可以線上添加或移除工作程序來處理schemaless 執行個體中其他trigger用戶端變化的負載。僅僅通過跟蹤架構裡面的進度,我們就可以給發送資料的schemaless 執行個體添加盡可能多的用戶端。在伺服器端沒有跟蹤用戶端并推送狀态給他們的邏輯。

schemaless triggers同樣也具備容錯性的,任何一個程序發生故障都不會影響到系統的運作。

如果一個用戶端程序發生錯誤了,leader會将失敗程序相關的工作重新配置設定給其他監控程序,確定所有切片都會配置設定到處理程序。

如果schemaless triggers節點上的leader發生故障了,一個新的節點将會被選舉作為leader,在leader選舉的過程中,單元仍然會被執行,但是工作不能被重新配置設定,而且不能添加或移除工作程序。

如果公共存儲(例如zookeeper)發生故障了,單元仍然會被處理,但是這種情況也像leader選舉期間,不能重新配置設定工作,工作程序也不能添加或移除。

最後,schemaless triggers架構與schemaless 執行個體裡面的故障是互相隔離的,任意資料庫節點宕了都沒問題,因為schemaless triggers可以從他們的備份節點上讀取。

總結

從運維的角度來看,schemaless triggers是一個非常好的夥伴。schemaless 是一個實時資料源理想的存儲,因為這裡的資料可以通過随機通路api或者通過日志類型通路api去通路。另外使用schemaless triggers的日志類型通路api可以将資料從生産者和消費者中解耦出來,讓開發者隻要關注邏輯處理而不必關心如何保證其擴充性和容錯性。

最後,我們可以在運作時添加更多的存儲伺服器去提升我們的性能和記憶體。如今,schemaless triggers架構是整個行程處理流中的核心驅動,包括将資料收進我們的分析資料倉庫和跨資料中心的複制。我們對2016及以後未來的前景充滿期待。

 平台授權轉載  來源:傑微刊 譯者:缪晨

<b></b>

<b>本文來自雲栖社群合作夥伴"dbaplus",原文釋出時間:2016-04-29</b>