構建高可用的寫服務

1、如何使用分庫分表支持海量數據的寫入?

1.1、是否真的要分庫?(分表也是不錯的選擇)

分庫當然能夠解決存儲的問題,假設原先單庫只能最多存儲2千萬的舒亮亮,採用分庫之後,存儲架構變成下圖所示的分庫架構,每個分庫都可以存儲2千萬數據量,容量的上限一下就提升了。

容量提升了,但也帶來了很多其他問題:

  • 分庫數據間的數據無法再通過數據庫直接查詢了,比如跨多個分庫的數據需要多次查詢或者藉助其他存儲進行聚合在查詢。
  • 分庫越多,出現的問題可能性就越大,維護成本越好。
  • 無法保障跨庫間的事務,只能藉助其他中間件實現最終一致性。

所以在解決容量問題上,可以根據業務場景選擇,不要一上來就要考慮分庫,分表也是一種選擇。

分表是指所有的數據均存在同一個數據庫實例中,只是將原先的一張大表按一定規則,劃分成多張行數較少的表,它與分庫的區別是,分表後的子表仍在原有庫中,而分庫則是子表移動到新的數據庫實例裏並在物理上單獨部署。

那麼何時使用分庫,何時使用分表呢?

假設訂單只是單量多而每一單的數據量較小,就適合採用分表,單條數據量小但行數多,會導致寫入(因爲要構建索引)和查詢非常慢,但整體對容量的佔用是可控的。採用分表後,大表變小表,寫入時構建索引的性能消耗會變小,其次小表的查詢性能也更好。如果採用分庫,雖然解決了寫入和查詢的問題,但是每張表所佔有的磁盤空間很少,也會產生資源浪費。

同時,分表除了能解決容量問題,還能在一定程度上解決分庫所帶來的的三個問題。

  • 分表後可以通過join等完成一些富查詢,相比分庫簡單。
  • 分表的數據仍存儲在一個數據庫裏,不會出現很多分庫,無須引入一些分庫中間件,因此維護成本和開發成本均較低。
  • 因爲在同一個數據庫裏,也可以很好解決事務問題。

1.2、如何實現分庫

在決定對數據庫進行分庫後,首先要解決的問題是如何選擇分庫的維度。不同分庫維度決定了部分查詢是否能直接使用數據庫,以及是否存在數據傾斜問題。

1.2.1直接滿足最重要的業務場景

在業務上,所有的訂單數據都隸屬於某一個用戶的,在選擇分庫維度時,可以按訂單歸屬的用戶這個字段進行分庫,按此維度分庫後,同一個用戶的訂單都在某一個分庫裏。

訂單模塊處理除了提供提交訂單接口外,還會提供給售賣商家對自己店鋪訂單進行查詢及修改等功能,這些維度的查詢和修改需求,在採用了按購買用戶進行分庫之後,均無法直接滿足了。

訂單模塊最重要的功能是什麼?

答案是保證客戶的各項訂單功能任何時候都能夠正常使用,比如下單、下單立刻查看已購的訂單信息、待支付、待發貨、待配送的訂單列表等。相對來說,訂單裏商品售賣方(即賣家)所使用的功能並不是優先級最高的,因爲當我們要對賣家和買家的功能做取捨時,賣家是願意降低優先級的。

按購買用戶劃分後,用戶的使用場景都可以直接通過分庫支持,而不需要通過異構數據(存在數據延遲)等手段解決,對用戶來說體驗較好,其次,同一個分庫中,便於修改同一個用戶的多條數據,因此也不存在分佈式事務的問題。

1.2.2最細粒度隨機劃分

上述劃分方法雖然直接滿足了最重要的場景,但可能會出現數據傾斜的問題,比如出現一個超級客戶(如企業客戶),購買的訂單量非常大,導致某一個分庫數據量居多,就會重現分庫前的場景,這屬於最極端的情況之一。

對於傾斜的問題,可以採用最細粒度的拆分,即按數據的唯一標識進行劃分,對於訂單來說唯一標識就是訂單號。採用訂單號進行分庫後,用戶的訂單會按Hash隨機均勻地分散到某一個分庫裏,這樣就解決了某一個分庫數據不均勻的問題。

採用最細粒度分庫後,雖然解決了數據均衡的問題,但又帶來了其他問題:

  • 除了細粒度查詢外,其他任何維度的查詢均不支持,這就需要通過異構等方式解決,但異構有延時,對業務有損。
  • 其次採用最細粒度後,對於防重邏輯在數據庫層面已經無法支持。比如用戶對同一個訂單在業務上只能支付一次這一訴求,在支付系統按支付號進行分庫後便不能直接滿足了,因爲上述分庫方式會導致不同支付訂單分散在不同的分庫裏。此時,期望在數據庫中通過訂單號的唯一索引進行支付防重就不可實施了。

1.3 分庫中間件選擇

現在開源提供分庫支持的中間件較多,整體上各類分庫中間件可以分爲兩大類:一種是代理式,一種是內嵌式。

代理式分庫中間件對於業務應用無任何侵入,業務應用和未分庫時一樣使用數據庫,分庫的選擇及分庫的維度對業務層完全隱藏,接入和使用成本極低。

