閒魚異地多活架構設計與實現

語言: CN / TW / HK

作者:閒魚技術——吳白

背景

首頁和搜尋一直以來都是閒魚導購的主陣地,為了保證高可用業務上做了很多保護方案。但是隨著原有地域的IDC日漸趨於飽和,一些更深層次的問題開始暴露出來:a)架構不具備擴充套件性。當服務量增大,單個IDC由於伺服器部署、電力等物理因素無法滿足訴求,不能簡單的通過IDC部署來應對新增的流量。以演算法為例,演算法在現有IDC資源飽和的情況下,其上新模型之前不得不等老模型下線,嚴重製約業務的迭代效率。b)容災問題。當現有IDC出現故障時,如何保證導購主鏈路依然高可用。​

常用高可用架構

•同城雙活:系統從接入層以下在同城兩個機房做部署,這樣可以應對斷電,斷網等單機房故障。由於同城機房距離足夠近,可以近似看成一個機房,因此在部署上和單機房相比沒有特殊要求。但是遇到同區域災害時,服務就會受到影響,而且擴充套件性較差。

image.png

•異地災備:系統從接入層以下除了在同城兩個機房做部署之外,在其他區域部署異地備份,底層資料根據實際要求做熱備/冷備,但是不承擔任何流量。當區域掛掉時,服務切至備份區域保證服務可用性。但異地災備的問題是:a)另一個區域不跑流量,出了問題不敢切。b)備份全站,資源利用率低。c)存在跨地域訪問。

image.png

•異地多活:異地多活從接入層開始做多區域多機房部署,各個區域之間沒有主備的概念,均承擔相應的流量。它的優勢在於資源利用率高,擴充套件性較好。

image.png

前面提到閒魚除了要解決容災問題之外還需要解決演算法同學的資源利用問題,因此異地多活很自然成為我們的不二選擇。

多地部署帶來的變化

當我們的系統從單地部署升級成多地域部署時,它並不是簡單將整個系統搬到各個地域去做部署。當你的系統部署過去之後,需要考慮非常多的因素,比如

•流量排程,系統部署過去後流量怎麼跟著怎麼過去。•流量自閉環。由於距離的原因,跨地域的物理延時是沒法避免的,流量過去之後怎麼保證所有的操作都在本地完成,如果做不到那怎麼將這種延時影響降到最低。•資料一致性,資料如何在多個地域之間保證一致性,針對一些如交易相關等資料時效性敏感的場景,如何解決多地域之間資料同步時延。•容災切流。當某個機房出現故障時,如何快速把流量無損地切至其他機房。這裡並不是說簡單把流量切過去就完事,由於資料在多區域同步,流量切過去之後能否保證資料的一致性?

還有很多其他因素,這裡不一一列舉。可以預想到當我們的系統升級成多地域部署架構之後,給整個系統帶來了很大的變化,同時也帶來了相當大的挑戰。​

多地域部署方案

面臨的挑戰

異地部署最大的特點是網路時延較高:一般來說同地域延時2~3ms,同機房延時小於1ms,而跨地域延時一般大於20ms。所以我們首先要解決的問題便是如何降低跨地域對導購鏈路的影響,這也是我們做異地容災的一個大原則,這面臨著幾個難點

image.png

1.流量如何做到地域內閉環,流量閉環必然有代價,如何平衡這兩者之間的關係。2.多地域部署帶來的系統架構的複雜度。無論是流量調控,服務路由還是資料讀寫同步,都要在龐大的系統中做好精細化的調控,對系統帶來的複雜度可想而知。3.如何做流量路由。如何識別流量的來源,控制流量的去向。4.系統部署規範。系統在一直不停的演進,如何避免架構快速腐化。5.流量如何做管控。流量調控規則需要統一做監控,管控等,一旦需要切流時,流量如何快速完成調整收斂。

使用者資料是否做拆分

