博客 / 詳情

返回

得物多活架構設計之路由服務設計

一 、背景

        隨着公司的業務發展,每次穩定性故障帶來的影響越來越大,提供穩定的服務,保證系統的高可用已經變成了整個技術部面對的問題。基於這種背景,公司開展了多雲/多活的技術項目,本人有幸參與了 “次日達” 項目【1】的異地雙活改造方案的設計。想以此來淺談一下我對多活乃至全球化的一些技術方案的認知。

        多活架構系列的文章我會按照總體技術方案、雙活/全球區域化部署技術、網絡調度技術、性能優化以及SRE五大部分來展開。本篇毒家Blog會着重討論總體技術方案以及雙活/全球區域化部署技術中的路由服務設計模塊,並會在後續的毒家Blog中逐步完善多活架構的完整技術方案。

二、多活/全球化的技術要求

        網絡服務除了要滿足用户對性能、可用性等的基礎要求外,多活/全球化的背景下還增加了合規、數據隔離性等要求。而這方方面面的要求都遇到了全新的挑戰。

1.  性能

        用户從發起請求到接收、響應的時效越短,代表性能越好。但是在雙活/全球化的背景下,用户可能在日本,機房可能在中國,物理的距離變得更長了,對應服務的響應時效也會成正比。有測試數據顯示,跨國家或者大範圍的跨機房調用,網絡的RTT會增加1s左右,而這1s可能會造成交易達成的“轉化率”下降,甚至用户流失。

2.  可用性

        多活\全球化的業務會跨越時區,這就要求我們的服務要7✖️24小時可用。這不僅是對系統的挑戰,也是對人力的挑戰。

3.  互聯互通

        互通互聯指的是電信網絡之間的物理連接。為了使一個電信運營商企業的用户可以和另一個電信運營商企業的用户進行相互通訊,在國內,這種跨越運營商的網絡通訊已經不是什麼大的問題。但是在國外,很多國家的網絡互通互聯的質量仍然不夠理想。

4.  數據一致性

        當數據被全球用户共享時,多地用户都可進行讀寫操作,如何確保數據的一致性?

5.  隱私保護

        全球化的業務必須遵守GDRP(General Data Protection Regulation,通用數據保護條例)。

6.  可伸縮性

        維基百科的解釋:當系統、網絡或者進程在任務量增減時,有能力進行應對。

三、多活/全球化的總體架構

3.1  領域建模

        系統的核心就是處理領域模型的關係,從領域模型出發讓整個系統滿足現狀和未來的需求。同時讓項目團隊更好地協作,以下是系統的核心對象。

User:網站平台上的用户。

UserGroup:用户組、具有相同特徵的用户一般會採用同樣的網絡鏈路和機房調度策略,因此會被歸屬到同一個用户組之中。實際上,多活系統是以用户組為基礎調度單位的。用户是用户組的一員,我們既可以以用户組為單位調度,也可以針對某個具體用户調度。

EdgeNode:邊緣節點。可以理解為靜態資源的提供節點。比如圖片、js文件、css等。一般的邊緣節點指的是CDN。

NetLink:網絡鏈路。

NetNode:網絡節點。

  • PoP:網絡服務接入點。(路由器、交換機等節點)
  • DSA:動態站點加速,是訪問由CDN廠商提供動態內容的加速器。

IDC:機房。

DNS:DNS服務器。

HTTP-DNS:理解成APP端的DNS服務器。

DomainName:域名。

VIP:虛擬「偶像」。

以上領域模型的關係如下;

圖片

3.2  總體架構

        按照以上的描述,其實是可以看到,從用户出發一直到機房,會存在邊緣節點調度、網絡調度以及機房調度。還有調度的執行(路由的使用)以及調度的控制(路由的產生)。路由即每個用户或者每個用户組隸屬於那個機房。 結構圖如下;

圖片

  • 全球用户:我們的系統會把全球用户分成不同的用户組,並且按照區域分為四組,即A洲、B洲、C洲和D洲。在調度執行時,常規的流程是把調度信息推送到每個用户組的 App 上。
  • Edge 調度:靜態信息的調度,決定了每個用户組應該使用哪個邊緣節點。
  • 網絡調度:基於大數據實時統計每一條可行的鏈路並且由決策模型確定走哪一條路線。(跟路由沒關係)
  • 機房:提供解決方案的源頭。
  • 調度執行:PC 使用 DNS 技術進行調度,App 使用 HTTP-DNS 以及 PUSH 技術調度。(路由的使用)
  • 調度控制:通過實時數據計算,確定具體的調度。(路由的產生以及配置)