代理式雖有使用成本低的好處,但也存在其他一些問題。

  • 代理式在業務應用和數據庫間增加了一層,導致性能下降。
  • 代理式需要解析業務應用的SQL,並根據SQL中的分庫字段進行路由,它需要解析和適配所有SQL語法,增加了代理模塊複雜度和出錯的可能性。
  • 代理層是單獨進程,需要部署佔用資源,帶來一定的成本。

內嵌式分庫中間件是將分庫中間件內置在業務應用中,它只負責分庫的選擇,並不會解析用戶的SQL。在使用時,業務應用需將分庫字段傳遞給內嵌中間件去計算具體對應的分庫。它相比代理式性能更好。

除了性能優勢外,內嵌式同樣存在問題。

  • 有一定侵入性,業務應用於原始單庫模式相比,需要進行一定改造去適配內嵌式的API。
  • 分庫在故障轉移,數據遷移等運維工作時,需要業務應用感知,不過現在的一些內嵌式代理,已經具備非常良好的配置功能,在分庫運維時,業務應用需要配合的內容較少。

2、如何打造無狀態的存儲實現隨時切庫的寫入服務?

分庫分表只解決了容量問題,並沒有解決寫服務的高可用問題,或者說分庫分表在一定程度上增加了系統故障的概率。在讀服務裏,可以採用數據冗餘來保障架構的高可用,但在寫服務裏則無法使用此方案,因爲寫入服務的數據是用戶提交產生的,無法在寫入時使用冗餘來提高高可用性。寫冗餘需要滿足CAP原則的存儲支持,CAP原則最多隻能同時滿足兩個特性,要麼CP,要麼AP,因此寫冗餘無法直接滿足。

2.1、寫入業務的目標是成功寫入

寫業務是指需要將用戶傳入的數據進行全部存儲的一種場景:

  • 在各大網站提交的申請表單,比如落戶申請、身份證辦理申請、護照辦理申請等;
  • 在電商、外派平臺裏的購物訂單,其中會包含商品、價格、收貨人等信息。

對於寫入業務,當出現各種故障時,最重要的是保證系統可寫入。

2.1.1、如何保證隨時可寫入?

在分庫分表的架構裏,假設當前只有兩個分庫,並且這兩個分庫分別部署在不同機房,當其中一個分庫所處的機房出現網絡故障,導致該分庫不可達時,理論上系統就出現故障了,分庫分表後,數據在寫入時是按固定規則(比如用戶賬號)路由到具體分庫,當某個分庫不可達時,對應規則的數據就無法寫入了。

但是寫服務最重要的是保障數據寫入,爲了保障可寫入,能不能在某一個分庫故障後,將原有的數據全部寫入當前可用的數據庫?從保障數據可隨時寫入的角度看,此方式是可行的。

存儲依然使用分庫分表,但寫入規則發生了變化,它不再按固定路由進行寫入,而是根據當前當時可用的數據庫列表進行隨機寫入,如果某一臺數據庫出現故障不可用後,則把它從當前可用數據庫列表中移除,如果數據庫大面積不可用,可用列表中的數據庫變少時,可以適當地擴容一些數據庫資源,並將它添加到當前可用的數據列中。因此此架構可以實現隨時切換問題數據庫、隨時低成本擴容數據庫,故又稱它爲無狀態存儲架構設計。

2.2.2、如何維護可用列表?

在寫服務運行過程中,可以通過自動感知或人工確認的方式維護可用的數據庫列表。在寫服務調用數據庫寫入時,可以設置一個閾值,如果寫入某一臺數據庫,在連續幾分鐘內,失敗多少次,則可以判斷此數據庫故障,並將此判定進行上報。

判定某一臺數據庫故障並將其下線是一個挺耗費成本的事情,爲了防止誤剔除某一臺只是發生網絡抖動的數據庫,可以在真正下線某一個機器前,增加一個報警,給人工確認一個機會,可以設置多少時間內,人工未響應,即可自動下線。

對於新擴容的數據庫資源,通過系統功能自動加入即可,建議將順序寫入升級爲按權重寫入,比如對新加入的機器設置更高的寫入權重,因爲新擴容的機器容量時空的,更高的寫入權重,可以讓數據更快地在全部數據庫裏變得均衡。

2.2、寫入後如何處理

通過數據庫寫入的隨機化,實現了寫服務高可用,但想要達成一個完整的架構方案,此設計還有幾個重要的技術點需要解決。

  • 如果某一個分庫故障後便將其從列表中移除,應該如何處理其中已寫入的數據呢?
  • 因爲數據庫是隨機寫入,應該如何查詢寫入的數據呢?

在數據寫入後,用戶需要立即查看寫入內容的場景並不太多,比如上傳完論文後,你只要立刻確定論文上傳成功且查看系統裏論文內容和你上傳的一致即可。

當數據寫入隨機存儲成功後,可以在請求返回前,主動將數據寫入緩存中,同時將此次寫入的數據全部返回給前臺,但此處並不強制緩存一定要寫成功,緩存寫入失敗也可以返回成功,對時延敏感的場景,可以直接查詢此緩存。

