蘇寧高時效、高併發秒殺業務中臺的設計與實現

設計背景

對於蘇寧易購主站而言,正常的用戶購物流程囊括選品、下單、庫存扣減、付款、訂單狀態更新、物流履約等。但是在電商業務中往往會涉及到對某些熱點商品的秒殺場景。相比於正常購物流程,秒殺場景具有時效性高、併發量大、瞬時業務量極高的業務特性,往往會出現顯著的分佈式一致性問題。正常的業務系統不能很好地應對瞬時高併發的業務需求,因此就需要針對於秒殺場景進行相應的架構優化,抑或是設計專門用於秒殺的中臺業務系統。

就秒殺業務而言,系統在瞬時會達到極高的併發量,如果與其它業務耦合,那麼勢必會對其它業務造成風險,因此基於安全性考慮和業務隔離原則,秒殺系統在設計上應該與其它系統充分解耦,單獨部署。本文將討論在蘇寧現有的技術架構和中臺組件的基礎上,如何去實現一個通用型秒殺業務中臺。

架構設計

1. 系統前端與負載層設計

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖一:前端與負載層設計

鑑於秒殺業務本身的高併發特性,對用戶請求進行前端分流是必不可少的一步。在系統上游就對部分用戶請求進行處理,可以避免海量請求對後端服務器產生過大壓力。因爲用戶往往在秒殺前幾分鐘就開始點擊下單按鈕,因此在秒殺開始前可以使用靜態資源頁面,用戶請求由 CDN 直接響應,不必到達後端服務器。

此外,由於秒殺業務的高時效性特徵,下單窗口基本集中在秒殺開始後的幾秒鐘之內。因此我們可以在秒殺前某個時間點再將下單 URL 發送給前端。爲了防止有人提前拿到下單 URL 進入支付流程,可以在 URL 中加入服務端生成的隨機字符串,或者對下單請求進行時間校驗,單就性能而言,前一種方案校驗邏輯更少,性能更優。

在秒殺開始前,需要在商品秒殺頁顯示活動開始倒計時,其一般情況下直接調用用戶本地時鐘,因此就可能存在客戶端與服務端時鐘不一致的情況。因此在服務端需要提供定期授時接口將服務端時鐘同步給客戶端,爲了節省帶寬可以將時間戳信息優化壓縮爲儘可能短的 JSON 格式,去除掉不必要的信息,減輕網絡帶寬壓力。

參與秒殺活動的商品一般數量稀少,註定只有少數用戶能夠進入下單支付流程,因此可以在負載層進行相應控制。下單接口可以在蘇寧應用防火牆配置流量控制,當下單請求超過閥值後熔斷下單接口。而對於那些被應用防火牆放行的下單請求,由 Ngnix 集羣將流量均勻負載到後端應用服務器。在服務器內存中可以定義一個請求計數器,當某臺服務器受理的下單請求超過閥值後,則該服務器不再受理用戶的下單信息,直接返回給用戶“活動已結束”頁面。

2. 系統服務端設計

(1)系統服務端縱向拆分

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖二:系統服務端縱向拆分

秒殺系統在縱向架構層面將主要分爲三大模塊:web 模塊、admin 模塊和 task 模塊。其中 web 和 admin 模塊爲了兼容獨佔型業務將會包含一個接口路由子模塊用於接口級路由策略控制,三大模塊將分別部署在不同的 JBoss 集羣上,通過分佈式遠程調用框架協同工作。

  • web 層:前臺業務模塊,該模塊主要用於處理用戶請求,這一模塊承擔最關鍵也是載荷最重的業務,因此必須對這一模塊進行單獨優化,除了服務器橫向擴容外,前臺模塊在系統部署層面將會分爲兩個實例,用於展示鏈路和交易鏈路的業務分流。
  • admin 層:後臺業務模塊,本模塊主要用於運維管理人員的日常數據維護,新增和管理秒殺活動的報名信息、商品信息、活動信息等。
  • task 層:中臺定時任務模塊,本模塊主要負責處理來自統一調度平臺的定時任務調度請求,如定時向前端授時,處理過期的活動,商品數據等。爲便於集中管理,授時任務每分鐘執行一次,對於需要向前端授時的活動,將單獨存表,每分鐘掃描需要執行授時任務的活動信息並下發時間戳。

