天天看點

如何設計一個秒殺系統 -- 架構原則

秒殺系統本質上就是一個滿足大并發,高性能和高可用的分布式系統。

架構原則:4 要 1 不要

1、資料要盡量少

首先是指使用者請求的資料能少就少。請求的資料包括上傳給系統的資料和系統傳回給使用者的資料。(網頁)

為什麼?因為首先這些資料在網絡上傳輸需要時間,其次不管是請求數還是傳回資料都需要伺服器做處理,而伺服器在寫網絡時通常都要做壓縮和字元編碼,這些都非常消耗CPU,是以減少傳輸的資料量可以顯著減少CPU的使用。例如,可以簡化秒殺頁面的大小,去掉不必要的頁面裝飾效果等。

其次,“資料要盡量少”還要求系統依賴的資料能少就少,包括系統完成某些業務邏輯需要讀取和儲存的資料,這些資料一般是和背景服務以及資料庫打交道的。調用其他服務會涉及資料的序列化和反序列化,這也是CPU的一大殺手,同時也會增加延時。而且,資料庫本身也會成為一個瓶頸,是以和資料庫打交道越少越好,資料越簡單,越小越好。

2、請求數要盡量少

使用者請求的頁面傳回後,浏覽器渲染這個頁面還要包括其他的額外請求,比如說,這個頁面依賴的CSS/JS,圖檔,以及Ajax請求等都定義為“額外請求”,這些額外請求應該盡量少。

因為浏覽器每發出一個請求,多少都會有一些消耗,例如建立連接配接需要三次握手,有的時候有頁面依賴或連接配接數限制,一些請求(JS)還要串行加載。另外不同請求的域名不一樣的話,還涉及這些域名的DNS解析,可能會耗時更久。是以減少請求書可以顯著減少以上這些因素導緻的資源消耗。

例如,減少請求數最常用的一個實踐就是合并 CSS 和 JavaScript 檔案,把多個 JavaScript 檔案合并成一個檔案,在 URL 中用逗号隔開(https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js)。這種方式在服務端仍然是單個檔案各自存放,隻是服務端會有一個元件解析這個 URL,然後動态把這些檔案合并起來一起傳回。

3、路徑要盡量短

所謂“路徑”,就是使用者送出請求到傳回資料這個過程中,需要經過的中間節點數。

通常,這些節點可以表示為一個系統或者一個新的Socket連接配接(比如代理伺服器隻是建立一個新的Socket連接配接來轉發請求)。每經過一個節點,一般都會産生一個新的Socket連接配接。

然而,每增加一個連接配接都會增加新的不确定性。從機率統計上來說,加入一次請求經過5個節點,每個節點的可用性為99.9%的話,那麼整個請求的可用性就是:99.9%的5次方,約等于99.5%。

是以縮短請求路徑不僅可以增加可用性,同樣可以有效提升性能(減少中間節點可以減少資料的序列化和反序列化),并減少延時(可以減少網絡傳輸耗時)。

要縮短通路路徑有一種辦法,就是多個互相強依賴的應用合并部署在一起,把遠端過程調用(RPC)變成JVM内部之間的方法調用。

4、依賴要盡量少

所謂依賴,指的是要完成一次使用者請求必須依賴的系統或服務,指的是強依賴。

舉個例子,比如說你要展示秒殺頁面,而這個頁面必須強依賴商品資訊,使用者資訊,還有其他如優惠券,成交清單等這些對秒殺不是非要不可的資訊(弱依賴),這些弱依賴在緊急情況下就可以去掉。

要減少依賴,我們可以給系統進行分級,比如0級系統,1級系統,2級系統,3級系統。如果0級系統是最重要的系統,那麼0級系統強依賴的系統也同樣是最重要的系統。

注意,0級系統要盡量減少對1級系統的強依賴,防止重要的系統被不重要的系統拖垮。例如支付系統是0級系統,而優惠券是1級的話,在極端情況下可以把優惠券降級,防止支付系統被優惠券這個1級系統給拖垮。

5、不要有單點

系統中的單點可以說是系統架構上的一個大忌,因為單點意味着沒有備份,風險不可控,我們設計分布式系統最重要的原則就是“消除單點”。

