1. 程式人生 > >歷經8年雙11流量洗禮,淘寶開放平臺架構和技術難點解密

歷經8年雙11流量洗禮,淘寶開放平臺架構和技術難點解密

高效能批量API呼叫

在雙11高併發的場景下,對商家和ISV的系統同樣是一個考驗,如何提高ISV請求API的效能,降低請求RT和網路消耗同樣是一個重要的事情。在ISV開發的系統中通常存在這樣的邏輯單元,需要呼叫多個API才能完成某項業務,在這種序列呼叫模式下RT較長同時多次呼叫傳送較多重複的報文導致網路消耗過多,在弱網環境下表現更加明顯。

API閘道器提供批量API呼叫模式緩解ISV在呼叫RT過高和網路消耗上的痛點。ISV發起的批量請求會在TOP SDK進行合併,併發送到指定的閘道器;閘道器接收到請求後在單執行緒模式下進行公共邏輯計算,計算通過後將呼叫安裝API維度拆分,並分別發起非同步化遠端呼叫,至此該執行緒結束並被回收;每個子API的遠端請求結果返回時會拿到一個執行緒進行私有邏輯處理,處理結束時會將處理結果快取並將完成計數器加一;最後完成處理的執行緒,會將結果進行排序合併和輸出。

多維度流量控制

TOP API閘道器暴露在網際網路環境,日呼叫量達幾百億。特別是在雙11場景中,API呼叫基數大、呼叫者眾多以及各個API的服務能力不一致,為了保證各個API能夠穩定提供服務,不會被暴漲的請求流量擊垮,那麼多維度流量控制是API閘道器的一個重要環節。API閘道器提供一系列通用的流量控制規則,如API每秒流控、API單日呼叫量控制、APPKEY單日呼叫量控制等。

在雙11場景中,也會有一些特殊的流量控制場景,比如單個API提供的能力有限,例如只能提供20萬QPS的能力而實際的呼叫需求可能會有40萬QPS。在這種場景下怎麼去做好流量分配,保證核心業務呼叫不被限流。

TOP API閘道器提供了流量分組的策略,比如我們可以把20萬QPS的能力分為3個組別,並可以動態去配置和調整每個組別的比例,如:分組1佔比50%、如分組2佔比40%、分組3佔比10%。我們將核心重要的呼叫放到分組1,將實時性要求高的呼叫放到分組2,將一些實時性要求不高的呼叫放到分組3。通過該模式我們能夠讓一些核心或者實時性要求高的呼叫能夠較高概率通過流量限制獲取到相應的資料。同時TOP API閘道器是一個外掛化的閘道器,我們可以編寫流控外掛並動態部署到閘道器,在流控外掛中我們可以獲取到呼叫上下文資訊,通過Groovy指令碼或簡單表示式編寫自定義流控規則,以滿足雙11場景中豐富的流控場景。

使用叢集流控還是單機流控?單機流控的優勢是系統開銷較小,但是存在如下短板:

  1. 叢集單機流量分配不均。

  2. 單日流控計數器在某臺伺服器掛掉或者重啟時比較難處理。

  3. API QPS限制小於閘道器叢集機器數量時,單機流控無法配置。

基於這些問題,API閘道器最開始統一使用叢集流控方案,但在雙11前壓測中發現如下一些問題:

  1. 單KEY熱點問題,當單KEY QPS超過幾十萬時,單臺快取伺服器RT明顯增加。

  2. 快取叢集QPS達到數百萬時,伺服器投入較高。

針對第一個問題的解法是,將快取KEY進行分片可將請求離散多臺快取伺服器。針對第二個問題,API閘道器採取了單機+叢集流控相結合的解決方案,對於高QPS API流控採取單機流控方案,服務端使用Google ConcurrentLinkedHashMap快取計數器,在併發安全的前提下保持了較高的效能,同時能做到LRU策略淘汰過期資料。

高可靠訊息服務