在介紹部署架構之前先講一下閒魚導購鏈路的一個大前提,後面的很多方案都是基於這個大前提之下做抉擇的,那就是儲存層的資料是否要做拆分。​

異地部署之後資料會存在多個區域中,區域之間的資料同步存在一定的延時,因此資料是否要做拆分取決於對資料一致性的要求。如果可以容忍對資料短時間不一致那麼則不需要做資料拆分。但是在電商某些場景下,比如買家加入購物車操作,如果資料寫在區域A,購物車列表讀的卻是區域B,那麼很有可能就會導致買家看不到剛加入購物車的商品,這是非常糟糕的體驗。因此在這種場景下就需要保證資料的讀寫都在相同的維度,這種情況下就需要對資料做拆分。​

但是前面提到閒魚導購鏈路特點是:a)可以容忍短時間的資料不一致。b)不涉及到資料庫的寫操作。顯而易見資料拆分顯得並沒有那麼必要,因此我們決定不做資料拆分。

部署架構

整體部署方案中,物理上各個區域是對等的,不存在主備的概念,但是邏輯上還是區分出了中心區域和其他可用區域,這是因為:a)總有部分長尾依賴沒法做多區域部署。b)並不是所有場景(非核心鏈路)都適合做多地域部署。我們把這些長尾依賴統一放在中心區域,做兜底部署。

image.png

流量路由方案

流量路由方案這裡麵包含了兩個問題:

•流量分發的原則是什麼,解決哪些流量應該到哪個地域。流量分發原則常見的有三種方式:a)完全隨機。b)按照地域就近訪問。c)按照使用者維度切分。•流量在哪一層做分發,解決使用者請求從哪裡開始分流。

如前面資料拆分部分提到,流量分發理論上需要和資料拆分邏輯保持一致。由於閒魚底層沒有做資料拆分,因此流量分發原則相對較為靈活。

1.最簡單的流量分發原則是完全隨機。前面提到資料在多區域之間存在資料同步延時,雖然導購鏈路可以容忍短時間的資料延時,但是我們需要避免使用者連續兩次請求看到的資料存在不一致(如果倆次請求分別落在不同地域)。

1.按照地域就近訪問能實現最低的訪問延時,但是這種方案最大的問題是地域之間的流量嚴重不均衡,而且在不停變化(正常時段&節假日),這會給整個系統的負載均衡帶來很高的複雜度。

1.按照使用者進行切分,保證部分使用者請求會固定路由到某個區域。

在我們導購場景下1和2都不適合,因為都有可能導致使用者兩次請求看到的資料不一致,最終我們選擇按照使用者進行切分,這也是公司內部成熟的路由方案。確定了流量分發原則,接下來需要決定流量在哪一層做分發。這點我們考慮了三個可選的方案

•方案一,在域名解析階段做不同地域的流量分發。a)這種方式成本較高,需要有獨立的DNS域名,獨立的路由規則。b)當路由規則調整的時候,收斂週期較長(依賴端側的快取更新)。c)和執行環境繫結,不支援H5,Web等場景。其優點是經過公司內部驗證,相對比較成熟。

image.png

•方案二,在統一接入層進行不同地域流量分發。這種方式成本低,可以複用現有的邏輯,只需要在統一接入層做規則配置,但是部分流量存在跨地域訪問(從接入層到業務叢集)。

image.png

•方案三,搭建邊緣閘道器。通過原生的DNS做域名解析,然後就近選擇邊緣閘道器訪問。切流等複雜邏輯放在邊緣閘道器中完成。這個方案和執行時無關,支援app&小程式&Web等,而且規則調整收斂速度快,擴充套件能力強。其缺點是需要從0開始搭基建。

image.png

方案一的跨地域問題是我們需要避免的,方案三雖然比較適合,但是成本太高,而且沒有成熟的經驗借鑑,權衡之下我們最終決定採用方案二。

全鏈路升級改造

