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

得物技術發表於2022-06-29

一 、背景

        隨著公司的業務發展,每次穩定性故障帶來的影響越來越大,提供穩定的服務,保證系統的高可用已經變成了整個技術部面對的問題。基於這種背景,公司開展了多雲/多活的技術專案,本人有幸參與了 “次日達” 專案【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###### 時段
00-1
11-2
22-3
33-4
44-5
…………
2323-24

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

###### 事件###### 權重
browsing0.2
ordering0.8

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

使用者id事件活躍id=0個數活躍id=1個數……活躍id=23個數總個數
abcbrowsing12 14
abcordering42 28

        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

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

相關文章