設計秒殺系統時應該注意的5個架構原則
說起秒殺,我想你肯定不陌生,這兩年,從雙十一購物到春節搶紅包,再到 12306 搶火車 票,「秒殺」的場景處處可見。簡單來說,秒殺就是在同一個時刻有大量的請求爭搶購買同一個 商品並完成交易的過程,用技術的行話來說就是大量的並發讀和並發寫。
不管是哪一門語言,並發都是程序員們最為頭疼的部分。同樣,對於一個軟體而言也是這樣,你 可以很快增刪改查做出一個秒殺系統,但是要讓它支持高並發訪問就沒那麼容易了。比如說,如 何讓系統面對百萬級的請求流量不出故障?如何保證高並發情況下數據的一致性寫?完全靠堆服 務器來解決嗎?這顯然不是最好的解決方案。
在我看來,秒殺系統本質上就是一個滿足大並發、高性能和高可用的分散式系統。今天,我們就 來聊聊,如何在滿足一個良好架構的分散式系統基礎上,針對秒殺這種業務做到極致的性能改 進。
架構原則:「4 要 1 不要」
如果你是一個架構師,你首先要勾勒出一個輪廓,想一想如何構建一個超大流量並發讀寫、高性 能,以及高可用的系統,這其中有哪些要素需要考慮。我把這些要素總結為「4 要 1 不要」。
1. 數據要盡量少
所謂「數據要盡量少」,首先是指用戶請求的數據能少就少。請求的數據包括上傳給系統的數據 和系統返回給用戶的數據(通常就是網頁)。
為啥「數據要盡量少」呢?因為首先這些數據在網路上傳輸需要時間,其次不管是請求數據還是 返回數據都需要伺服器做處理,而伺服器在寫網路時通常都要做壓縮和字元編碼,這些都非常消 耗 CPU,所以減少傳輸的數據量可以顯著減少 CPU 的使用。例如,我們可以簡化秒殺頁面的大 小,去掉不必要的頁面裝修效果,等等。
其次,「數據要盡量少」還要求系統依賴的數據能少就少,包括系統完成某些業務邏輯需要讀取 和保存的數據,這些數據一般是和後台服務以及資料庫打交道的。調用其他服務會涉及數據的序 列化和反序列化,而這也是 CPU 的一大殺手,同樣也會增加延時。而且,資料庫本身也容易成 為一個瓶頸,所以和資料庫打交道越少越好,數據越簡單、越小則越好。
2. 請求數要盡量少
用戶請求的頁面返回後,瀏覽器渲染這個頁面還要包含其他的額外請求,比如說,這個頁面依賴 的 CSS/JavaScript、圖片,以及 Ajax 請求等等都定義為「額外請求」,這些額外請求應該盡量 少。因為瀏覽器每發出一個請求都多少會有一些消耗,例如建立連接要做三次握手,有的時候有 頁面依賴或者連接數限制,一些請求(例如 JavaScript)還需要串列載入等。另外,如果不同請 求的域名不一樣的話,還涉及這些域名的 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. 不要有單點
系統中的單點可以說是系統架構上的一個大忌,因為單點意味著沒有備份,風險不可控,我們設 計分散式系統最重要的原則就是「消除單點」。
那如何避免單點呢?我認為關鍵點是避免將服務的狀態和機器綁定,即把服務無狀態化,這樣服 務就可以在機器中隨意移動。
如何那把服務的狀態和機器解耦呢?這裡也有很多實現方式。例如把和機器相關的配置動態化, 這些參數可以通過配置中心來動態推送,在服務啟動時動態拉取下來,我們在這些配置中心設置 一些規則來方便地改變這些映射關係。
應用無狀態化是有效避免單點的一種方式,但是像存儲服務本身很難無狀態化,因為數據要存儲 在磁碟上,本身就要和機器綁定,那麼這種場景一般要通過冗餘多個備份的方式來解決單點問 題。
前面介紹了這些設計上的一些原則,但是你有沒有發現,我一直說的是「盡量」而不是「絕 對」?
我想你肯定會問是不是請求最少就一定最好,我的答案是「不一定」。我們曾經把有些 CSS 內聯 進頁面里,這樣做可以減少依賴一個 CSS 的請求從而加快首頁的渲染,但是同樣也增大了頁面的 大小,又不符合「數據要盡量少」的原則,這種情況下我們為了提升首屏的渲染速度,只把首屏 的 HTML 依賴的 CSS 內聯進來,其他 CSS 仍然放到文件中作為依賴載入,盡量實現首屏的打開 速度與整個頁面載入性能的平衡
所以說,架構是一種平衡的藝術,而最好的架構一旦脫離了它所適應的場景,一切都將是空談。 我希望你記住的是,這裡所說的幾點都只是一個個方向,你應該盡量往這些方向上去努力,但也 要考慮平衡其他因素。
不同場景下的不同架構案例
前面我說了一些架構上的原則,那麼針對「秒殺」這個場景,怎樣才是一個好的架構呢?下面我 以淘寶早期秒殺系統架構的演進為主線,來幫你梳理不同的請求體量下,我認為的最佳秒殺系統 架構。
如果你想快速搭建一個簡單的秒殺系統,只需要把你的商品購買頁面增加一個「定時上架」功 能,僅在秒殺開始時才讓用戶看到購買按鈕,當商品的庫存賣完了也就結束了。這就是當時第一 個版本的秒殺系統實現方式
但隨著請求量的加大(比如從 1w/s 到了 10w/s 的量級),這個簡單的架構很快就遇到了瓶頸, 因此需要做架構改造來提升系統性能。這些架構改造包括:
1. 把秒殺系統獨立出來單獨打造一個系統,這樣可以有針對性地做優化,例如這個獨立出來的系 統就減少了店鋪裝修的功能,減少了頁面的複雜度; 2. 在系統部署上也獨立做一個機器集群,這樣秒殺的大流量就不會影響到正常的商品購買集群的 機器負載; 3. 將熱點數據(如庫存數據)單獨放到一個緩存系統中,以提高「讀性能」; 4. 增加秒殺答題,防止有秒殺器搶單。
此時的系統架構變成了下圖這個樣子。最重要的就是,秒殺詳情成為了一個獨立的新系統,另外 核心的一些數據放到了緩存(Cache)中,其他的關聯繫統也都以獨立集群的方式進行部署。