當前位置: 妍妍網 > 碼農

會員購交易系統架構演進

2024-02-17碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

1.背景

會員購是B站2017年推出的IP消費體驗服務平台,在售商品以手辦、漫畫、JK制服等貼合平台生態的商品為主。隨著業務發展,會員購從最開始的預售,現貨拓展到全款預售,盲盒,眾籌等多種售賣方式,銷售渠道也遍布 貓耳(現已下線),QQ小程式,漫畫等多個業務渠道,再加上不斷增加的行銷活動玩法,每年幾次大促活動的爆發式流量,對於會員購交易系統來說,無疑是一個巨大的挑戰。

2.效能

每年的拜年紀,626(公司周年慶),919(會員購周年慶),會員購都會搞大促活動,營運會挑選一些比較熱門的手辦進行先發,加上提前發放紅包優惠券,各種優惠活動的刺激,每次大促0點開售流量就是幾百倍的爆發,早期也因為壓力太多出過幾次事故,所以如何最佳化效能,提高交易的吞吐量是首要的。

2.1呼叫鏈路最佳化

面臨問題:

在最初版的系統中,下單介面有明顯的等待時間,使用者體驗不是很好,能支持的最大qps也有限

如圖 2-1 所示 通看分析下單呼叫鏈路發現,存在多個介面重復呼叫,介面全是序列呼叫的情況,下單介面耗時太長,達到400+ms,已經嚴重影響系統效能及使用者體驗

圖 2-1 初版下單鏈路圖

從鏈路上可以看出下單是IO密集型套用,CPU 利用率低,程式碼序列執行的話同步等待時間較長,為此我們重新梳理下單業務邏輯,對下單流程進行責任鏈模式改造,如2-2圖所示

2-2 下單鏈路簡單示意圖

同時我們對系統做了以下最佳化

  1. 對沒有依賴的服務進行並行呼叫(商品/店鋪/活動/使用者資訊等一起並行呼叫),如圖2-3所示

  2. 最佳化呼叫鏈減少冗余呼叫,推動下遊服務介面改造及合並,保證一次請求下來,每個基礎介面只會被呼叫一次,如圖2-3所示

  3. 設定合理的超時時間和及連線重試(200ms, 部份介面99分位上浮100%,connect連線重試)

  4. 排除事務內的外部呼叫(服務依賴,mq,緩存)

  5. 對弱依賴介面進行mq或異步呼叫(設定關註/緩存手機號/回滾庫存優惠券等)

2-3 最佳化後的呼叫鏈路

經過最佳化後的介面耗時如2-4圖所示,從原來300ms降低100ms左右,效果比較顯著,使用者的下單體驗得到較大提升

2-4 下單耗時對比圖

2.2異步下單最佳化

面臨問題:電商活動離不開秒殺場景,通常情況下小庫存秒殺做好限流的話問題不大,但拜年祭手辦通常有5000個左右的庫存,如2-5如圖所示,屬於大庫存秒殺 , 限流值設得太小會嚴重影響使用者體驗 ,大庫存搶購時下單qps遇到瓶頸 600+qps的時候庫存服務行鎖比較嚴重,耗時開始大幅上升,大量資料庫操作占用連線數較高。

2-5 拜年紀商品

思考:伺服器的處理能力是恒定的,就像早高峰一樣,需要錯峰限行,這就是我們說的削峰,對流量進行削峰不僅讓伺服器處理得更加平穩,也節省伺服器資源。一般削峰的手段有驗證碼,排隊等方式,這裏我們主要是采用異步下單這種排隊的做法。

說到排隊,最容易想到的就是訊息佇列,可以透過訊息佇列把兩個系統模組進行解耦,對於搶購場景來說也是非常合適的,可以有效把流量透過佇列來承接,然後平滑得進行處理,如圖2-6所示

2-6 訊息佇列解耦

按照訊息佇列的排隊方案,我們把整個下單流程調整為異步批次下單鏈路(圖2-7所示) ,在合法校驗過後生成訂單號送出到databus訊息佇列 (圖2-8所示),再監聽databus批次拉取訂單進行合並下單(圖2-9所示),目前設定的是最多20個一消費,下單結果會在資料庫及redis中保存。

2-7異步下單鏈路

2-8 送出下單請求至mq

