萬億級調用下的優雅——微信序列號生成器架構設計及演變(下)

版權聲明:本文由曾欽松原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/201算法

來源:騰雲閣 https://www.qcloud.com/community緩存

 

上一篇文章介紹了seqsvr的原型,這篇會簡單地介紹下seqsvr容災架構的演變。咱們知道,後臺系統絕大部分狀況下並無一種惟一的、完美的解決方案,一樣的需求在不一樣的環境背景下甚至有可能演化出兩種大相徑庭的架構。既然架構是多變的,那純粹講架構的意義並非特別大,這裏也會講下seqsvr容災設計時的一些思考和權衡,但願對你們有所幫助。微信

容災設計

接下來咱們會介紹seqsvr的容災架構。咱們知道,後臺系統絕大部分狀況下並無一種惟一的、完美的解決方案,一樣的需求在不一樣的環境背景下甚至有可能演化出兩種大相徑庭的架構。既然架構是多變的,那純粹講架構的意義並非特別大,期間也會講下seqsvr容災設計時的一些思考和權衡,但願對你們有所幫助。架構

seqsvr的容災模型在五年中進行過一次比較大的重構,提高了可用性、機器利用率等方面。其中無論是重構前仍是重構後的架構,seqsvr一直遵循着兩條架構設計原則:負載均衡

  1. 保持自身架構簡單框架

  2. 避免對外部模塊的強依賴運維

這兩點都是基於seqsvr可靠性考慮的,畢竟seqsvr是一個與整個微信服務端正常運行息息相關的模塊。按照咱們對這個世界的認識,系統的複雜度每每是跟可靠性成反比的,想獲得一個可靠的系統一個關鍵點就是要把它作簡單。相信你們身邊都有一些這樣的例子,設計方案裏有不少高大上、複雜的東西,同時也總能看到他們在默默地填一些高大上的坑。固然簡單的系統不意味着粗製濫造,咱們要作的是理出最核心的點,而後在知足這些核心點的基礎上,針對性地提出一個足夠簡單的解決方案。異步

那麼,seqsvr最核心的點是什麼呢?每一個uid的sequence申請要遞增不回退。這裏咱們發現,若是seqsvr知足這麼一個約束:任意時刻任意uid有且僅有一臺AllocSvr提供服務,就能夠比較容易地實現sequence遞增不回退的要求。

圖5. 兩臺AllocSvr服務同個uid形成sequence回退。Client讀取到的sequence序列爲10一、20一、102優化

但也因爲這個約束,多臺AllocSvr同時服務同一個號段的多主機模型在這裏就不適用了。咱們只能採用單點服務的模式,當某臺AllocSvr發生服務不可用時,將該機服務的uid段切換到其它機器來實現容災。這裏須要引入一個仲裁服務,探測AllocSvr的服務狀態,決定每一個uid段由哪臺AllocSvr加載。出於可靠性的考慮,仲裁模塊並不直接操做AllocSvr,而是將加載配置寫到StoreSvr持久化,而後AllocSvr按期訪問StoreSvr讀取最新的加載配置,決定本身的加載狀態。

圖6. 號段遷移示意。經過更新加載配置把0~2號段從AllocSvrA遷移到AllocSvrBui

同時,爲了不失聯AllocSvr提供錯誤的服務,返回髒數據,AllocSvr須要跟StoreSvr保持租約。這個租約機制由如下兩個條件組成:

  1. 租約失效:AllocSvr N秒內沒法從StoreSvr讀取加載配置時,AllocSvr中止服務

  2. 租約生效:AllocSvr讀取到新的加載配置後,當即卸載須要卸載的號段,須要加載的新號段等待N秒後提供服務

    圖7. 租約機制。AllocSvrB嚴格保證在AllocSvrA中止服務後提供服務

這兩個條件保證了切換時,新AllocSvr確定在舊AllocSvr下線後纔開始提供服務。但這種租約機制也會形成切換的號段存在小段時間的不可服務,不過因爲微信後臺邏輯層存在重試機制及異步重試隊列,小段時間的不可服務是用戶無感知的,並且出現租約失效、切換是小几率事件,總體上是能夠接受的。