有了API閘道器,服務商可以很方便獲取淘係數據,但是如何實時獲取資料呢?輪詢 !資料的實時性依賴於應用輪詢間隔時間,這種模式,API呼叫效率低且浪費機器資源。基於這樣的場景,開放平臺推出了訊息服務技術,提供一個實時的、可靠的、非同步雙向資料交換通道,大大提高API呼叫效率。目前,整個系統日均處理百億級訊息,可支撐百萬級瞬時流量,如絲般順滑。

總體架構

訊息系統從部署上分為三個子系統,路由系統、儲存系統以及推送系統。訊息資料先儲存再推送,保證每條訊息至少推送一次。寫入與推送分離,傳送方不同步等待接收方應答,客戶端的任何異常不會影響傳送方系統的穩定性。系統模組互動如圖所示。

路由系統,各個處理模組管道化,擴充套件性強。系統監聽主站的交易、商品、物流等變更事件,針對不同業務進行訊息過濾、鑑權、轉換、儲存、日誌打點等。系統執行過程記錄各個訊息的處理狀況,通過日誌採集器輸出給JStorm分析叢集處理並記錄訊息軌跡,做到每條訊息有跡可循。

儲存系統,主要用於削峰填谷,基於BitCask儲存結構和記憶體對映檔案,磁碟完全順序寫入,速度極佳。資料讀取基於FileRegion零拷貝技術,減少記憶體拷貝消耗,資料讀取速度極快。儲存系統部署在多個機房,有一定容災能力。

推送系統,基於Disputor構建事件驅動模型,使用Netty作為網路層框架,構建海量連線模型,根據連線吞吐量智慧控制流量,降低慢連線對系統的壓力;使用WebSocket構建 長連線通道,延時更低;使用物件池技術,有效降低系統GC頻率;從訊息的觸發,到拉取,到傳送,到確認,整個過程完全非同步,效能極佳。

選擇推送還是拉取

在訊息系統中,一般有兩種消費模式:服務端推送和客戶端拉取。本系統主要面向公網的伺服器,採用推送模式,有如下優點 :

  1. 實時性高。從訊息的產生到推送,總體平均延時100毫秒,最大不超過200毫秒。

  2. 伺服器壓力小。相比於拉取模式,每次推送都有資料,避免空輪詢消耗資源。

  3. 使用簡便。使用拉取模式,客戶端需要維護消費佇列的位置,以及處理多客戶端同時消費的併發問題。而在推送模式中,這些事情全部由伺服器完成,客戶端僅需要啟動SDK監聽訊息即可,幾乎沒有使用門檻。

當然,系統也支援客戶端拉取,推送系統會將客戶端的拉取請求轉換為推送請求,直接返回。推送伺服器會據此請求推送相應資料到客戶端。即拉取非同步化,如果客戶端沒有新產生的資料,不會返回任何資料,減少客戶端的網路消耗。

如何保證低延時推送

在採用推送模式的分散式訊息系統中,最核心的指標之一就是推送延時。各個長連線位於不同的推送機器上,那麼當訊息產生時,該連線所在的機器如何快速感知這個事件?

在本系統中,所有推送機器彼此連線(如圖所示),構成一個通知網,其中任意一臺機器感知到訊息產生事件後,會迅速通知此訊息歸屬的長連線的推送機器,進而將資料快速推送給客戶端。而路由系統每收到一條訊息,都會通知下游推送系統。上下游系統協調一致,確保訊息一觸即達。

如何快速確認訊息

評估訊息系統另外一個核心指標是訊息丟失問題。由於面向廣大開發者,因此係統必須兼顧各種各樣的網路環境問題,開發者能力問題等。為了保證不丟任何一條訊息,針對每條推送的訊息,都會開啟一個事務,從推送開始,到確認結束,如果超時未確認就會重發這條訊息,這就是訊息確認。