(2)系統服務端橫向拆分

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖三:系統服務端橫向拆分
  • 網絡層:鑑於秒殺系統本身的高併發特性,在架構設計上要儘可能踐行前端處理的原則,能在前端響應的請求,就絕不放在後端。在秒殺開始前,CDN 直接響應靜態頁面給用戶,爲服務器分流大部分流量。在靜態資源緩存時間設計上要精準靈活,當秒殺開始前幾秒向服務器放行用戶請求。如果 CDN 本身存在性能瓶頸或者後端服務器業務處理能力有限的話還可以在負載層加一套 Varish 集羣作爲二級緩存,進一步爲後端分流。
  • 負載層:到達蘇寧內網的請求,首先經過蘇寧應用防火牆。防火牆將會作爲到應用服務器的第二道防線,承擔過濾惡意請求,黃牛用戶,黑客攻擊的任務。對於下單接口,應用防火牆應當設置合理的流控策略。對於同一 IP 的用戶,最多執行 10 次下單操作,超過 10 次的請求將直接攔截不再轉發到後端。同時防火牆還應當在宏觀層面對流量閥值進行控制,TPS 超過閥值後進行接口級熔斷,防止流量過高引發應用服務器宕機。
  • 應用層:在 CDN 和防火牆兩層防線的加持下,最終到達應用服務器的請求應當只剩佔比較小的一部分。有使於龐大的用戶基數,這部分流量仍然不容小覷。除了校驗,下單,支付外,還會有一部分商品信息相關的狀態查詢請求。因爲前端頁面已經儘可能實現了靜態化,所以只需要對返回前端的商品狀態數據格式進行合理的壓縮,並在前端予以更新即可。爲了進一步解除業務耦合,可以對展示鏈路和交易鏈路採用分集羣部署,按域名分流的方案,進一步按業務分導流量,提高系統安全性和可用性。
  • 數據層:爲了有效減輕數據庫壓力,在數據層設計上將會採用雙機房數據互補的獨佔型數據庫設計,這一部分將在後文中詳述。

(3)系統緩存設計與庫存扣減方案

就秒殺業務場景而言,因爲存在下單校驗等前置流程,這就註定大部分用戶都走不到支付這一步。該部分用戶對數據庫都只是發送讀請求,而只有少部分下單成功的用戶纔會對數據庫產生寫請求。因此將大部分讀請求放在緩存中處理將使系統性能顯著提升。

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖四:系統緩存設計

以庫存爲例,秒殺場景中庫存校驗和庫存扣減必然在短時間內產生極高的併發量,庫存緩存的設計將對系統性能產生即爲重要的影響。秒殺活動開始前,運營會在後臺維護商品的總庫存,剩餘庫存和可鎖庫存信息,同時將信息提前預熱到 Redis 緩存中。當用戶通過支付校驗並進入下單流程後,系統會首先操作 Redis 中的可鎖庫存數,同時在 DB 中寫入一條鎖庫存記錄。當用戶支付成功後,扣減 Redis 中的剩餘庫存,同時刪除 DB 中的鎖庫存記錄。因爲在這一過程中主要數據更新發生在 Redis 中,因此需要將 Redis 中的數據定時同步給 DB。系統管理員可以根據業務需求和實際的系統性能對數據同步週期進行配置。對 DB 和 Redis 的操作將放置在 TCC 分佈式一致性框架中,當某一步驟失敗時同時回滾 DB 和 Redis,避免數據庫和緩存出現數據不一致的情形。