四、多活/全球化區域化部署技術

4.1  整體架構

4.1.1  功能優先級

        調度的編排具體策略由業務需求決定,通常來講就是會考慮合規、數據一致性、可伸縮性、成本、容量、性能和穩定性來考慮。通常重要順序是

合規 > 數據一致性 > 可伸縮性 > 其他

4.1.2  部署架構

        目前我們有四個機房(A洲、B洲、C洲、D洲),建設每個機房所在的地域服務對應的地域用户。機房之間的數據需要按需複製,每個機房都部署所有的應用以及數據庫,使得每個機房都是對等的。當備份好數據之後,就可以讓所有的機房互為災備機房。數據一致性和可伸縮性會在後續介紹。

圖片

4.1.3  問題分析

        以電商場景中的買賣、就近訪問以及異地容災為例。比如買家和賣家分別來自不同的地區,進行交易必定會有部分共享的數據一致性問題;當異地容災時,也會面臨數據一致性問題;當用户遷移到其他區域時,仍然要保證就近訪問,那麼又涉及到同一個用户的數據一致性問題。

我們應對的策略如下:

  • 就近訪問:用户會路由到固定的機房(正常情況下),並且確保用户的數據都是在同一個機房閉環。
  • 異地容災:由於應用是對等的,那麼就要確定數據在備份機房裏存在。

<!---->

  • 全球買、賣:將數據按需同步,將商品信息同步到全部機房。
  • 數據一致性:確保單一數據master原則,即同一條數據只有一個機房會進行變更。會確保業務的優先級(買家>賣家>運營)。

4.1.4  解決方案

從應用程序分層的視角來看,解決方案如圖;

圖片

圖片

4.2  路由服務

        區域化部署技術本質就是多層路由,而在每一層路由中,都是基於用户對應的歸屬機房調用的路由的。路由服務的作用就是告訴調用方,這個用户歸屬於哪個用户。

圖片

  1. 路由服務結構
-  內存路由表:理解為 HashMap,key 為用户 id,value 為用户歸屬機房以及用户狀態。
-  RPC 服務。
  1. 路由表如何使用
- 用户請求進入機房第一個應用程序是同一接入層。使用 Nginx 作為統一接入的應用程序,Nginx 內嵌路由表,並且在多進程進行共享。Nginx 接受請求後做的第一件事情就是獲取用户 id,然後調用路由表取得用户歸屬機房以及用户狀態。若用户歸屬於本機房則繼續向下透傳。


-  下游需要路由信息直接獲取上層丟下來的路由信息。如下圖;



路由透傳存在時效限制,當超出一定時效,透傳內容會失效。至於在透傳過程中,用户路由改變怎麼辦?本文後續解答。

圖片

4.2.1  路由表原理

路由表設計規範必須瞭解以下幾點:

  • 必須保存在內存。
  • 保證性能和吞吐量。

<!---->

  • 不能依賴第三方系統。
  • 路由設計應該支持自由升級。
4.2.1.1  方案比較

        方案比較包括以下的引入分佈式緩存、HashMap、布隆過濾器等,以下的方案各有缺點,具體如下。

4.2.1.1.1引入分佈式緩存
  • 缺陷

<!---->

    • 所有的系統都要調用遠程緩存,依賴性強。
    • 用户歸屬變更,客户端緩存要更新,遠程的緩存也要更新。
    • 各方系統都要加入一個強依賴。
4.2.1.1.2  HashMap
  • 缺陷

<!---->

    • 保存5000萬條大約需要2GB內存。
4.2.1.1.3  布隆過濾器
  • 缺陷

<!---->

    • 存在False Positive。

這樣看來沒有一個現存的方案,需要根據場景來 定製 化路由表。

4.2.1.2  路由表設計

        基於上述啓發,選擇使用比特數組進行存儲路由信息。我們可以用4個 bit 來表達一個用户。如圖所示;

圖片

這樣存儲的話存儲一億的數據只需要47M左右的內存空間。

但是如果用户ID的分佈是分段的呢:

        0~ 80000000

        100000000~ 300000000

        700000000~ 800000000

        2000000000~ 2000100000

        儘管真實的用户數量也只有1億左右,但是id分佈如此廣泛,這樣大約要消耗900多M的內存。

        