由於公網環境複雜,訊息超時時間註定不能太短,如果是內網環境,5秒足矣,訊息事務在記憶體就能完成。然後在公網環境中,5秒遠遠不夠,因此需要持久化訊息事務。在推送量不大的時候,可以使用資料庫記錄每條訊息的傳送記錄,使用起來也簡單方便。但是當每秒推送量在百萬級的時候,使用資料庫記錄的方式就顯得捉襟見肘,即便是分庫分表也難以承受如此大的流量。

對於訊息推送事務資料,有一個明顯特徵,99%的資料會在幾秒內讀寫各一次,兩次操作完成這條資料就失去了意義。在這種場景,使用資料庫本身就不合理,就像是在資料庫中插入一條几乎不會去讀的資料。這樣沒意義的資料放在資料庫中,不僅資源浪費,也造成資料庫成為系統瓶頸。

如上圖所示,針對這種場景,本系統在儲存子系統使用HeapMemory、DirectMemory、FileSystem三級儲存結構。為了保護儲存系統記憶體使用情況,HeapMemory儲存最近10秒傳送記錄,其餘的資料會非同步寫入記憶體對映檔案中,並寫入磁碟。HeapMemory基於時間維度劃分成三個HashMap,隨著時鐘滴答可無鎖切換,DirectMemory基於訊息佇列和時間維度劃分成多個連結串列,形成連結串列環,最新資料寫入指標頭連結串列,末端指標指向的是已經超時的事務所在連結串列。這裡,基於訊息佇列維護,可以有效隔離各個佇列之間的影響;基於時間分片不僅能控制連結串列長度,也便於掃描超時的事務。

在這種模式下,95%的訊息事務會在HeapMemory內完成,5%的訊息會在DirectMemory完成,極少的訊息會涉及磁碟讀寫,絕大部分訊息事務均在記憶體完成,節省大量伺服器資源。

零漏單資料同步

我們已經有了API閘道器以及可靠的訊息服務,但是對外提供服務時,使用者在訂單資料獲取中常常因為經驗不足和程式碼缺陷導致延遲和漏單的現象,於是我們對外提供資料同步的服務。

傳統的資料同步技術一般是基於資料庫的主備複製完成的。在簡單的業務場景下這種方法是可行的,並且已經很多資料庫都自帶了同步工具。 但是在業務複雜度較高或者資料是對外同步的場景下,傳統的資料同步工具就很難滿足靈活性、安全性的要求了,基於資料的同步技術無法契合複雜的業務場景。

雙11場景下,資料同步的流量是平常的數十倍,在峰值期間是百倍,而資料同步機器資源不可能逐年成倍增加。保證資料同步寫入的平穩的關鍵在於流量調控及變更合併。

分散式資料一致性保證

在資料同步服務中,我們使用了訊息 + 對賬任務雙重保障機制,訊息保障資料同步的實時性,對賬任務保障資料同步一致性。以訂單資料同步為例,訂單在建立及變更過程中都會產生該訂單的訊息,訊息中夾帶著訂單號。接受到該訊息後,對短時間內同一訂單的訊息做合併,資料同步客戶端會拿訊息中的訂單號請求訂單詳情,然後寫入DB。訊息處理過程保證了訂單在建立或者發生了任意變更之後都能在極短的延遲下更新到使用者的DB中。

對賬任務排程體系會同步執行。初始化時每個使用者都會生成一個或同步任務,每個任務具有自己的唯一ID。資料同步客戶端存活時每30秒發出一次心跳資料,針對同一分組任務的機器的心跳資訊將會進行彙總排序,排序結果一般使用IP順序。每臺客戶端在獲取需執行的同步任務列表時,將會根據自身機器在存活機器總和x中的順序y,取得任務ID % x = y - 1的任務列表作為當前客戶端的執行任務。執行同步任務時,會從訂單中心取出在過去一段時間內發生過變更的訂單列表及變更時間,並與使用者DB中的訂單進行一一對比,如果發現訂單不存在或者與儲存的訂單變更時間不一致,則對DB中的資料進行更新。

