閒魚異地多活架構設計與實現
背景
首頁和搜索一直以來都是閒魚導購的主陣地,爲了保證高可用業務上做了很多保護方案。但是隨着原有地域的 IDC 日漸趨於飽和,一些更深層次的問題開始暴露出來:
a)架構不具備擴展性。當服務量增大,單個 IDC 由於服務器部署、電力等物理因素無法滿足訴求,不能簡單的通過 IDC 部署來應對新增的流量。以算法爲例,算法在現有 IDC 資源飽和的情況下,其上新模型之前不得不等老模型下線,嚴重製約業務的迭代效率。
b)容災問題。當現有 IDC 出現故障時,如何保證導購主鏈路依然高可用。
常用高可用架構
• 同城雙活:系統從接入層以下在同城兩個機房做部署,這樣可以應對斷電,斷網等單機房故障。由於同城機房距離足夠近,可以近似看成一個機房,因此在部署上和單機房相比沒有特殊要求。但是遇到同區域災害時,服務就會受到影響,而且擴展性較差。
• 異地災備:系統從接入層以下除了在同城兩個機房做部署之外,在其他區域部署異地備份,底層數據根據實際要求做熱備 / 冷備,但是不承擔任何流量。當區域掛掉時,服務切至備份區域保證服務可用性。但異地災備的問題是:a)另一個區域不跑流量,出了問題不敢切。b)備份全站,資源利用率低。c)存在跨地域訪問。
• 異地多活:異地多活從接入層開始做多區域多機房部署,各個區域之間沒有主備的概念,均承擔相應的流量。它的優勢在於資源利用率高,擴展性較好。
前面提到閒魚除了要解決容災問題之外還需要解決算法同學的資源利用問題,因此異地多活很自然成爲我們的不二選擇。
多地部署帶來的變化
當我們的系統從單地部署升級成多地域部署時,它並不是簡單將整個系統搬到各個地域去做部署。當你的系統部署過去之後,需要考慮非常多的因素,比如
還有很多其他因素,這裏不一一列舉。可以預想到當我們的系統升級成多地域部署架構之後,給整個系統帶來了很大的變化,同時也帶來了相當大的挑戰。
多地域部署方案
面臨的挑戰
異地部署最大的特點是網絡時延較高:一般來說同地域延時 2~3ms,同機房延時小於 1ms,而跨地域延時一般大於 20ms。所以我們首先要解決的問題便是如何降低跨地域對導購鏈路的影響,這也是我們做異地容災的一個大原則,這面臨着幾個難點
-
流量如何做到地域內閉環,流量閉環必然有代價,如何平衡這兩者之間的關係。
-
多地域部署帶來的系統架構的複雜度。無論是流量調控,服務路由還是數據讀寫同步,都要在龐大的系統中做好精細化的調控,對系統帶來的複雜度可想而知。
-
如何做流量路由。如何識別流量的來源,控制流量的去向。
-
系統部署規範。系統在一直不停的演進,如何避免架構快速腐化。
-
流量如何做管控。流量調控規則需要統一做監控,管控等,一旦需要切流時,流量如何快速完成調整收斂。
用戶數據是否做拆分
在介紹部署架構之前先講一下閒魚導購鏈路的一個大前提,後面的很多方案都是基於這個大前提之下做抉擇的,那就是存儲層的數據是否要做拆分。
異地部署之後數據會存在多個區域中,區域之間的數據同步存在一定的延時,因此數據是否要做拆分取決於對數據一致性的要求。如果可以容忍對數據短時間不一致那麼則不需要做數據拆分。但是在電商某些場景下,比如買家加入購物車操作,如果數據寫在區域 A,購物車列表讀的卻是區域 B,那麼很有可能就會導致買家看不到剛加入購物車的商品,這是非常糟糕的體驗。因此在這種場景下就需要保證數據的讀寫都在相同的維度,這種情況下就需要對數據做拆分。
但是前面提到閒魚導購鏈路特點是:a)可以容忍短時間的數據不一致。b)不涉及到數據庫的寫操作。顯而易見數據拆分顯得並沒有那麼必要,因此我們決定不做數據拆分。
整體部署方案中,物理上各個區域是對等的,不存在主備的概念,但是邏輯上還是區分出了中心區域和其他可用區域,這是因爲:a)總有部分長尾依賴沒法做多區域部署。b)並不是所有場景 (非核心鏈路) 都適合做多地域部署。我們把這些長尾依賴統一放在中心區域,做兜底部署。
流量路由方案
流量路由方案這裏麪包含了兩個問題:
-
流量分發的原則是什麼,解決哪些流量應該到哪個地域。流量分發原則常見的有三種方式:a)完全隨機。b)按照地域就近訪問。c)按照用戶維度切分。
-
流量在哪一層做分發,解決用戶請求從哪裏開始分流。
如前面數據拆分部分提到,流量分發理論上需要和數據拆分邏輯保持一致。由於閒魚底層沒有做數據拆分,因此流量分發原則相對較爲靈活。
-
最簡單的流量分發原則是完全隨機。前面提到數據在多區域之間存在數據同步延時,雖然導購鏈路可以容忍短時間的數據延時,但是我們需要避免用戶連續兩次請求看到的數據存在不一致 (如果倆次請求分別落在不同地域)。
-
按照地域就近訪問能實現最低的訪問延時,但是這種方案最大的問題是地域之間的流量嚴重不均衡,而且在不停變化 (正常時段 & 節假日),這會給整個系統的負載均衡帶來很高的複雜度。
-
按照用戶進行切分,保證部分用戶請求會固定路由到某個區域。
在我們導購場景下 1 和 2 都不適合,因爲都有可能導致用戶兩次請求看到的數據不一致,最終我們選擇按照用戶進行切分,這也是公司內部成熟的路由方案。確定了流量分發原則,接下來需要決定流量在哪一層做分發。這點我們考慮了三個可選的方案
- 方案一,在域名解析階段做不同地域的流量分發。a)這種方式成本較高,需要有獨立的 DNS 域名,獨立的路由規則。b)當路由規則調整的時候,收斂週期較長 (依賴端側的緩存更新)。c)和運行環境綁定,不支持 H5,Web 等場景。其優點是經過公司內部驗證,相對比較成熟。
- 方案二,在統一接入層進行不同地域流量分發。這種方式成本低,可以複用現有的邏輯,只需要在統一接入層做規則配置,但是部分流量存在跨地域訪問 (從接入層到業務集羣)。
- 方案三,搭建邊緣網關。通過原生的 DNS 做域名解析,然後就近選擇邊緣網關訪問。切流等複雜邏輯放在邊緣網關中完成。這個方案和運行時無關,支持 app & 小程序 & Web 等,而且規則調整收斂速度快,擴展能力強。其缺點是需要從 0 開始搭基建。
方案一的跨地域問題是我們需要避免的,方案三雖然比較適合,但是成本太高,而且沒有成熟的經驗借鑑,權衡之下我們最終決定採用方案二。
全鏈路升級改造
-
應用代碼改造。導購鏈路所有的依賴是否都能做多地部署,如果沒法多地部署跨地域時延是否會被放大。
-
服務之間的流量路由策略。導購鏈路涉及到很多異構的子系統,這些異構系統之間的流量是否遵循同地域優先,當某個地域服務掛了之後流量是否允許自動切到其餘地域。
-
流量強糾偏。導購的請求鏈路較爲複雜,會依賴衆多異構的子系統。雖然域名解析時流量會路由至對應的區域,但是在後續鏈路仍然有可能發生流量竄到其餘地域的情況,這種情況下理論上會對用戶體驗造成影響,所以在導購鏈路的每一跳節點都應該有糾偏策略。
-
外部流量由於分發策略我們沒法管控,會導致預期之外的流量流入。爲了避免這種情況,我們也需要有一個流量糾偏的策略。
-
對於那些沒法做多地部署的依賴,評估其對數據一致性的約束,如果是弱一致性,則考慮使用富客戶端模式,在富客戶端模式中優先讀緩存,不命中再走一次 RPC,通過緩存降低跨地域請求的頻率。
-
沒法做多地部署且要求數據強一致性的依賴,需要避免跨地域訪問時延被放大。不存在跨地域延時的時候串行並行的區別並不明顯,但是引入跨地域時延之後串行和並行的區別就會非常明顯,因此對這部分依賴需要做併發改造。另一方面在改造過程中梳理出核心依賴 & 非核心依賴,核心依賴強制要求單元化,對於非核心依賴做到併發 & 可觀測 & 可降級。
-
緩存改造。由於以前對緩存的使用不夠嚴謹,會導致單地域部署下被掩蓋的問題在多地域部署之下暴露出來。比如下面這種場景,在某個場景寫入某個 key,然後在另一個場景下讀取這個 key。在單地域部署下不會有問題,但是一旦多地域部署之後就有可能出現讀寫不同地域的情況導致數據不一致。
-
如果是非持久化緩存,則不用做任何改造。因爲這種場景緩存不命中會有數據加載過程。但是很多非持久化緩存場景濫用了持久化緩存,針對這類 case 需要規範使用,改造成非持久化緩存。
-
如果是持久化緩存,分爲兩種情況:a)強一致性,如分佈式鎖,這種情況強制讀寫中心主集羣。b)非強一致性,則強制寫中心主節點,就近讀。
導購鏈路涉及到很多異構系統,包括各個子領域應用構成的微服務集羣,以及衆多搜索 & 推薦服務。異構主要體現在:a)編寫語言以及部署 & 運維平臺的差異。b)服務註冊發現機制不一樣,主要包括 configserver/vipserver/zookeeper。因此主要改造內容在於規範對這些組件的使用,調整流量路由策略保證流量區域內自閉環。
爲了防止外部流量對閒魚導購流量的影響,我們在統一接入層加了一條流量糾偏策略:對於外部非導購鏈路的流量,強制切回中心區域。這一點非常重要,因爲對於部署範圍之外的服務,如果因爲這個原因導致流量到了其他可用區域,其返回數據的正確性我們沒法做保證。
服務集羣部署方案
微服務集羣整體採用對等部署。微服務集羣按照服務發現 & 註冊機制的不同劃分成三類:
-
採用 HSF 作爲 RPC 框架的業務服務,採用 configserver 做服務發現,configserver 同時在多地域部署,彼此之間互相隔離,各地域部署的服務只拉取本地域內的 configserver 數據,通過這種方式實現地域之間的流量隔離。但是中心區域的數據會同步至其他區域 (區域掛了流量可以路由到中心區域,保證服務可用)。
-
採用 HTTP 調用的算法服務集羣由於歷史 & 異構原因,採用了兩種服務註冊 & 發現方式
-
Zookeeper。Zookeeper 在中心和區域都做單獨部署,客戶端請求的時候按照地域拉取對應的 Zookeeper。通過這種方式實現流量的同機房訪問,地域彼此間數據隔離,當單個地域服務出現問題時,只能通過將其他區域的服務數據掛載到故障地域對應的 Zookeeper 下面來進行恢復。
-
vipserver(阿里自研的一套集羣路由軟件負載均衡系統)。由於 vipserver 本身是分佈式的負載均衡系統,且支持多種路由方式,故只部署一套。
導購鏈路使用緩存的地方很多,大致分成兩種用法
- 緩解持久層的訪問壓力。先訪問緩存,緩存如果沒有數據則請求持久化層並把數據加載至緩存中,緩存本身不做數據一致性保證。這種情況比較好處理,因爲不涉及到多區域之間的同步,只需要簡單做多地域部署即可。
-
用作數據持久化。典型的如分佈式鎖,計數器等。這種場景會有中心和區域的概念,彼此間雙向同步,這種場景在單區域部署的時候和上面的用法沒有太大區別,但是在多地域部署架構下,就會因爲雙寫導致數據出現不一致,因此需要保證同一個 key 同一時間不能在多區域同時寫。
-
區域同步至中心。因爲數據需要做持久化,所以會在中心有一份完整的數據集,區域保證數據的最終一致性即可。
-
中心同步至區域。保證區域的數據和中心的數據一致。
數據庫部署
按照分佈式系統的 CAP 定理:Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區容錯性),三者不可得兼。所以嚴格意義上來說,數據庫的異地部署只能三選二。但是在分佈式系統中必然是分區的,而且分區之間的網絡我們沒法控制,也就是說 P 是一個事實,我們只能從 C 和 A 中二選一,這分別對應着數據庫的兩種數據複製方式。
- 主從複製模式的 MySql:中心寫成功即返回,從節點依賴主從之間的數據同步。這種模式下保證了 A 和 P,犧牲了 C。
- 雙向複製模式的 MySql:沒有主從節點之分,節點與節點之間實現數據最終一致性。這種模式下同樣保證了 A 和 P,犧牲了 C。
- 採用 Paxos 協議的分佈式數據庫如 Google 的 Spanner 等,採用 Paxos 協議來保證數據的強一致性,但是在 Master 節點掛了之後在新的 Master 選舉出來之前不可用,即保證了 C 和 P,犧牲了 A。
一方面根據導購鏈路的特點 (絕大部分都是數據讀取操作,可以容忍短時間內的不一致)。另一方面原有的數據存儲採用 MySql,考慮到成本,最終選擇主從複製模式 MySql。
總結
異地部署給系統帶來的最大挑戰是物理距離帶來的網絡延時,整個系統設計都圍繞着這個展開。總的來說在解決跨地域延時過程中我們遵循兩個大的原則:a)流量地域內自閉環。b)堅持可用性優先。在這兩個大原則之下從接入層,服務層以及數據存儲層做了相應的改造 & 部署。
目前閒魚部分鏈路已經實現了兩地三機房部署,並且已經承接線上流量,具備了異地容災的能力。同時經過本次改造,導購鏈路具備了較好的擴展性,能夠以極低的成本快速部署至更多機房。
但是一方面由於導購鏈路大部分都是隻讀場景,對數據要求弱一致性即可。對於數據強一致性場景帶給系統的挑戰會更大。另一方面業務是一個不停演進的過程,如何保證在演進過程中仍然能保證異地多活的部署架構,這是急需解決的問題。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/dyvggvqDUpp-bdGjRtIUUQ