對於無狀態存儲中的數據,可以在寫入請求中主動觸發同步模塊進行遷移,同步模塊在接收到請求後,立即將數據同步至分庫分表及緩存中。

3、如何利用依賴管控來提升寫服務的性能和可用性?

在寫業務的系統架構裏,除了需要關注存儲上的高可用,寫鏈路上各項外部依賴的管控同樣十分重要,因爲即使存儲的高可用做好了,也可能因爲外部依賴的不可用進而導致系統故障。比如寫鏈路上依賴的某一個接口性能抖動或者接口故障,都會導致你的系統不可用。

3.1、鏈路依賴的全貌

完成一個寫請求時,不僅需要依賴存儲,大部分場景還需要依賴各種外部第三方提供的接口。比如在創建訂單時,先要校驗用戶有效性、再校驗用戶的收貨地址合法性,以及獲取最新價格、扣減庫存、扣減支付金額等。完成上述的校驗和數據獲取,最後一步纔是寫存儲。

3.2、依賴並行化

假設一次寫請求要依賴二十個外部接口,可以將這些依賴全部並行化,如果一個依賴接口的性能爲10ms,以串行執行的方式,請求完所有外部依賴就需要200ms,但是改爲並行執行後,只需要10ms即可完成。實際場景中並沒有這麼精確的數字,有的外部依賴可能快一點,有的可能慢一點,實際並行執行的耗時,等於最慢的那個接口性能。

全部外部依賴的接口都可以並行是一種理想情況,接口能否並行執行的一個前置條件,就是兩個接口間沒有任何依賴關係,如果A接口執行的前置條件是需要B接口返回的數據才能執行,那麼這兩個接口則不能並行執行,按相互依賴梳理後的並行執行如下圖所示,對於並行中存在相互依賴的場景,並行化後的性能等於最長字串的性能總和。

3.3、依賴後置化

雖然整個鏈路上會有較多外部接口,但大部分場景中,很多接口是可以後置化的,後置化是指當接口裏的業務流程處理完成並返回給用戶後,後置去處理一些非重要切對實時性無要求的場景,比如在提交訂單後,用戶只需要查看訂單是否下單成功,以及對應價格、商品和數量是否正確,而對於商品的詳細描述信息,所歸屬的商家名稱等信息並不會特別關心,如果在提單的同時還需要獲取這些用戶不太關係的信息,會給整個提單的性能和可用率帶來非常大的影響,鑑於這種情況,可以在提單後異步補齊這些僅供展示的信息。

對於一些可以後置補齊的數據,可以在寫請求完成時將原始數據寫入一張任務表,然後啓動一個異步Worker,異步Worker再調用後置化的接口去補齊數據,以及執行相應的後置流程(比如發送MQ等)

3.4、顯式設置超時和重試

即便是使用了後置化的方案,仍然會有一些外部接口需要同步調用,如果這些同步調用的接口出現性能抖動或者可用率下降,就需要通過顯式設置超時和重試來規避上述問題。

3.4.1、超時設置

設置超時是爲了防止依賴的外部接口性能突然變差,比如從幾十毫秒飆升至十幾秒及以上,進而導致你的請求被阻塞,此請求線程得不到釋放,還會導致你的微服務的RPC線程池被佔滿,此時又會帶來新的問題,進程的RPC線程池被佔滿後,就無法再接受任何新的請求了,你的系統基本上也就宕機了。

3.4.2、重試設置

除了超時之外,還可以對依賴的讀接口設置調用失敗自動重試,重試次數設置爲一次,自動重試只能設置讀接口,讀接口是無副作用的,重試對被依賴方無數據上的影響,而寫接口是有狀態的,如果依賴方沒有做好冪等,設置自動重試可能會導致髒數據產生。設置自動重試是爲了提高接口的可用性,因爲依賴的外部接口的某一臺機器可能會因爲網絡波動、機器重啓等導致當次調用超時進而失敗。如果設置了自動重試,就可能重試到另外一臺正常機器,保障服務的可用性。

3.5、降級方式

當依賴的讀服務接口,同時該接口返回的數據只用來補齊本次請求的數據時,可以對其返回的數據採用前置緩存,當出現故障時,可以使用前置緩存頂一段時間,給依賴提供方提供一定時間去修復緩存。

對產生故障的依賴進行後置處理,比如發佈微博前需要判斷是否爲非法內容,可能要依賴風控的接口進行合規性判斷,當風控接口故障後,可以直接降級,先將微博數據寫入存儲並標記未校驗,但此數據可能是不合規的,可以在業務上進行適當降級,未校驗的數據只允許用戶自己看,待風控故障恢復後再進行數據校驗,校驗通過後允許所有人可見。

對於需要寫下游的場景,比如提單時扣減庫存,當庫存不夠邊不能下單的場景,當庫存故障時,可降級直接跳過庫存扣減,但需要提示用戶後續可能無貨,修復故障後進行異步校驗庫存,如果校驗不通過,系統取消訂單或發送消息通知客戶進行人工判斷是否需要等待商家補貨。此方式是一種預承載,但最終有可能失敗的有損降級方案。

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