到此講了AllocSvr容災切換的基本原理,接下來會介紹整個seqsvr架構容災架構的演變

容災1.0架構:主備容災

第一版本的seqsvr採用了主機+冷備機容災模式:全量的uid空間均勻分紅N個Section,連續的若干個Section組成了一個Set,每一個Set都有一主一備兩臺AllocSvr。正常狀況下只有主機提供服務;在主機出故障時,仲裁服務切換主備,原來的主機下線變成備機,原備機變成主機後加載uid號段提供服務。

圖8. 容災1.0架構:主備容災

可能看到前文的敘述,有些同窗已經想到這種容災架構。一主機一備機的模型設計簡單,而且具備不錯的可用性——畢竟主備兩臺機器同時不可用的機率極低,相信不少後臺系統也採用了相似的容災策略。

設計權衡

主備容災存在一些明顯的缺陷,好比備機閒置致使有一半的空閒機器;好比主備切換的時候,備機在瞬間要接受主機全部的請求,容易致使備機過載。既然一主一備容災存在這樣的問題,爲何一開始還要採用這種容災模型?事實上,架構的選擇每每跟當時的背景有關,seqsvr誕生於微信發展初期,也正是微信快速擴張的時候,選擇一主一備容災模型是出於如下的考慮:

  1. 架構簡單,能夠快速開發

  2. 機器數少,機器冗餘不是主要問題

  3. Client端更新AllocSvr的路由狀態很容易實現

前兩點好懂,人力、機器都不如時間寶貴。而第三點比較有意思,下面展開講下

微信後臺絕大部分模塊使用了一個自研的RPC框架,seqsvr也不例外。在這個RPC框架裏,調用端讀取本地機器的client配置文件,決定去哪臺服務端調用。這種模型對於無狀態的服務端,是很好用的,也很方便實現容災。咱們能夠在client配置文件裏面寫「對於號段x,能夠去SvrA、SvrB、SvrC三臺機器的任意一臺訪問」,實現三主機容災。

但在seqsvr裏,AllocSvr是預分配中間層,並非無狀態的。而前面咱們提到,AllocSvr加載哪些uid號段,是由保存在StoreSvr的加載配置決定的。那麼這時候就尷尬了,業務想要申請某個uid的sequence,Client端其實並不清楚具體去哪臺AllocSvr訪問,client配置文件只會跟它說「AllocSvrA、AllocSvrB…這堆機器的某一臺會有你想要的sequence」。換句話講,原來負責提供服務的AllocSvrA故障,仲裁服務決定由AllocSvrC來替代AllocSvrA提供服務,Client要如何獲知這個路由信息的變動?

這時候假如咱們的AllocSvr採用了主備容災模型的話,事情就變得簡單多了。咱們能夠在client配置文件裏寫:對於某個uid號段,要麼是AllocSvrA加載,要麼是AllocSvrB加載。Client端發起請求時,儘管Client端並不清楚AllocSvrA和AllocSvrB哪一臺真正加載了目標uid號段,可是Client端能夠先嚐試給其中任意一臺AllocSvr發請求,就算此次請求了錯誤的AllocSvr,那麼就知道另一臺是正確的AllocSvr,再發起一次請求便可。

也就是說,對於主備容災模型,最多也只會浪費一次的試探請求來肯定AllocSvr的服務狀態,額外消耗少,編碼也簡單。但是,若是Svr端採用了其它複雜的容災策略,那麼基於靜態配置的框架就很難去肯定Svr端的服務狀態:Svr發生狀態變動,Client端沒法肯定應該向哪臺Svr發起請求。這也是爲何一開始選擇了主備容災的緣由之一。

主備容災的缺陷

在咱們的實際運營中,容災1.0架構存在兩個重大的不足:

  1. 擴容、縮容很是麻煩

  2. 一個Set的主備機都過載,沒法使用其餘Set的機器進行容災

在主備容災中,Client和AllocSvr須要使用徹底一致的配置文件。變動這個配置文件的時候,因爲沒法實如今同一時間更新給全部的Client和AllocSvr,所以須要很是複雜的人工操做來保證變動的正確性(包括須要使用iptables來作請求轉發,具體的詳情這裏不作展開)。