2-9 從mq消費訂單訊息

如圖2-10所示,在進入佇列後,前端會提示活動火爆,正在努力下單中 ,同時在0~2秒內隨機呼叫下單結果查詢介面,輪詢30秒(必須設定最大時間兜底,防止無限查詢)

對於合並的訂單進行批次凍結庫存,並列凍結優惠券,批次合並sql插入資料庫,最大限度上減少效能消耗

2-10 異步下單範例圖

其他最佳化細節

  1. 下單限頻/限流

  2. 其中對於一些弱依賴的操作直接進行降級,比如設定商鋪關註,緩存手機號,記錄操作日誌等

  3. 批次操作異常時(介面超時則fail fast),會分解為單個訂單重新進行呼叫(庫存操作會試探單庫存扣減 單庫存扣減成功 並行請求剩余訂單,單庫存扣減失敗 剩余訂單全部置為失敗)

  4. 下單結果查詢走redis,異常情況降級為資料庫

  5. databus異常時,直接降級為同步下單(庫存服務也會做限流)

  6. databus消費者會做冪等及超時判斷(訂單投遞時間跟當前時間差值),超過一定時間會自動拋棄,下單失敗

經過改造,壓測下單支持4000+tps,最終也順利利用異步下單支撐了早期的拜年祭手辦搶購,如圖2-11所示

2-11 活動搶購qps圖

2.3分庫分表

首先並不是所有表都需要進行切分,主要還是看數據的增長速度。切分後會在某種程度上提升業務的復雜度,避免"過度設計"和"過早最佳化"。分庫分表之前,不要為分而分,先盡力去做力所能及的事情,例如:升級硬體、升級網路、垂直拆分、讀寫分離、索引最佳化等等。當數據量達到單表的瓶頸時候,再考慮分庫分表。

數據量過大的風險如下:

1)高負載下主從延遲嚴重,影響使用者體驗,並且對資料庫備份,如果單表太大,備份時需要大量的磁盤IO和網路IO。例如1T的數據,網路傳輸占50MB時候,需要20000秒才能傳輸完畢,整個過程的風險都是比較高的

2)對一個很大的表進行DDL修改時,MySQL會鎖住全表,這個時間會很長,這段時間業務不能存取此表,影響很大。如果使用pt-online-schema-change,使用過程中會建立觸發器和影子表,也需要很長的時間。在此操作過程中,都算為風險時間。將數據表拆分,總量減少,有助於降低這個風險。

3)大表會經常存取與更新,就更有可能出現鎖等待,一旦出現慢查詢,風險很大,容錯性很低。將數據切分,用空間換時間,變相降低存取壓力,而且利用水平切分,當一個資料庫出現問題時,不會影響到100%的使用者,每個庫只承擔業務的一部份數據,這樣整體的可用性就能提高