基於這點,要引入分段模式:

  • 分段模式

分段模式如圖所示,核心思路就是建立一個段索引表,每個索引表上指定了一段比特序列,用來保存用户信息。(例如我們以 100 萬用户為一段)。針對這些索引項中一個用户都沒有的,我們執行一個 NULL 段。其對應的比特序列也不會分配存儲空間。這樣大大的節省了內存空間。這樣還是同樣的用户登記才需要消耗 58 M左右的存儲空間。

圖片

4.2.1.3  路由表相關設計

        上階段解決了路由表的基礎存儲方案,但是有一些場景還是需要我們持續設計改進的。現在我們思考兩個問題:

  • 當某個機房出現故障需要容災切換時,如果基於現有的路由表實現方案,則需要變更對應機房所有用户的路由歸屬信息,可能涉及到幾千萬或者是上億的用户變更,成本非常高。
  • 在雙十一的場景內,雖然可以通過大數據規劃用户的行為分佈,但是雙十一一年才一次,學習樣本少,很容易就會出現用户行為和預期不一致的現象,那就有可能會造成 A 洲機房的容量不足,但是美國的機房容量卻很空餘。此時就需要部分的 A 洲用户分流到美國機房,如果通過現在的路由表怎麼支持呢?基於以上場景我們提出了一個叫做“邏輯機房”的概念。

<!---->

      • 當一切正常時,邏輯機房直接映射到一個原機房。
      • 當發生容災切換時,直接將邏輯機房映射到災備機房。
      • 當需要對部分用户進行分流時,按照用户 ID 進行 Hash 取模,將 Hash 結果不同的用户映射到不同的物理機房內。

圖片

具體的配置邏輯可以基於各個公司使用的配置系統來進行集中配置。

4.2.2  路由表更新機制

路由表更新機制的確立需要有以下設計約束;

  • 數據一致性:在路由表變更的過程中,會出現一個用户的歸屬信息在不同的機房或者機器節點不一致的可能性。
  • 可恢復、可回滾:無論系統處於什麼狀態,都可以確定性的恢復到一個期望狀態。
  • 快速變更:在一致性的保障過程,或者恢復、回滾的過程中,都會影響用户體驗,甚至無法使用系統。所以在變更過程中需要在極短的時間內完成。
4.2.2.1  數據一致性思路

        很多時候分佈式系統都在解決一個問題,那就是如何讓任何一條記錄修改在所有機房或者多機房上同時生效。解決思路並不複雜,並且有通用性。雖然我們無法保證變更在所有機房或者多機房同時生效,但我們可以知道變更在多機房中是否已經生效,在此基礎上我們設置一箇中間狀態,這個狀態與變更前的狀態和變更後的狀態都兼容,就解決了這個問題。

        如圖所示,狀態A是變更之前的狀態,狀態C是變更後的目標狀態,狀態A與狀態C是不能同時出現的,但是狀態A變更為狀態B,在等待直到所有機房的所有相關機器全部都變更為狀態B,那麼再從B到狀態C,這樣就不會出現狀態A和狀態C同時出現的情況。

圖片

        為了解決路由更新過程中業務數據全局一致性問題,我們引入了一個“禁寫”過渡版本。在切換到目標路由機房之前,我們先將路由置為當前機房的“禁寫”過渡版本,在這個狀態下,用户不能繼續在當前機房以及其他任何機房中進行任何會修改相關業務數據的動作。在“禁寫”過渡版本變更到新版本之前,必須確保所有路由解析的本地版本已經升級到“禁寫”過渡版本。“禁寫”過渡版本將新舊路由版本的生效時間嚴格的隔離開來,不存在某個時刻新舊版本的路由都生效的情況,從而確保了業務數據的全局一致性

圖片

(註釋:處在“禁寫”過渡版本中的用户在“禁寫”過程中的其他業務的可用性會受到一定程度的影響。這種影響應當被業務所接受,將其理解為一種業務可用性的局部臨時降級。這種降級會安排在用户不活躍的時段,往往不會對用户的體驗造成太大的影響。迴歸到路由表的視線中,用户Id是不需要被存儲的,歸屬機房對應用户bit的前三位,可寫標誌對應的第4位,為0時表示用户可寫,為1時表示用户禁寫。)