即便將熱點數據操作都放置在 Redis 中,仍然有可能產生活動超賣的情形。比如某商品只剩一件時,同時有多個用戶提交下單請求。因爲庫存剩餘一件,因此每個用戶都通過庫存校驗並進入下單流程,進而引發商品超售,對此我們可以採用以下幾種解決方案:

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖五:Redis 悲觀鎖機制下的庫存扣減方案

方案一,採用 Redis 的悲觀鎖機制,當一個線程訪問庫存數據時拒絕其它線程的訪問,這樣顯然可以解決多個用戶同時通過下單引發超售的問題。但是這一方案會顯著拖累系統性能,尤其是秒殺場景下併發量極高,如果每個用戶都只能等到其他用戶鎖釋放之後才能訪問庫存數據,那麼有一部分用戶可能永遠都沒有機會進入下單流程。

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖六:FIFO 隊列機制下的庫存扣減方案

方案二,採用 FIFO 隊列進行多線程轉單線程處理,用一個先進先出的隊列使用戶請求實現序列化,這就保證每個用戶請求都將基於先後順序到達 Redis,進而有效避免了某些用戶永遠訪問不到庫存數據的情況。不過這一方案也存在弊端,因爲這一中間隊列顯然會是一個入多出少的隊列,那麼如果隊列本身內存冗餘不夠,那麼海量用戶請求有可能瞬間將隊列擠爆,而中間隊列所需要的資源也將進一步提升系統開銷。

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖七:Redis 樂觀鎖機制下的庫存扣減方案

方案三,採用 Redis 的樂觀鎖機制。樂觀鎖與悲觀鎖的區別在於,當多個線程同時訪問某個資源時,樂觀鎖並不會阻滯未得到鎖的線程對資源的訪問。但是更新數據是,只有版本號符合的請求才能夠成功更新緩存數據。一言以蔽之,樂觀鎖是一種只限制更新,不限制查詢的加鎖機制。我們此處以雙線程併發場景爲例:

當庫存爲 1 時,兩個用戶同時進入庫存校驗流程。此時用戶 A 先訪問庫存數據,並拿到庫存爲 1,當前版本號爲 10,通過校驗後,用戶 A 進入下單流程。此時用戶 B 訪問庫存數據,庫存爲 1,當前版本號爲 10,並進入下單流程。之後用戶 A 下單成功,庫存信息更新爲 0,版本號置爲 11。此時用戶 B 嘗試修改庫存信息,但拿到版本號信息爲 11,版本不符合,放棄更新庫存,回滾相關操作,並向用戶返回秒殺結束頁面。這一機制將能夠很好實現庫存數據在高併發場景下的線程安全問題,有效規避商品超售的情況。雖然這一方案會增加 CPU 開銷,但是相較於前兩種方案,在整體設計上更爲均衡,沒有明顯的短板,是最爲適合的一種庫存緩存設計方案。

(4)系統數據庫設計

鑑於秒殺系統對安全性和可用性的要求,在數據層設計上要儘可能細化和深化,分割業務數據,儘量避免一刀切的情況。因此在秒殺系統數據層將採用 8+1 型獨佔數據庫設計。

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖八:系統數據庫設計

首先我們按照作用域將業務數據切分爲全局數據和用戶數據,全局作用的數據,比如活動信息,商品信息,價格信息,所有用戶看到的都是一樣的。而用戶數據則是和用戶相關的差異性數據,比如用戶個人的訂單記錄等,更具體的說就是帶有 memberId 字段信息的數據。在這一數據分類的基礎上,進一步採用 8+1 型數據庫設計。所謂 8+1 指的是 8 個分庫組 +1 個單庫的設計,8 個分庫組只保存帶有 memberId 的用戶數據,並通過 MyCat 中間件按照 memberId 取模分片,而一個單庫中只保存活動信息,商品信息等全局數據,8 個分庫組採用獨佔型多活部署,而 1 個單庫採用競爭型多活部署,這一部分將在後文中詳細解釋。

系統多活部署與單機房宕機場景下的降級方案

1. 系統多活部署方案