這裏我們明確下分庫 分表到底能解決什麽問題

  • 分表:解決單表過大導致的查詢效率下降(海量儲存,即使索引正確也會很慢) MySQL 為了提高效能,會將表的索引裝載到記憶體中。InnoDB buffer size 足夠的情況下,其能完成全載入進記憶體,查詢不會有問題。但是,當單表資料庫到達某個量級的上限時,導致記憶體無法儲存其索引,使得之後的 SQL 查詢會產生磁盤 IO,從而導致效能下降。當然,這個還有具體的表結構的設計有關,最終導致的問題都是記憶體限制。這裏,增加硬體配置,可能會帶來立竿見影的效能提升。

  • 分庫:解決Master伺服器無法承受讀寫操作壓力(高並行存取,吞吐量)

  • 在2020年的時候,會員購隨著業務發展,訂單數據快速增長,基本每半年數據量就會翻倍,所有核心表均達到千萬級別 大表的DDL,查詢效率,健壯性都有問題 ,並且高負載下,會有較為明顯的主從延遲,影響到使用者體驗。

    首先是技術選型:

    站在巨人的肩膀上能省力很多,目前分庫分表已經有一些較為成熟的開源解決方案:

  • 阿裏的TDDL,DRDS和cobar

  • 開源社群的sharding-jdbc(3.x開始已經更名為sharding-sphere)

  • 民間組織的MyCAT

  • 360的Atlas

  • 美團的zebra

  • 這麽多的分庫分表中介軟體全部可以歸結為兩大型別:CLIENT模式 PROXY模式

    無論是CLIENT模式,還是PROXY模式。幾個核心的步驟是一樣的:SQL解析,重寫,路由,執行,結果歸並。

    經過討論大家更傾向於CLIENT模式,架構簡單,效能損耗較小,運維成本低,而且目前部份計畫中都已經被引入shardingjdbc,並且部份模組已經在使用其分庫分表功能,網上文件豐富,框架比較成熟 。

    選擇sharding key:

    sharding column的選取是很重要的,sharding column選擇的好壞將直接決定整個分庫分表方案最終是否成功。

    sharding column的選取跟業務強相關,選擇sharding column的方法最主要分析你的API流量,優先考慮流量大的API,將流量比較大的API對應的SQL提取出來,將這些SQL共同的條件作為sharding column 例如一般的OLTP系統都是對使用者提供服務,這些API對應的SQL都有條件使用者ID,那麽,使用者ID就是非常好的sharding column。

    非sharding column查詢該怎麽辦?

    1. 建立非sharding column內容到sharding column的對映關系

    2. 雙寫冗余全量數據(不需要二次查詢)

    3. 數據異構(TIDB,ES,HIVE等,應對復雜條件查詢,近即時或離線查詢)

    4. 基因融合(比如訂單號裏融合mid基因,最新的訂單號規則:orderId+midQ2 比如4004164057659338)

    切分策略:

    1.範圍切分

    比如按照時間區間或ID區間來切分,如圖3-1所示,優點:單表大小可控,天然水平擴充套件。缺點:無法解決集中寫入瓶頸的問題。

    3-1 範圍切分

    2.Hash切分

    如圖3-2所示,如果希望一勞永逸或者是易於水平擴充套件的,還是推薦采用mod 2^n這種一致性Hash

    3-2 Hash切分

    3.會員購交易切分策略

    如圖3-3所示,切分鍵選擇:mid 和 order_id相關

    根據 mid 分表,使用新的orderid 生成規則,orderid 融入(midQ2)

    庫表數量:4個集群(主從),每個集群4個庫,每個庫16張表,總計256張表

    庫路由策略:mid 

    表路由策略:(midQ2)/32

    公式:

    中間變量 = MID % (庫數量*表數量)

    庫路由 = 中間變量 % 庫數量

    表路由 = 取整(中間變量 /庫數量)

    3-3 庫表策略示意圖

    我們采用的是不清洗老數據的方式,好處是老的訂單數據依然走老庫,這樣能節省一部份清洗數據的工作量

    梳理sql:

    計畫中的sql有些是不滿足分片條件的,所以我們是要提前梳理計畫中的sql的

    1.透過 druid 界面 可以統計到所有執行的 sql

    2.配合靜態掃描sql工具

    3.DBA 拉取 SQL

    4.人工檢視程式碼 當梳理出對應所有 SQL

    針對沒有分片鍵的SQL進行改造,不確定的SQL進行驗證,不支持的SQL給出處理方式

    當然如何進行遷移也是很重要的步驟,我們是采用下面的步驟,如圖3-4所示

    1. 歷史數據歸檔,不做遷移,老數據修改依舊路由到老庫

    2. 切讀寫請求,即將讀寫流量請求引入到新系統中

    3. 回寫數據,binlog 監聽新資料庫回寫到老系統中,並進行校驗

    3-4 不停機遷移示意圖

    最後總結下整個分庫分表的步驟

    1.根據容量(當前容量和增長量)評估分庫分表個數

    2.選key(均勻)

    3.分表規則(hash或range等)

    4.梳理sql並驗證

    5.執行(一般雙寫)

    在整個交易系統完成分庫分表後,徹底解決了資料庫的瓶頸問題,歷經多次大促壓測突發流量等場景都沒有出問題,保障了整個平台系統的穩定性。

    3.總結

    在經過調研鏈路最佳化,異步下單改造,資料庫分庫分表後,整個交易系統的效能得到了較大的提升,也較為順利得支撐了歷次大促活動,後續我們也會繼續對一些歷史系統(比如票務系統)進行改造升級來提升使用者體驗。

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:姜健

    來源:嗶哩嗶哩技術

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]