全鏈路改造的目的在於使我們的系統適應從單地部署到多地域部署的轉變,改造涉及到的點非常多,主要包括

1.應用程式碼改造。導購鏈路所有的依賴是否都能做多地部署,如果沒法多地部署跨地域時延是否會被放大。2.服務之間的流量路由策略。導購鏈路涉及到很多異構的子系統,這些異構系統之間的流量是否遵循同地域優先,當某個地域服務掛了之後流量是否允許自動切到其餘地域。3.流量強糾偏。導購的請求鏈路較為複雜,會依賴眾多異構的子系統。雖然域名解析時流量會路由至對應的區域,但是在後續鏈路仍然有可能發生流量竄到其餘地域的情況,這種情況下理論上會對使用者體驗造成影響,所以在導購鏈路的每一跳節點都應該有糾偏策略。4.外部流量由於分發策略我們沒法管控,會導致預期之外的流量流入。為了避免這種情況,我們也需要有一個流量糾偏的策略。

改造點3在資料強一致場景是必不可少的,但是對本次導購鏈路,由於改造成本和時間的關係,最終我們放棄了改造點3。因為改造點2保證了正常情況下流量路徑是符合預期的,只有異常情況才有可能發生流量竄到其他區域,但是這種情況我們認為:a)低頻且持續時間不長。b)短時間的不一致對業務影響可控。​

應用程式碼改造主要包括​

•對於那些沒法做多地部署的依賴,評估其對資料一致性的約束,如果是弱一致性,則考慮使用富客戶端模式,在富客戶端模式中優先讀快取,不命中再走一次RPC,通過快取降低跨地域請求的頻率。

•沒法做多地部署且要求資料強一致性的依賴,需要避免跨地域訪問時延被放大。不存在跨地域延時的時候序列並行的區別並不明顯,但是引入跨地域時延之後序列和並行的區別就會非常明顯,因此對這部分依賴需要做併發改造。另一方面在改造過程中梳理出核心依賴&非核心依賴,核心依賴強制要求單元化,對於非核心依賴做到併發&可觀測&可降級。

•快取改造。由於以前對快取的使用不夠嚴謹,會導致單地域部署下被掩蓋的問題在多地域部署之下暴露出來。比如下面這種場景,在某個場景寫入某個key,然後在另一個場景下讀取這個key。在單地域部署下不會有問題,但是一旦多地域部署之後就有可能出現讀寫不同地域的情況導致資料不一致。

image.png

這種情況下我們需要:a)強制寫中心主節點。b)開通主節點到其他地域的資料同步。總的來說快取改造兩大原則

•如果是非持久化快取,則不用做任何改造。因為這種場景快取不命中會有資料載入過程。但是很多非持久化快取場景濫用了持久化快取,針對這類case需要規範使用,改造成非持久化快取。•如果是持久化快取,分為兩種情況:a)強一致性,如分散式鎖,這種情況強制讀寫中心主叢集。b)非強一致性,則強制寫中心主節點,就近讀。

導購鏈路涉及到很多異構系統,包括各個子領域應用構成的微服務叢集,以及眾多搜尋&推薦服務。異構主要體現在:a)編寫語言以及部署&運維平臺的差異。b)服務註冊發現機制不一樣,主要包括configserver/vipserver/zookeeper。因此主要改造內容在於規範對這些元件的使用,調整流量路由策略保證流量區域內自閉環。​

為了防止外部流量對閒魚導購流量的影響,我們在統一接入層加了一條流量糾偏策略:對於外部非導購鏈路的流量,強制切回中心區域。這一點非常重要,因為對於部署範圍之外的服務,如果因為這個原因導致流量到了其他可用區域,其返回資料的正確性我們沒法做保證。

服務叢集部署方案

image.png

微服務叢集整體採用對等部署。微服務叢集按照服務發現&註冊機制的不同劃分成三類:

