「一致性」架構設計

一、session一致

1、緣起

什麼是session

服務器爲每個用戶創建一個會話,存儲用戶的相關信息,以便多次請求能夠定位到同一個上下文。

 

Web開發中,web-server可以自動爲同一個瀏覽器的訪問用戶自動創建session,提供數據存儲功能。最常見的,會把用戶的登錄信息、用戶信息存儲在session中,以保持登錄狀態。

 

什麼是session一致性問題?

只要用戶不重啓瀏覽器,每次http短連接請求,理論上服務端都能定位到session,保持會話。


當只有一臺web-server提供服務時,每次http短連接請求,都能夠正確路由到存儲session的對應web-server(廢話,因爲只有一臺)。

 

此時的web-server是無法保證高可用的,採用「冗餘+故障轉移」的多臺web-server來保證高可用時,每次http短連接請求就不一定能路由到正確的session


如上圖,假設用戶包含登錄信息的session都記錄在第一臺web-server上,反向代理如果將請求路由到另一臺web-server上,可能就找不到相關信息,而導致用戶需要重新登錄。

 

web-server高可用時,如何保證session路由的一致性,是今天將要討論的問題。

 

2、session同步法


思路多個web-server之間相互同步session,這樣每個web-server之間都包含全部的session

 

優點web-server支持的功能,應用程序不需要修改代碼

 

不足

  • session的同步需要數據傳輸,內網帶寬,有時延

  • 所有web-server都包含所有session數據,數據量受內存限制,無法水平擴展

  • 有更多web-server時要歇菜

 

3、客戶端存儲法


思路:服務端存儲所有用戶的session,內存佔用較大,可以session存儲到瀏覽器cookie,每個端只要存儲一個用戶的數據了

 

優點服務端不需要存儲

 

缺點

  • 每次http請求都攜帶session外網帶寬

  • 數據存儲在端上,並在網絡傳輸,存在泄漏、篡改、竊取等安全隱患

  • session存儲的數據大小受cookie限制

 

「端存儲」的方案雖然不常用,但確實是一種思路。

 

4、反向代理hash一致性

思路web-server爲了保證高可用,有多臺冗餘,反向代理層能不能做一些事情,讓同一個用戶的請求保證落在一臺web-server呢?


方案一:四層代理hash

反向代理層使用用戶ip來做hash,以保證同一個ip的請求落在同一個web-server

 


方案二:七層代理hash

反向代理使用http協議中的某些業務屬性來做hash,例如sidcity_iduser_id等,能夠更加靈活的實施hash策略,以保證同一個瀏覽器用戶的請求落在同一個web-server

 

優點

  • 只需要改nginx配置,不需要修改應用代碼

  • 負載均衡,只要hash屬性是均勻的,多臺web-server的負載是均衡的

  • 可以web-server水平擴展session同步法是不行的,受內存限制)

 

不足

  • 如果web-server重啓,一部分session會丟失,產生業務影響,例如部分用戶重新登錄

  • 如果web-server水平擴展,rehashsession重新分佈,也會有一部分用戶路由不到正確的session

 

session一般是有有效期的,所有不足中的兩點,可以認爲等同於部分session失效,一般問題不大。

 

對於四層hash還是七層hash,個人推薦前者讓專業的軟件做專業的事情,反向代理就負責轉發,儘量不要引入應用層業務屬性,除非不得不這麼做(例如,有時候多機房多活需要按照業務屬性路由到不同機房的web-server)。

 

5、後端統一存儲


思路session存儲在web-server後端的存儲層,數據庫或者緩存

 

優點

  • 沒有安全隱患

  • 可以水平擴展,數據庫/緩存水平切分即可

  • web-server重啓或者擴容都不會有session丟失

 

不足:增加了一次網絡調用,並且需要修改應用代碼

 

對於db存儲還是cache,個人推薦後者session讀取的頻率會很高,數據庫壓力會比較大。如果有session高可用需求,cache可以做高可用,但大部分情況下session可以丟失,一般也不需要考慮高可用。

 

6、總結

保證session一致性的架構設計常見方法:

  • session同步法:多臺web-server相互同步數據

  • 客戶端存儲法:一個用戶只存儲自己的數據

  • 反向代理hash一致性:四層hash和七層hash都可以做,保證一個用戶的請求落在一臺web-server

  • 後端統一存儲web-server重啓和擴容,session也不會丟失

 

對於方案3和方案4,個人建議推薦後者

  • web層、service層無狀態是大規模分佈式系統設計原則之一session屬於狀態,不宜放在web

  • 讓專業的軟件做專業的事情web-serversession?還是讓cache去做這樣的事情吧


二、數據庫主從一致性

需求緣起

大部分互聯網的業務都是讀多寫少的場景,數據庫層面,讀性能往往成爲瓶頸。如下圖:業界通常採用一主多從,讀寫分離,冗餘多個讀庫數據庫架構來提升數據庫的讀性能。