那如何避免單點呢?我認為最關鍵的點是避免将服務的狀态和機器綁定,即把服務無狀态化,這樣服務就可以在機器中随意移動。

如何把服務的狀态和機器解耦呢?這裡也有很多實作方式,例如把和機器相關的配置動态化,這些參數可以通過配置中心來動态推送,在服務啟動時動态拉取下來,我們在這些配置中心設定一些規則來友善的改變這些映射關系。

應用無狀态化是有效避免單點的一種方式,但是像存儲服務本身很難無狀态化,因為資料要存儲在磁盤上,本身就要和機器綁定,那麼這種場景一般要通過備援多個備份的方式來解決單點問題。

架構是一種平衡的藝術,而最好的架構一旦脫離了它所适應的場景,一切都将是空談。

不同場景下的不同架構案例:

如果快速搭建一個簡單的秒殺系統,隻需要把你的商品購買頁面增加一個“定時上架”的功能,僅在秒殺開始時才讓使用者看到購買按鈕,當商品的庫存賣完了也就結束了。

但随請求量從1w/s達到10w/s的量級,這個簡單的架構很快就遇到了瓶頸,是以需要做架構改造來提升性能:

  1. 把秒殺系統獨立出來單獨打造一個系統,這樣可以有針對性地做優化,例如這個獨立出來的系統就減少了店鋪裝修的功能,減少了頁面的複雜度。
  2. 在系統部署上也獨立做一個機器叢集,這樣秒殺的大流量就不會影響到正常的商品購買叢集的機器負載。
  3. 将熱點資料(庫存資料)單獨放到一個緩存系統中,以提高“讀性能”。
  4. 增加秒殺答題,防止有秒殺器搶單。
如何設計一個秒殺系統 -- 架構原則

然而這個架構支援不了超過100w/s的請求量,是以進一步更新系統:

5. 對頁面進行徹底的動靜分離,使得使用者秒殺時不需要重新整理整個頁面,而隻需要點選購買按鈕,借此把頁面重新整理的資料降到最少。

6. 在服務端對秒殺商品進行本地緩存,不需要再調用依賴系統的背景服務擷取資料,甚至不需要去公共的緩存叢集中查詢資料,這樣不僅可以減少系統調用,而且能夠避免壓垮公共緩存叢集。

7. 增加系統限流保護,防止最壞情況發生。

如何設計一個秒殺系統 -- 架構原則

在這裡,我們對頁面進行了進一步的靜态化,秒殺過程中不需要重新整理整個頁面,而隻需要向服務端請求很少的動态資料。而且,最關鍵的詳情和交易系統都增加了本地緩存,來提前緩存秒殺商品的資訊,熱點資料庫也做了獨立部署,等等。

一些疑問?

1、不同的QPS量級瓶頸是不一樣的,10w的瓶頸可能在資料讀取上,通常增加緩存就能解決。100w可能服務端的網絡都會是瓶頸,是以要把大部分的靜态資料放到CDN上甚至緩存在浏覽器裡。

2、本地緩存用什麼實作好?通過什麼方式向本地cache寫資料?

本地cache一般就是用記憶體實作,用Java集合類型就可以了。

用訂閱的方式,在初始化時加載到記憶體。

3、秒殺系統的及時性非常高,把庫存寫進cache,怎麼及時更新?

有兩種方法,一是定時更新取3秒,二是主動更新,資料庫字段更新後發消息更新緩存,這個需要用到一個元件阿裡叫metaq,就是資料庫字段更新會産生一條消息。另外cache裡庫存不需要100%和資料庫一緻,這個在後面的文章也有介紹。

4、秒殺的時間怎麼控制的,各個用戶端,伺服器之間如何精确同步?

以服務端的時間為準,服務端的時間同步需要依賴一個時間同步元件完成,如ntp。當然還是有一定延時,影響不大。

5、庫存不會放在本地緩存localcache,localcache隻放靜态資料,如商品描述這類。

庫存是放在獨立的緩存系統裡,如redis,庫存是采用主動失效的方式來失效緩存。

繼續閱讀