對於第二個問題,常見的方法是用一致性Hash算法替代主備,一個Set有多臺機器,過載機器的請求被分攤到多臺機器,容災效果會更好。在seqsvr中使用相似一致性Hash的容災策略也是可行的,只要Client端與仲裁服務都使用徹底同樣的一致性Hash算法,這樣Client端能夠啓發式地去嘗試,直到找到正確的AllocSvr。

例如對於某個uid,仲裁服務會優先把它分配到AllocSvrA,若是AllocSvrA掛掉則分配到AllocSvrB,再不行分配到AllocSvrC。那麼Client在訪問AllocSvr時,按照AllocSvrA -> AllocSvrB -> AllocSvrC的順序去訪問,也能實現容災的目的。但這種方法仍然沒有克服前面主備容災面臨的配置文件變動的問題,運營起來也很麻煩。

容災2.0架構:嵌入式路由表容災

最後咱們另闢蹊徑,採用了一種不一樣的思路:既然Client端與AllocSvr存在路由狀態不一致的問題,那麼讓AllocSvr把當前的路由狀態傳遞給Client端,打破以前只能根據本地Client配置文件作路由決策的限制,從根本上解決這個問題。

因此在2.0架構中,咱們把AllocSvr的路由狀態嵌入到Client請求sequence的響應包中,在不帶來額外的資源消耗的狀況下,實現了Client端與AllocSvr之間的路由狀態一致。具體實現方案以下:

seqsvr全部模塊使用了統一的路由表,描述了uid號段到AllocSvr的全映射。這份路由表由仲裁服務根據AllocSvr的服務狀態生成,寫到StoreSvr中,由AllocSvr看成租約讀出,最後在業務返回包裏旁路給Client端。

圖9. 容災2.0架構:動態號段遷移容災

把路由表嵌入到請求響應包看似很簡單的架構變更,倒是整個seqsvr容災架構的技術奇點。利用它解決了路由狀態不一致的問題後,能夠實現一些之前不容易實現的特性。例如靈活的容災策略,讓全部機器都互爲備機,在機器故障時,把故障機上的號段均勻地遷移到其它可用的AllocSvr上;還能夠根據AllocSvr的負載狀況,進行負載均衡,有效緩解AllocSvr請求不均的問題,大幅提高機器使用率。

另外在運營上也獲得了大幅簡化。以前對機器進行運維操做有着繁雜的操做步驟,而新架構只須要更新路由便可輕鬆實現上線、下線、替換機器,不須要關心配置文件不一致的問題,避免了一些因爲人工誤操做引起的故障。

圖10. 機器故障號段遷移

路由同步優化

把路由表嵌入到取sequence的請求響應包中,那麼會引入一個相似「先有雞仍是先有蛋」的哲學命題:沒有路由表,怎麼知道去哪臺AllocSvr取路由表?另外,取sequence是一個超高頻的請求,如何避免嵌入路由錶帶來的帶寬消耗?

這裏經過在Client端內存緩存路由表以及路由版本號來解決,請求步驟以下:

  1. Client根據本地共享內存緩存的路由表,選擇對應的AllocSvr;若是路由表不存在,隨機選擇一臺AllocSvr

  2. 對選中的AllocSvr發起請求,請求帶上本地路由表的版本號

  3. AllocSvr收到請求,除了處理sequence邏輯外,判斷Client帶上版本號是否最新,若是是舊版則在響應包中附上最新的路由表

  4. Client收到響應包,除了處理sequence邏輯外,判斷響應包是否帶有新路由表。若是有,更新本地路由表,並決策是否返回第1步重試

基於以上的請求步驟,在本地路由表失效的時候,使用少許的重試即可以拉到正確的路由,正常提供服務。

總結

到此把seqsvr的架構設計和演變基本講完了,正是如此簡單優雅的模型,爲微信的其它模塊提供了一種簡單可靠的一致性解決方案,支撐着微信五年來的高速發展,相信在可預見的將來仍然會發揮着重要的做用。

相關文章
相關標籤/搜索