•採用HSF作為RPC框架的業務服務,採用configserver做服務發現,configserver同時在多地域部署,彼此之間互相隔離,各地域部署的服務只拉取本地域內的configserver資料,通過這種方式實現地域之間的流量隔離。但是中心區域的資料會同步至其他區域(區域掛了流量可以路由到中心區域,保證服務可用)。

•採用HTTP呼叫的演算法服務叢集由於歷史&異構原因,採用了兩種服務註冊&發現方式

•Zookeeper。Zookeeper在中心和區域都做單獨部署,客戶端請求的時候按照地域拉取對應的Zookeeper。通過這種方式實現流量的同機房訪問,地域彼此間資料隔離,當單個地域服務出現問題時,只能通過將其他區域的服務資料掛載到故障地域對應的Zookeeper下面來進行恢復。•vipserver(阿里自研的一套叢集路由軟體負載均衡系統)。由於vipserver本身是分散式的負載均衡系統,且支援多種路由方式,故只部署一套。

導購鏈路使用快取的地方很多,大致分成兩種用法

•緩解持久層的訪問壓力。先訪問快取,快取如果沒有資料則請求持久化層並把資料載入至快取中,快取本身不做資料一致性保證。這種情況比較好處理,因為不涉及到多區域之間的同步,只需要簡單做多地域部署即可。

image.png

•用作資料持久化。典型的如分散式鎖,計數器等。這種場景會有中心和區域的概念,彼此間雙向同步,這種場景在單區域部署的時候和上面的用法沒有太大區別,但是在多地域部署架構下,就會因為雙寫導致資料出現不一致,因此需要保證同一個key同一時間不能在多區域同時寫。

•區域同步至中心。因為資料需要做持久化,所以會在中心有一份完整的資料集,區域保證資料的最終一致性即可。•中心同步至區域。保證區域的資料和中心的資料一致。

image.png

資料庫部署

按照分散式系統的CAP定理:Consistency(一致性)、 Availability(可用性)、Partition tolerance(分割槽容錯性),三者不可得兼。所以嚴格意義上來說,資料庫的異地部署只能三選二。但是在分散式系統中必然是分割槽的,而且分割槽之間的網路我們沒法控制,也就是說P是一個事實,我們只能從C和A中二選一,這分別對應著資料庫的兩種資料複製方式。

•主從複製模式的MySql:中心寫成功即返回,從節點依賴主從之間的資料同步。這種模式下保證了A和P,犧牲了C。

image.png

•雙向複製模式的MySql:沒有主從節點之分,節點與節點之間實現資料最終一致性。這種模式下同樣保證了A和P,犧牲了C。

image.png

•採用Paxos協議的分散式資料庫如Google的Spanner等,採用Paxos協議來保證資料的強一致性,但是在Master節點掛了之後在新的Master選舉出來之前不可用,即保證了C和P,犧牲了A。

一方面根據導購鏈路的特點(絕大部分都是資料讀取操作,可以容忍短時間內的不一致)。另一方面原有的資料儲存採用MySql,考慮到成本,最終選擇主從複製模式MySql。​

總結

異地部署給系統帶來的最大挑戰是物理距離帶來的網路延時,整個系統設計都圍繞著這個展開。總的來說在解決跨地域延時過程中我們遵循兩個大的原則:a)流量地域內自閉環。b)堅持可用性優先。在這兩個大原則之下從接入層,服務層以及資料儲存層做了相應的改造&部署。​

目前閒魚部分鏈路已經實現了兩地三機房部署,並且已經承接線上流量,具備了異地容災的能力。同時經過本次改造,導購鏈路具備了較好的擴充套件性,能夠以極低的成本快速部署至更多機房。​

但是一方面由於導購鏈路大部分都是隻讀場景,對資料要求弱一致性即可。對於資料強一致性場景帶給系統的挑戰會更大。另一方面業務是一個不停演進的過程,如何保證在演進過程中仍然能保證異地多活的部署架構,這是急需解決的問題。