一、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,例如sid,city_id,user_id等,能夠更加靈活的實施hash策略,以保證同一個瀏覽器用戶的請求落在同一個web-server上
優點:
只需要改nginx配置,不需要修改應用代碼
負載均衡,只要hash屬性是均勻的,多臺web-server的負載是均衡的
可以支持web-server水平擴展(session同步法是不行的,受內存限制)
不足:
如果web-server重啓,一部分session會丟失,產生業務影響,例如部分用戶重新登錄
如果web-server水平擴展,rehash後session重新分佈,也會有一部分用戶路由不到正確的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-server存session?還是讓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)單庫情況下,服務層的併發讀寫,緩存與數據庫的操作交叉進行
雖然只有一個DB,在上述詭異異常時序下,也可能髒數據入緩存:
1)請求A發起一個寫操作,第一步淘汰了cache,然後這個請求因爲各種原因在服務層卡住了(進行大量的業務邏輯計算,例如計算了1秒鐘),如上圖步驟1
2)請求B發起一個讀操作,讀cache,cache miss,如上圖步驟2
3)請求B繼續讀DB,讀出來一個髒數據,然後髒數據入cache,如上圖步驟3
4)請求A卡了很久後終於寫數據庫了,寫入了最新的數據,如上圖步驟4
這種情況雖然少見,但理論上是存在的, 後發起的請求B在先發起的請求A中間完成了。
(2)主從同步,讀寫分離的情況下,讀從庫讀到舊數據
在數據庫架構做了一主多從,讀寫分離時,更多的髒數據入緩存是下面這種情況:
1)請求A發起一個寫操作,第一步淘汰了cache,如上圖步驟1
2)請求A寫數據庫了,寫入了最新的數據,如上圖步驟2
3)請求B發起一個讀操作,讀cache,cache miss,如上圖步驟3
4)請求B繼續讀DB,讀的是從庫,此時主從同步還沒有完成,讀出來一個髒數據,然後髒數據入cache,如上圖步4
5)最後數據庫的主從同步完成了,如上圖步驟5
這種情況請求A和請求B的時序是完全沒有問題的,是主動同步的時延(假設延時1秒鐘)中間有讀請求讀從庫讀到髒數據導致的不一致。
那怎麼來進行優化呢?
有同學說「那能不能先操作數據庫,再淘汰緩存」,這個是不行的。
出現不一致的根本原因:
(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發送一個消息,發送完成之後馬上就能返回
這樣的話,寫請求的處理時間幾乎沒有增加,這個方法淘汰了緩存兩次,因此被稱爲「緩存雙淘汰」法。這個方法付出的代價是,緩存會增加1次cache miss(代價幾乎可以忽略)。
而在下游,有一個異步淘汰緩存的消費者,在接收到消息之後,asy-expire在1s之後淘汰緩存。這樣,即使1s內有髒數據入緩存,也有機會再次被淘汰掉。
上述方案有一個缺點,需要業務線的寫操作增加一個步驟,有沒有方案對業務線的代碼沒有任何入侵呢,是有的,通過分析線下的binlog來異步淘汰緩存:
業務線的代碼就不需要動了,新增一個線下的讀binlog的異步淘汰模塊,讀取到binlog中的數據,異步的淘汰緩存。
提問:爲什麼上文總是說1s,這個1s是怎麼來的?
回答:1s只是一個舉例,缺點,需要業務線的寫操作增加一個步驟,有沒有方案對業務線的代碼沒有任何入侵呢,是有的,通過分析線下的binlog來異步淘汰緩存:
業務線的代碼就不需要動了,新增一個線下的讀binlog的異步淘汰模塊,讀取到binlog中的數據,異步的淘汰緩存。
提問:爲什麼上文總是說1s,這個1s是怎麼來的?
回答:1s只是一個舉例,需要根據業務的數據量與併發量,觀察主從同步的時延來設定這個值。例如主從同步的時延爲200ms,這個異步淘汰cache設置爲258ms就是OK的。
在「異常時序」或者「讀從庫」導致髒數據入緩存時,可以用二次異步淘汰的「緩存雙淘汰」法來解決緩存與數據庫中數據不一致的問題,具體實施至少有三種方案:
(1)timer異步淘汰(本文沒有細講,本質就是起個線程專門異步二次淘汰緩存)
(2 4、總結
在「異常時序」或者「讀從庫」導致髒數據入緩存時,可以用二次異步淘汰的「緩存雙淘汰」法來解決緩存與數據庫中數據不一致的問題,具體實施至少有三種方案:
(1)timer異步淘汰(本文沒有細講,本質就是起個線程專門異步二次淘汰緩存)
(2)總線異步淘汰
(3)讀binlog異步淘汰
五、數據冗餘一致性
1、需求緣起
互聯網很多業務場景的數據量很大,此時數據庫架構要進行水平切分,水平切分會有一個patition key,通過patition key的查詢能夠直接定位到庫,但是非patition key 五、數據冗餘一致性