這種架構的一個潛在缺點是,業務方有可能讀取到並不是最新的舊數據


1)系統先對DB-master進行了一個寫操作,寫主庫

2)很短的時間內併發進行了一個讀操作,讀從庫,此時主從同步沒有完成,故讀取到了一箇舊數據

3)主從同步完成

 

有沒有辦法解決或者緩解這類由於主從延時導致讀取到舊數據」的問題呢,這是本文要集中討論的問題。

 

方案一(半同步複製)

不一致是因爲寫完成後,主從同步有一個時間差,假設是500ms,這個時間差有讀請求落到從庫上產生的。有沒有辦法做到,等主從同步完成之後,主庫上的寫請求再返回呢?答案是肯定的,就是大家常說的「半同步複製」semi-sync


1)系統先對DB-master進行了一個寫操作,寫主庫

2)等主從同步完成,寫主庫的請求才返回

3)讀從庫,讀到最新的數據(如果讀請求先完成,寫請求後完成,讀取到的是「當時」最新的數據)

方案優點:利用數據庫原生功能,比較簡單

方案缺點:主庫的寫請求時延會增長,吞吐量會降低

 

方案二(強制讀主庫)

如果不使用「增加從庫」的方式來增加提升系統的讀性能,完全可以讀寫都落到主庫,這樣就不會出現不一致了:


方案優點:「一致性」上不需要進行系統改造

方案缺點:只能通過cache來提升系統的讀性能,這裏要進行系統改造

 

方案三(數據庫中間件)

如果有了數據庫中間件,所有的數據庫請求都走中間件,這個主從不一致的問題可以這麼解決:


1)所有的讀寫都走數據庫中間件,通常情況下,寫請求路由到主庫,讀請求路由到從庫

2記錄所有路由到寫庫的key,在經驗主從同步時間窗口內(假設是500ms),如果有讀請求訪問中間件,此時有可能從庫還是舊數據,就把這個key上的讀請求路由到主庫

3)經驗主從同步時間過完後,對應key的讀請求繼續路由到從庫

方案優點:能保證絕對一致

方案缺點:數據庫中間件的成本比較高

 

方案四(緩存記錄寫key法)

既然數據庫中間件的成本比較高,有沒有更低成本的方案來記錄某一個庫的某一個key上發生了寫請求呢?很容易想到使用緩存,當寫請求發生的時候:


1)將某個庫上的某個key要發生寫操作,記錄在cache裏,並設置「經驗主從同步時間」的cache超時時間,例如500ms

2)修改數據庫

 

而讀請求發生的時候:


1)先到cache裏查看,對應庫的對應key有沒有相關數據

2如果cache hit,有相關數據,說明這個key上剛發生過寫操作,此時需要將請求路由到主庫讀最新的數據

3如果cache miss,說明這個key上近期沒有發生過寫操作,此時將請求路由到從庫,繼續讀寫分離

方案優點:相對數據庫中間件,成本較低

方案缺點:爲了保證「一致性」,引入了一個cache組件,並且讀寫數據庫時都多了一步cache操作

 

總結

爲了解決主從數據庫讀取舊數據的問題,常用的方案有四種:

1)半同步複製

2)強制讀主

3)數據庫中間件

4)緩存記錄寫key

三、數據庫雙主一致性

1、雙主保證高可用

MySQL數據庫集羣常使用一主多從,主從同步,讀寫分離的方式來擴充數據庫的讀性能,保證讀庫的高可用,但此時寫庫仍然是單點。

 

在一個MySQL數據庫集羣中可以設置兩個主庫,並設置雙向同步,以冗餘寫庫的方式來保證寫庫的高可用

 

2、併發引發不一致

數據冗餘會引發數據的一致性問題,因爲數據的同步有一個時間差,併發的寫入可能導致數據同步失敗,引起數據丟失:


如上圖所述,假設主庫使用了auto increment來作爲自增主鍵:

  • 兩個MySQL-master設置雙向同步可以用來保證主庫的高可用

  • 數據庫中現存的記錄主鍵是1,2,3

  • 主庫1插入了一條記錄,主鍵爲4,並向主庫2同步數據

  • 數據同步成功之前,主庫2也插入了一條記錄,由於數據還沒有同步成功,插入記錄生成的主鍵也爲4,並向主庫1也同步數據

  • 主庫1和主庫2都插入了主鍵爲4的記錄,雙主同步失敗,數據不一致

 

3、相同步長免衝突

能否保證兩個主庫生成的主鍵一定不衝突呢?

回答

  • 設置不同的初始值

  • 設置相同的增長步長

就能夠做到。

 


