實戰經驗丨如何避免雲服務數據不一樣步?

本文是野狗科技聯合創始人&架構師謝喬在ArchSummit 北京2015全球架構師峯會上進行的《基於數據同步雲服務架構實踐》的演講實錄,主要分爲三個方面:野狗的數據同步理念,數據同步的架構演進,數據同步的細節問題。
野狗官博:https://blog.wilddog.com/
野狗官網:https://www.wilddog.com/
公衆訂閱號:wilddogbaas前端

圖片描述

如下爲演講實錄:

可能你們在實際的應用場景中不使用數據同步的業務模式,可是我是想跟你們分享咱們在演進過程當中一些問題的解決思路,但願能對你們有所幫助。redis

圖片描述

今天的演講內容主要分三個議題:算法

  • 野狗的數據同步理念sql

  • 數據同步的架構演進mongodb

  • 數據同步的細節問題數據庫

幻燈片4

野狗的數據同步理念

首先從雲端這塊兒開始講起,咱們的數據存儲是個Schema-free的形式,樹形的數據庫像一顆Json樹,更像前端工程師們用的數據結構,它能把原來的關係型數據經過一些關聯查詢造成聚合型的數據,好比blog,裏面有標題、回覆等內容,就至關於把數據從新聚合,這樣數據之間的關係就更直觀了,方便你們快速的設計比較好的數據結構,完美的與url結合,每條數據都經過url來惟必定位,每一個path做爲一個key,就成爲了key-value的數據結構。api

幻燈片5

經典的雲服務是這樣的:現提供一個API,而後有其餘的auth接入,雲端有存儲,有用戶管理,有hosting功能,還有周邊的一些工具,客戶端經過rest api這種方式與雲端進行交互來開發你的業務模型。緩存

幻燈片6

而野狗除了這一部分之外,還有一個富客戶端的SDK,本地也作了存儲,當本地數據發生變化的時候會經過一個事件來通知用戶,而後用戶進行修改。服務器

幻燈片7

具體來說,是客戶端與服務端創建一個長鏈接,來完成數據同步,當同步完成以後產生數據變化,就能夠完成業務邏輯的實現。若是咱們把模型再抽象一點,就像一個主從的同步,客戶端做爲從,和雲端進行副本級的同步過程。前端工程師

也能夠有另一種同步方式,你們的服務能夠與野狗雲進行實時同步。好比說,你的服務端進行了一次數據修改,同步到雲端,雲端把這個修改同步給關注這個數據的客戶端。

幻燈片8

數據實現同步的基本模型是這樣的:

開始有一個初始化的慢同步,能夠作全量的同步或者條件同步,好比這個例子,客戶端A進行了條件同步,同步到本地產生了一個本地副本,客戶端B經過全同步拉取到本地造成一個本地副本。當客戶端A修改後,產生了新的數據,咱們把它叫增量同步,數據會push到雲端。而後本地使用best-effort模式,客戶端先成功觸發事件,而後再同步到雲端,雲端再同步到其餘的客戶端,實現最終一致性。

這個過程很像op log的過程,也是基於長鏈接的,若是每次鏈接發生了異常,這裏會從新鏈接進行一次初始化慢同步過程。這也是咱們所作的數據同步和消息推送的根本區別,緣由是,消息推送要保證每一個消息順序到達,並且不丟失,數據同步則是在性能上的提高,只關心最終的數據狀態。一旦發生異常,客戶端從新連入到雲端之後,不會把以前過程當中的op log都傳過去,只須要從新進行一次初始化操做,讓兩端進行同步恢復就能夠了。

數據同步的架構演進

幻燈片9

剛纔講的業務方面的內容可能比較枯燥,接下來就是咱們技術架構的演進過程。

首先看一下咱們技術架構的特色,跟其餘傳統業務不太同樣,屬於寫多讀少。由於讀只須要讀一次到客戶端之後,讀客戶端的副本就能夠了,並且一些修改操做直接修改客戶端本地,再由終端同步到雲端,剩下的操做大部分都是寫操做。寫同步固然是越實時越好,但問題就是讀的性能確定會有一些延遲,後面會詳細講解。

咱們實現的是最終一致性,由於這不是強一致性的架構,不少客戶端能夠關注同一個數據節點的變化。由於咱們採用最終一致性,因此會致使多個客戶端能夠同時進行寫操做,就必然會產生寫衝突的問題,因此並行寫衝突的問題也要解決。

實時性是咱們的特色,這裏暫時不詳細說。

最後一個是冪等操做。

幻燈片11

這是0.1版本的架構框圖,這個主要面向咱們的初期用戶,用來驗證咱們產品是否被用戶承認。這個架構由一個接入層組成,用來維護和客戶端的長鏈接,若是有一個請求過來,會產生數據操做到數據處理,數據處理直接寫Mysql。