4.2.2.2  解決方案
4.2.2.2.1  數據準備與生效過程分離

        “禁寫”狀態會對用户產生影響,如果用户被禁寫,則意味着用户無法下單,雖然在用户不活躍時段進行變更可以降低對用户產生影響的概率,但是變更可以在此基礎上進一步降低這個概率。我們可以採用數據準備與生效過程分離的方式實現。

  • 數據準備過程就是將用户歸屬的信息寫入分佈式持久數據庫中。
  • 由於要求快速回滾,因為必須是多版本的寫入。這就要求我們持久層數據庫的數據是多版本的。準備好數據後,對版本的生效過程採用Zookeeper的watch機制進行交互,過程如下:

<!---->

    • 當需要進行路由變更時,會由路由變更控制程序將數據寫入數據庫中,並且定義版本號。
    • 數據準備好之後,將版本號寫入Zookeeper的監聽節點中,所有watch都會受到推送。
    • 需要加載路由表的機器讀取數據庫內的數據,並進行新版的路由表加載。

圖片

4.2. 2.2.2  一致性具體方案

        Zookeeper 是一種高性能的分佈式協調工具,用於節點之間的通訊,常被使用分佈式的配置管理中,各個廠商在路由表的數據一致性的建設中,大部分使用的也是這種解決方案。

        在分佈式協調場景中,常常會用到短暫節點,這個節點與創建他的session同在,當session消失,節點也會消失。這個機制常用於做心跳檢查。而在路由節點的建設中,所有需要監聽路由表的節點都會創建一個短暫節點,用於路由表加載節點的心跳檢查。單次變更的流程如下:

  • 所有的節點都會與Zk的currentVersion節點建立watcher,用於獲取最新版本的推送。
  • 所有的節點都會創建一個短暫節點,以機器名稱命名,表示此節點正在監聽變更,建立在SessionList目錄下;當session消失時,表示這個節點不會在監聽變更。
  • 當節點被推送有新版本的變更後,它會使用這一個版本號去分佈式數據庫內查詢數據(4.2.2.2.1之前已經準備好數據了)
  • 當獲取完成,並在本地內存中初始化路由表,會將機器名字作為節點寫入AckList目錄下的currentVersion子目錄,表示此節點已經對於當前版本更新完成。
  • 變更程序會比較AckList目錄下的currentVersion子目錄中的所有機器節點是否覆蓋了SessionList目錄下的所有機器節點,如果是則證明所有節點更新到最新版本。
  • 因為我們知道所有節點是否已經更新完成,並且有與前後兼容的“禁寫”狀態,所以可以在所有節點都更新到“禁寫”狀態後,再進行新版本的路由信息變更,這樣就可以確保出現的狀態都是相互兼容的,從而保證了數據一致性問題。

上述步驟説明的ZK的目錄節點結構如下:

圖片

4.2.2.3  整體架構

        前面對關鍵的技術細節進行了介紹,下面介紹整體的架構。前面介紹過,管控系統會負責所有機房的區域化管理,包含路由表的變更流程。在每個機房中都會有一個管控的Agent,管控系統會調用Agent對所有的機房進行管理。在路由變更過程中,對每個Agent會把機房中的路由數據寫入對應的分佈式數據庫中,Zk再推送信息寫入,這裏不再贅述。

圖片

4.2.2.3  變更流程

當路由表變更時,完整的流程變更如下:

  1. 保存當前版本號V1,用於處理回滾的方案。
  2. 獲取當前機房列表,得到所有機房,循環調用每個機房的Agent,依次向下執行。

<!---->

  1. 每個機房的Agent調用我們上述説的解決方案,將數據寫入分佈式數據庫內。
  2. 如果失敗,則直接調用第8步。

<!---->

  1. 獲取當前機房列表,得到所有機房,並且循環調用每個機房的Agent,將用户狀態修改。
  2. 之後利用一致性的具體方案(4.2.2.2.2),並將所有用户狀態改成最終狀態。
  3. 如果失敗,則直接調用第8步。如果成功,則流程結束。
  4. 循環調用每個機房的Agent,將版本回退。如果失敗,則進行人工干預。

<!---->

4.2.3  用户路由更新方案

        前面介紹過了路由表的更新機制,但是如何確定用户歸屬的機房?如何變更用户歸屬機房?如何將網站的存量用户加入到路由表中?以及有了新用户如何加入路由表中?