如上圖所示:

  • 兩個MySQL-master設置雙向同步可以用來保證主庫的高可用

  • 庫1的自增初始值是1,庫2的自增初始值是2,增長步長都爲2

  • 庫1中插入數據主鍵爲1/3/5/7,庫2中插入數據主鍵爲2/4/6/8,不衝突

  • 數據雙向同步後,兩個主庫會包含全部數據


如上圖所示,兩個主庫最終都將包含1/2/3/4/5/6/7/8所有數據,即使有一個主庫掛了,另一個主庫也能夠保證寫庫的高可用。

 

4、上游生成ID避衝突

換一個思路,爲何要依賴於數據庫的自增ID,來保證數據的一致性呢?

完全可以由業務上游,使用統一的ID生成器,來保證ID的生成不衝突


如上圖所示,調用方插入數據時,帶入全局唯一ID,而不依賴於數據庫的auto increment,也能解決這個問題。

 

5、消除雙寫不治本

使用auto increment兩個主庫併發寫可能導致數據不一致,只使用一個主庫提供服務,另一個主庫作爲shadow-master,只用來保證高可用,能否避免一致性問題呢?


如上圖所示:

  • 兩個MySQL-master設置雙向同步可以用來保證主庫的高可用

  • 只有主庫1對外提供寫入服務

  • 兩個主庫設置相同的虛IP,在主庫1掛掉或者網絡異常的時候,虛IP自動漂移,shadow master頂上,保證主庫的高可用

 

這個切換由於虛IP沒有變化,所以切換過程對調用方是透明的,但在極限的情況下,也可能引發數據的不一致:


如上圖所示:

  • 兩個MySQL-master設置雙向同步可以用來保證主庫的高可用,並設置了相同的虛IP

  • 網絡抖動前,主庫1對上游提供寫入服務,插入了一條記錄,主鍵爲4,並向shadow master主庫2同步數據

  • 突然主庫1網絡異常,keepalived檢測出異常後,實施虛IP漂移,主庫2開始提供服務

  • 在主鍵4的數據同步成功之前,主庫2插入了一條記錄,也生成了主鍵爲4的記錄,結果導致數據不一致

 

6、內網DNS探測

虛IP漂移,雙主同步延時導致的數據不一致,本質上,需要在雙主同步完數據之後,再實施虛IP偏移,使用內網DNS探測,可以實現shadow master延時高可用:

  • 使用內網域名連接數據庫,例如:db.58daojia.org

  • 主庫1和主庫2設置雙主同步,不使用相同虛IP,而是分別使用ip1和ip2

  • 一開始db.58daojia.org指向ip1

  • 用一個小腳本輪詢探測ip1主庫的連通性

  • 當ip1主庫發生異常時,小腳本delay一個x秒的延時,等待主庫2同步完數據之後,再將db.58daojia.org解析到ip2

  • 程序以內網域名進行重連,即可自動連接到ip2主庫,並保證了數據的一致性

 

7、總結

主庫高可用,主庫一致性,一些小技巧:

  • 雙主同步是一種常見的保證寫庫高可用的方式

  • 設置相同步長,不同初始值,可以避免auto increment生成衝突主鍵

  • 不依賴數據庫,業務調用方自己生成全局唯一ID是一個好方法

  • shadow master保證寫庫高可用,只有一個寫庫提供服務,並不能完全保證一致性

  • 內網DNS探測,可以實現在主庫1出現問題後,延時一個時間,再進行主庫切換,以保證數據一致性

四、數據庫與緩存一致性

本文主要討論這麼幾個問題:

1)數據庫主從延時爲何會導致緩存數據不一致

2)優化思路與方案

 

1、需求緣起

上一篇《緩存架構設計細節二三事》中有一個小優化點,在只有主庫時,通過「串行化」的思路可以解決緩存與數據庫中數據不一致。引發大家熱烈討論的點是「在主從同步,讀寫分離的數據庫架構下,有可能出現髒數據入緩存的情況,此時串行化方案不再適用了」,這就是本文要討論的主題。

 

2、爲什麼數據會不一致

爲什麼會讀到髒數據,有這麼幾種情況:

1)單庫情況下,服務層的併發讀寫,緩存與數據庫的操作交叉進行


雖然只有一個DB,在上述詭異異常時序下,也可能髒數據入緩存:

1)請求A發起一個寫操作,第一步淘汰了cache,然後這個請求因爲各種原因在服務層卡住了(進行大量的業務邏輯計算,例如計算了1秒鐘),如上圖步驟1

2)請求B發起一個讀操作,讀cachecache miss,如上圖步驟2

3)請求B繼續讀DB,讀出來一個髒數據,然後髒數據入cache,如上圖步驟3

4)請求A卡了很久後終於寫數據庫了,寫入了最新的數據,如上圖步驟4

這種情況雖然少見,但理論上是存在的 後發起的請求B在先發起的請求A中間完成了。

 