Mysql這塊兒直接用了主從同步的模式來保留必定的可用性,而後再進行數據推送。數據推送的時候,先從Redis集羣中進行lookup操做,這個操做的目的是尋找要修改的數據節點被哪些終端所關注,而後再進行push操做。

這裏的數據採用了物化路徑存儲,也就是說,若是存的是/a/b/c的數據,其實是存/a一條/a/b一條,/a/b/c一條。

幻燈片12

業務獲得承認以後,須要對早期用戶有一個性能的保證,因此就有了這個0.2版本的架構框圖,把以前的Mysql改爲了mongodb。使用mongodb的緣由是能夠動態建立數據庫,把用戶的數據在APP級別進行隔離,這樣不會互相影響。同時,mongodb也帶來了讀寫性能的提高。

同時咱們採用了副本集多活,利用mongodb本身的副本集主掛了以後自動切從的方案。

機槍換導彈的意思是以前是一次一次對數據庫進行操做,如今咱們作了批量的操做和合並的push。以前的操做一個push會影響多個數據節點發生變化,會一條一條的推給關注的終端,如今能夠作一個合併的push。

幻燈片13

當咱們的產品進入bate版測試以後就須要面向廣大的公測用戶了,咱們逐漸要面對的就是寫壓力了。由於mongodb的寫操做對於同一個數據表是鎖表的,因此寫是一個串行的性能問題,因此咱們這裏加了一個寫緩衝隊列,這是你們都會想到的解決方案。

咱們這裏使用了kafka。一條數據來了以後,由生產者進入kafka,而後由消費者把kafka的數據拿出來進行批量消費,最後內存生成一個操做樹的緩存,再批量寫入mongodb。這塊兒更相似Nagle算法,達到必定的操做量或者達到必定的超時時間後,就同步到Mysql數據庫。

可能你們有過加寫緩衝的經驗,這時候確定會面臨讀性能降低的問題。由於這時候咱們在讀到mongodb的時候是一個已通過時的數據快照,有一些操做還暫存在kafka,寫緩存隊列中,因此必需要解決這個讀不一致的問題。當讀操做來的時候,先從mongodb中讀取到快照,而後再記錄你當前執行到哪,一共有哪些操做還未執行。讀取完以後,在內存進行一個回放操做,拿到的就是比較新的快照版本了。

幻燈片14

可是這裏還有一個問題,在操做的過程當中,還會有新的寫操做過的內容,就算回放完,也是過時的版本。這裏有點像redis的主從同步同樣,拿到內存的最後版本後還有新過來的寫操做進入push和wait隊列,先把歷史版本推給客戶端,再把以後的寫操做一次推給客戶端。最後在客戶端進行計算達到的就是最終一致性,用戶拿到的就是最新的數據版本。

幻燈片15

在beta版發佈一段時間以後,服務器的負載是很平穩的上升,延遲是十、十一、12ms,每週是這樣一種遞增。可是忽然有一天咱們發現延遲暴增到上百ms,甚至到700ms,咱們開始各類排查。可是查過以後,kafka、mongodb等等,都一切正常,最後才查到原來是由於push這裏須要查一次redis形成的。也就是說,咱們在redis中存的是路徑Key,路徑下面是有哪些客戶端節點關注了這個key,因此這裏要進行一次模糊匹配查詢,當一個實例的redis數據量到達20w、30w條的時候,若是用模糊查詢性能會很是低,延遲會達到幾百ms。因此咱們這裏採用了臨時方案,用mongodb來代替redis,用mongodb加它的索引來提高模糊查詢的性能。

幻燈片16

這裏也爲咱們敲了個警鐘,咱們須要作性能監控,才能真正的面對用戶。後來咱們就基於flume作了一套本身的性能監控。Flume能夠統計日誌,還有對每個系統延遲的調用,以及異常報警,都寫入flume,再作一個flume的後臺處理。

咱們在設計架構的時候,老是把咱們的關注點放在最容易發生問題的位置,而每每有時候雖然你解決了這塊兒的問題,可是因爲總量上來了,還會影響一些原來不關注的地方出現問題,徹底出乎意料。

數據同步的細節問題

剛纔是簡單架構框圖的介紹,如今是咱們數據同步面臨的一些細節的介紹。

幻燈片18

幻燈片19

兩個客戶端同時修改本地的副本,須要考慮到數據的靜態一致性,同時還要考慮到寫隔離的問題。對於這個問題其實有兩個解決方案:一是中心化鎖機制;另一個是進程間協商機制。可是鎖機制會有單點故障問題。因此咱們作了一個分佈式樹形鎖機制。不過這裏有一些須要注意的問題:一、tryLock和release 須要2次的交互;二、須要注意註冊Lock的有效期;三、要等待Lock超時;四、最好使用動態hash;五、鏈接異常時退化。