4.2.3.1  確定 用户歸屬機房

        在真實應用場景中,絕大部分用户的歸屬邏輯採用性能優先原則,基本等同於用户歸屬於訪問延遲最小的機房。當然對於大部分場景下,延遲最小的機房就是物理距離最近的機房。

我們如何來判斷用户的歸屬機房,方案如下:

  1. 每個用户都會在所有機房進行異步訪問,用於確認用户和所有機房的延遲。
  2. 以用户區域為粒度進行統計,最穩定的機房是哪一個。

<!---->

  1. 在路由表中將區域中每個用户與這個區域整體表現最好的機房做關聯。

圖片

4.2.3.2  變更用户歸屬機房

        確定了用户歸屬機房之後,假設新的歸屬機房與原機房不同,那麼就要落實一個用户到機房的歸屬。前面介紹過,在用户路由歸屬過程中,需要將表改寫成向前向後都兼容的“禁寫”狀態,這個過程確保了路由表本身變更不會帶來數據不一致的情況。但是從“禁寫”用户到用户“可寫”的過渡中,還需要將用户的數據在原機房複製到目標機房,並且確保複製完成。相關數據複製的技術這裏不展開討論,會在後續章節講述。

4.2.3.3變更優化-分時變更

        由於禁寫可能會對用户產生影響,因此我們需要在變更的時間上進行優化,降低對用户生產影響的概率。主要的方法就是找到用户最可能閒時。

(1)以小時為單位,並且賦予時間段標識id。

###### 時段標識id ###### 時段
0 0-1
1 1-2
2 2-3
3 3-4
4 4-5
…… ……
23 23-24

(2)為用户的不同行為設置權重,權重代表禁寫對用户影響的大小。\

###### 事件 ###### 權重
browsing 0.2
ordering 0.8

(3)建設用户 abc 在一段時間內操作記錄如下,則採用下面的計算方法計算每個事件的衝突值。

用户id 事件 活躍id=0個數 活躍id=1個數 …… 活躍id=23個數 總個數
abc browsing 1 2 1 4
abc ordering 4 2 2 8

        P(0)=1/(1+2+1)0.2+4/(4+2+2)0.8=0.45 代表標識id為0的時間段衝突值為0.45

        P(1)=2/(1+2+1)0.2+4/(4+2+2)0.8=0.3 代表標識id為1的時間段衝突值為0.3

        P(2)=1/(1+2+1)0.2+4/(4+2+2)0.8=0.25 代表標識id為0的時間段衝突值為0.25

值越大,代表此時間段的避開價值就越大。


4.2.3.4  存量更新方案

存量更新方案是指兩種場景

  • 方案剛上線
  • 機器剛啓動

        這兩種場景一般都是指重新計算所有目前系統中存在的用户的歸屬機房。基於前面介紹的知識,目前採用的方案就是之前我們提到的確定用户歸屬機房(4.2.3.1)方案。

        這裏特殊提一下歸屬的默認優化,我們將某一個機房作為默認機房,所有歸屬到此機房的用户無需加入路由表,當調用路由服務查詢此用户路由時,路由表返回空值,路由服務直接返回默認機房,從而大大降低路由表的大小。

4.2.3.5  全量更新方案

增量更新方案一般也指的是兩個場景

  • 用户註冊
  • 用户遷移

        對於第一種情況,新機房的用户都會歸屬到默認機房,不進行任何路由表的變更,之後的過程與第二種情況相同。

        對於第二種情況,在對新用户進行多機房探測過程,發現用户可能不應該屬於本機房,或者發現新註冊用户確實訪問默認機房不是最快的。那麼就需要做用户遷移,即進行增量更新。在確認歸屬之後,增量更新與存量更新方案一致,相比之下,增量更新方案需要變更的用户比較少。存量更新方案需要運行的次數並不多。

五、小結

        這一篇文章主要介紹了在異地多活/全球化改造過程中的基本概念以及領域建模還有路由系統的存儲優化過程。後續還會持續更新異地多活/全球化的更多內容,歡迎關注「得物技術」公眾號。

註釋:

【1】次日達(Leadtime、LT)是一款得物推出的履約承諾產品,核心邏輯是通過發貨園區、收貨城市、商品屬性匹配後台配置的線路,以此給用户承諾商品是否支持商品次日送達。

文|FUGUOFENG

關注得物技術,做最潮技術人!

user avatar zebin_5fb6ddb261421 頭像 kevinwan 頭像 years 頭像
3 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.