2)主從同步,讀寫分離的情況下,讀從庫讀到舊數據

在數據庫架構做了一主多從,讀寫分離時,更多的髒數據入緩存是下面這種情況:


1)請求A發起一個寫操作,第一步淘汰了cache,如上圖步驟1

2)請求A寫數據庫了,寫入了最新的數據,如上圖步驟2

3)請求B發起一個讀操作,讀cachecache miss,如上圖步驟3

4)請求B繼續讀DB,讀的是從庫,此時主從同步還沒有完成,讀出來一個髒數據,然後髒數據入cache,如上圖步4

5)最後數據庫的主從同步完成了,如上圖步驟5

這種情況請求A和請求B的時序是完全沒有問題的,是主動同步的時延(假設延時1秒鐘)中間有讀請求讀從庫讀到髒數據導致的不一致。

 

那怎麼來進行優化呢?


3、不一致優化思路

有同學說「那能不能先操作數據庫,再淘汰緩存」,這個是不行的。

 

出現不一致的根本原因:

1)單庫情況下,服務層在進行1s的邏輯計算過程中,可能讀到舊數據入緩存

2)主從庫+讀寫分離情況下,在1s鍾主從同步延時過程中,可能讀到舊數據入緩存

既然舊數據就是在那1s的間隙中入緩存的,是不是可以在寫請求完成後,再休眠1s,再次淘汰緩存,就能將這1s內寫入的髒數據再次淘汰掉呢?

答案是可以的

 

寫請求的步驟由2步升級爲3步:

1)先淘汰緩存

2)再寫數據庫(這兩步和原來一樣)

3)休眠1秒,再次淘汰緩存

這樣的話,1秒內有髒數據如緩存,也會被再次淘汰掉,但帶來的問題是:

1所有的寫請求都阻塞了1秒,大大降低了寫請求的吞吐量,增長了處理時間,業務上是接受不了的

 

再次分析,其實第二次淘汰緩存是爲了保證緩存一致而做的操作,而不是業務要求,所以其實無需等待,用一個異步的timer,或者利用消息總線異步的來做這個事情即可


寫請求由2步升級爲2.5步:

1)先淘汰緩存

2)再寫數據庫(這兩步和原來一樣)

2.5)不再休眠1s,而是往消息總線esb發送一個消息,發送完成之後馬上就能返回

這樣的話,寫請求的處理時間幾乎沒有增加,這個方法淘汰了緩存兩次,因此被稱爲「緩存雙淘汰」法。這個方法付出的代價是,緩存會增加1cache miss(代價幾乎可以忽略)。

 

而在下游,有一個異步淘汰緩存的消費者,在接收到消息之後,asy-expire1s之後淘汰緩存。這樣,即使1s內有髒數據入緩存,也有機會再次被淘汰掉。

 

上述方案有一個缺點需要業務線的寫操作增加一個步驟有沒有方案對業務線的代碼沒有任何入侵呢,是有的,通過分析線下的binlog來異步淘汰緩存:


業務線的代碼就不需要動了,新增一個線下的讀binlog的異步淘汰模塊,讀取到binlog中的數據,異步的淘汰緩存。

 

提問:爲什麼上文總是說1s,這個1s是怎麼來的?

回答:1s只是一個舉例,缺點需要業務線的寫操作增加一個步驟有沒有方案對業務線的代碼沒有任何入侵呢,是有的,通過分析線下的binlog來異步淘汰緩存:


業務線的代碼就不需要動了,新增一個線下的讀binlog的異步淘汰模塊,讀取到binlog中的數據,異步的淘汰緩存。

 

提問:爲什麼上文總是說1s,這個1s是怎麼來的?

回答:1s只是一個舉例,需要根據業務的數據量與併發量,觀察主從同步的時延來設定這個值。例如主從同步的時延爲200ms,這個異步淘汰cache設置爲258ms就是OK的。

 

4、總結

異常時序或者讀從庫導致髒數據入緩存時,可以用二次異步淘汰緩存雙淘汰法來解決緩存與數據庫中數據不一致的問題,具體實施至少有三種方案:

1timer異步淘汰(本文沒有細講,本質就是起個線程專門異步二次淘汰緩存)

2 4、總結

異常時序或者讀從庫導致髒數據入緩存時,可以用二次異步淘汰緩存雙淘汰法來解決緩存與數據庫中數據不一致的問題,具體實施至少有三種方案:

1timer異步淘汰(本文沒有細講,本質就是起個線程專門異步二次淘汰緩存)

2總線異步淘汰

3binlog異步淘汰

五、數據冗餘一致性

1、需求緣起

互聯網很多業務場景的數據量很大,此時數據庫架構要進行水平切分,水平切分會有一個patition key通過patition key的查詢能夠直接定位到庫,但是非patition key 五、數據冗餘一致性

相關文章
相關標籤/搜索