幻燈片20

還有一些性能問題,由於每一個App都有一個樹形鎖,因此是單進程就算你進行了這種操做,在理論上是會有一個吞吐量的上限的。任何操做都要先去嘗試先得到鎖,這個操做實際上是一個浪費的操做。主要性能的點有兩個:一個是單次push sync量比較大,能夠致使阻塞。另一個就是異步push sync。

幻燈片21

由於以上這些緣由,一個噁心的架構就誕生了。主要由於縮減了write操做的過程,還有要保證雲端與客戶端的一致性。整個系統就會太過於複雜,不肯定因素太多。

幻燈片22

可是咱們作技術不能意淫。在真實的應用場景中,有同一客戶端場景和不一樣客戶端場景。可是二者所佔的比例是不同的。不一樣客戶端的寫衝突有0.3%,同一客戶端寫衝突有4.1%。因此說,其實衝突的機率是很是小的。用上面那種方式就會有種「殺雞焉用宰牛刀」的感受。

幻燈片23

因此,咱們提出了一個理念:讓上帝的歸上帝,野狗的歸野狗。具體到實施上就是讓用戶進行可配置化,主要有四種方式:一、默認不啓用;二、減小沒必要要的開銷;三、下降鎖粒度;四、由appld hash改進爲path hash。在這裏技術的同窗就要注意了,有些問題其實不須要多麼厲害的架構,若是能在業務層面進行解決,就儘可能將問題在業務層面解決,不要作特別複雜的架構去解決一些虛無縹緲的問題。

幻燈片24

要解決這些問題,主要仍是依賴寫時的樹形鎖,達到順序push的效果。若是沒有這個操做,就會出現客戶端數據不一致的問題,因此push順序很重要,必定要一致。

幻燈片25

幻燈片26

主要是須要保證同一客戶端的順序性。以「太空站」這個遊戲爲例。飛機走着走着回發生回退的現象,形成這個現象的緣由,是由於客戶端在進行寫處理的時候是進行並行處理的。這個問題很好解決,能夠按照客戶端ID散列到每個數據處理的進程上,在數據處理進程內部達到一個數據寫一致的效果。進程內的鎖也要實現順序性,因此目標又變成了解決write的性能。

幻燈片27

第四個問題就是最終一致性的問題,剛纔咱們說的都是雲端和被同步客戶端之間的問題。

可是這塊兒還會產生的問題模型是客戶端A在本地先作修改,由1修改爲2,將2同步到雲端之後,雲端也修改爲2,雲端再push到其餘的客戶端,對這個數據有關注的,也會修改爲2,這樣就解決了最終一致性的問題。

看似很完美,但仍是有漏洞。

幻燈片28

剛纔所作的這一切,只能保證雲端和被同步的客戶端的數據是一致的,可是這種狀況因爲客戶端能夠都先對本地進行修改,客戶端A修改爲2,客戶端B修改爲3,在推送到雲端的過程當中,A進行的修改會寫入,B進行的修改也會寫入。最後執行的時候若是在雲端執行的時候是以某種順序推送過來的,假設雲端最後生成的是2那就是說,雲端和左側是一致的,就會與另外一側的節點產生不一致。

也就是說,因爲並行寫,最後會有一個客戶端產生不一致的問題。

幻燈片29

這裏咱們也沒有用到一些複雜的算法,用了一個push給本身的模型來化解這個問題,達到最終的一致性。在並行寫和推送的時候仍然推送給本身,因爲推送的過程是串行的,只有推送完前面的一次,纔會推送對這個節點的下一次改變操做。這個推送完畢之後,由於是TCP的,因此會按順序推送過去,那就能夠認爲,在這個推送過程當中,全部終端都達到了一致性。

會產生的問題你們也能夠看到就是可能會出現,數據由2修改爲3,再修改爲2。在這裏咱們須要對一致性問題和性能作一個取捨,固然仍是選擇爲了達到實時,因此採用這種比較弱的最終一致性方案。

幻燈片30

最後一個問題,是一個原子性問題,由於咱們是冪等操做,因此不會支持if then,i ++的操做。咱們在這裏用了一個自旋鎖的CAS機制,在本地拉到數據以後作一個hash,這個hash和要修改的值作一個複合操做一塊兒發到雲端,而云端也對這個數據進行一個hash,若是兩個hash是一致的,那才能認爲能夠操做,才能覆蓋。若是不一致的話,從新從雲端再次同步一些數據到本地產生一些副本,進行上一步的操做,直到成功爲止。不過咱們也有一個重試次數,如今的設置是20次。

今天的演講就到這裏了,謝謝你們。

相關文章
相關標籤/搜索