資源動態調配與隔離

在雙11場景下如何保證資料同步的高可用,資源調配是重點。最先面臨的問題是,如果每臺機器都是冪等的對應全體使用者,那麼光是這些使用者身後的DB連線數消耗就是很大問題;其次,在淘寶的生態下,賣家使用者存在熱點,一個熱點賣家的訂單量可能會是一個普通賣家的數萬倍,如果使用者之間直接共享機器資源,那麼大流量使用者將會佔用幾乎全部的機器資源,小流量使用者的資料同步實效會受到很大的影響。

為了解決以上問題,我們引入了分組隔離。資料同步機器自身是一個超大叢集,在此之上,我們將機器和使用者進行了邏輯叢集的劃分,同一邏輯叢集的機器只服務同一個邏輯叢集的使用者。在劃分邏輯叢集時,我們將熱點使用者從使用者池中取出,劃分到一批熱點使用者專屬叢集中。分組隔離解決了DB連線數的問題,在此場景下固定的使用者只會有固定的一批機器為他服務,只需要對這批機器分配連線數即可,而另一個好處是,我們可以進行指定邏輯叢集的資源傾斜保障大促場景下重點使用者的資料同步體驗。

資料同步服務大叢集的機器來源於三個機房, 在劃分邏輯叢集時,每個邏輯分組叢集都是至少由兩個以上機房的機器組成,在單個機房宕機的場景下,邏輯叢集還會有存活機器,此時訊息和任務都會向存活的機器列表進行重新分配,保證該邏輯叢集所服務的使用者不受影響。 在機器發生宕機或者單個邏輯叢集的壓力增大時,排程程式將會檢測到這一情況並且對冗餘及空閒機器再次進行邏輯叢集劃分,以保證資料同步的正常執行。在叢集壓力降低或宕機機器恢復一段時間後,排程程式會自動將二次劃分的機器回收,或用於其他壓力較大的叢集。

通用資料儲存模型

訂單上儲存的資料結構隨著業務的發展也在頻繁的發生的變化,進行訂單資料的同步,需要在上游結構發生變化時,避免對資料同步服務產生影響,同時兼顧使用者的讀取需求。對此我們設計了應對結構易變資料的大欄位儲存模型。在訂單資料的儲存模型中,我們將訂單號、賣家暱稱、更新時間等需要被當做查詢/索引條件的欄位抽出獨立欄位儲存,將整個的訂單資料結構當成json串存入一個大欄位中。

這樣的好處是通過大欄位儲存做到對上游業務的變化無感知,同時,為了在進行增量資料同步時避免對大欄位中的訂單詳情進行對比,在進行資料同步寫入的同時將當前資料的hashcode記錄儲存,這樣就將訂單資料對比轉換成了hashcode與modified時間對比,提高了更新效率。

如何降低資料寫入開銷

在雙11場景下,資料同步的瓶頸一般不在淘寶內部服務,而在外部使用者的DB效能上。資料同步是以訊息的方式保證實時性。在處理非建立訊息的時候,我們會使用直接update + modified時間判斷的更新方式,替換傳統的先select進行判斷之後再進行update的做法。這一優化降低了90%的DB訪問量。

傳統寫法:

SELECT * FROM jdp_tb_trade WHERE tid = #tid#;

UPDATE jdp_tb_trade SET jdp_response = #jdpResponse#, jdp_modified = now() WHERE tid = #tid#

優化寫法:

UPDATE jdp_tb_trade SET jdp_response = #jdpResponse#, jdp_modified = now() WHERE tid = #tid# AND modified #modified#

訂單資料存在明顯的時間段分佈不均的現象,在白天訂單成交量較高,對DB的訪問量增大,此時不適合做頻繁的刪除。採用邏輯刪除的方式批量更新失效資料,在晚上零點後交易低峰的時候再批量對資料錯峰刪除,可以有效提升資料同步體驗。