就秒殺業務而言,系統的安全性和可用性無疑是第一考量,因此多活部署幾乎是一個必然的選擇。蘇寧秒殺中臺系統採用同城雙機房部署方案,一方面可以對用戶請求持續分流,同時也可以規避單點部署策略在意外因素下整機房宕機的風險,保證業務的持續可用性。

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖九:系統多活部署方案

在網絡層,CDN 首先對公網流量進行初次劃撥,正常情況下每個機房負載 1/2 流量。

在負載層,對於 CDN 調撥過來的公網流量,經過高可用 VIP 到達防火牆後,防火牆按分片策略二次切分。當 CDN 與防火牆的流量切分策略一致時,防火牆不會進行額外的流量劃撥(不帶分片路由信息的請求除外)。當 CDN 和防火牆切分策略不一致時,防火牆會進行補償性撥分,最終實際的流量調撥情況將遵循防火牆層面的撥分策略。

防火牆流量調度以系統爲基本單元,不同系統可根據實際情況配置分撥策略。除了對 CDN 初次調撥進行補償外,防火牆還可以承擔在 CDN 失效後的替代方案,保證不會因爲 CDN 失效而導致單機房負載壓力過大。

在應用層,所有 HTTP 請求,經由接口路由子模塊封裝後,進一步轉發到服務層和數據層。根據接口作用域的不同,採用主機房路由和分片路由的複合型策略來精準調度請求。對於涉及到全局數據的請求,比如活動新增,商品報名,庫存查詢,庫存更新等,採用主機房路由策略路。而對於用戶數據相關請求,則採取分片路由策略調撥到對應的獨佔庫。蘇寧分佈式調用中臺支持根據接口參數切分路由,這一規則可根據獨佔庫設置自行定製。

在數據層面,採用與應用層一致的獨佔型加競爭型複合部署策略,所有全局數據的讀寫請求均路由到機房 A 的單庫,並拓撲復制給機房 B。而用戶數據則採用交叉互備的分庫設計,機房 A 編號 1,2,3,4 的分庫爲主庫,編號 5,6,7,8 的庫爲從庫,機房 B 反之,數據通過數據庫服務中臺進行拓撲復制。

2. 單機房宕機場景下的降級方案

當發生單機房故障時(以機房 A 宕機爲例):

蘇寧高時效、高併發秒殺業務中臺的設計與實現
圖十:單機房宕機場景下的降級方案

在網絡層,由 CDN 統一控制將所有回源請求分撥到機房 B。

在負載層,此時機房 A 已經沒有來自公網的請求,但是可能仍然會有部分內網請求,因此需要修改內網 DNS 解析值,完成內網流量的調撥。同時需要在應用防火牆層面切換流量分撥策略,防止仍有流量被防火牆分撥到機房 A。

在應用層面,需要將分佈式調用中臺的主機房策略修改爲機房 B,同時將分片路由策略修改爲“機房 A 流量:機房 B 流量”爲“0:1”,將所有請求調度到機房 B。

在數據層面,需要將機房 B 單庫和編號 1,2,3,4 的分庫置爲主庫,修改拓撲復制關係。

至此,完成了機房 A 宕機情況下的降級,機房 B 將負載所有業務請求。

小結

就秒殺系統的設計而言,關鍵是要緊抓幾條設計原則,一是前端過濾,將大部分請求截流在上游緩存,減輕服務端壓力。二是高併發安全,在瞬時極高併發的情況下既要保證系統可用性,又要避免出現超售場景等業務異常。三是多活容災,冗餘部署,在系統風險較大的情況下要儘可能異地分流,均攤風險,提高系統抗災容災能力。只要抓住這幾點,那麼就掌握了秒殺系統設計的核心奧義。

作者介紹:

王翔,蘇寧科技集團消費者平臺研發中心工程師。畢業於安徽大學電子信息工程專業。目前致力於蘇寧隨時、蘇寧基礎、蘇寧拼購等系統多活方案設計及部署、系統架構拆分、後端性能優化、服務端開發等工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章