1. Overview
1.1 并發(fā)讀寫
秒殺要解決的主要問題是:并發(fā)讀與并發(fā)寫。
并發(fā)讀的優(yōu)化理念是盡量減少用戶到服務(wù)端來讀數(shù)據(jù),或者讓他們讀更少的數(shù)據(jù);并發(fā)寫的處理原則一樣,要求我們在數(shù)據(jù)庫層面獨立出一個庫,做特殊的處理。
其次,還需要針對秒殺系統(tǒng)做一些保護,針對意料之外的情況設(shè)計兜底方案,以防止最壞的情況發(fā)生。
1.2 API設(shè)計原則
值得注意的地方是:如果想打造并維護一個超大流量并發(fā)讀寫、高性能、高可用的系統(tǒng),在整個用戶請求路徑上從瀏覽器到服務(wù)端我們要遵循幾個原則,就是保證**用戶請求的數(shù)據(jù)盡量少、請求數(shù)盡量少、路徑盡量短、依賴盡量少,不要有單點**
1.3 秒殺架構(gòu)原則
1.3.1 高可用
整個系統(tǒng)架構(gòu)需要滿足高可用性,流量符合預(yù)期的時候肯定要穩(wěn)定,就是超出預(yù)期也同樣不能掉鏈子,保證秒殺產(chǎn)品順利賣出。
1.3.2 一致性
數(shù)據(jù)必須一致,即成交總量必須和設(shè)定的數(shù)量一致。
1.3.3 高可用
系統(tǒng)的性能要足夠強,支撐足夠大的流量,不僅是服務(wù)端要做極致的性能優(yōu)化,而且在整個請求鏈路上都要做協(xié)同的優(yōu)化,每個地方都要快一點,整個系統(tǒng)就完美了。
本文將從這三個原則上來分別進行詳細說明。
2. 架構(gòu)原則
秒殺系統(tǒng)本質(zhì)上是一個滿足大并發(fā)、高性能和高可用的分布式系統(tǒng)。
2.1 數(shù)據(jù)盡量少
用戶請求的數(shù)據(jù)能少就少,請求的數(shù)據(jù)包括上傳給系統(tǒng)的數(shù)據(jù)和系統(tǒng)返回給用戶的數(shù)據(jù)。
因為這些數(shù)據(jù)在網(wǎng)絡(luò)上傳輸需要時間,其次不管是請求數(shù)據(jù)還是返回數(shù)據(jù)都需要服務(wù)器處理,而服務(wù)器在寫網(wǎng)絡(luò)的時候通常都要做壓縮和字符編碼,這些都非常消耗CPU,所以減少傳輸?shù)臄?shù)據(jù)量可以顯著減少CPU的使用。
同樣,數(shù)據(jù)盡量少還要求系統(tǒng)依賴的數(shù)據(jù)能少就少,包括系統(tǒng)完成某些業(yè)務(wù)邏輯需要讀取和保存的數(shù)據(jù),這些數(shù)據(jù)一般是和后臺服務(wù)以及數(shù)據(jù)庫打交道的。調(diào)用其他服務(wù)會涉及數(shù)據(jù)的序列化和反序列化,這也是CPU的一大殺手,同樣也會增加延時。而且數(shù)據(jù)庫本身也很容易成為瓶頸,因此越少和數(shù)據(jù)庫打交道越好。
2.2 請求數(shù)盡量少
用戶請求的頁面返回后,瀏覽器渲染這個頁面還要包含其他的額外請求,比如說,這個頁面依賴的 CSS/JavaScript、圖片,以及 Ajax 請求等等都定義為“額外請求”,這些額外請求應(yīng)該盡量少。因為瀏覽器每發(fā)出一個請求都多少會有一些消耗,例如建立連接要做三次握手,有的時候有頁面依賴或者連接數(shù)限制,一些請求(例如 JavaScript)還需要串行加載等。另外,如果不同請求的域名不一樣的話,還涉及這些域名的 DNS 解析,可能會耗時更久。所以你要記住的是,減少請求數(shù)可以顯著減少以上這些因素導(dǎo)致的資源消耗。
例如,減少請求數(shù)最常用的一個實踐就是合并 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](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))。這種方式在服務(wù)端仍然是單個文件各自存放,只是服務(wù)端會有一個組件解析這個 URL,然后動態(tài)把這些文件合并起來一起返回。
2.3 路徑要盡量短
路徑指的是用戶發(fā)出請求到返回數(shù)據(jù)這個過程中需要經(jīng)過的中間節(jié)點的數(shù)量。
通常,這些節(jié)點可以表示為一個系統(tǒng)或者一個新的 Socket 連接(比如代理服務(wù)器只是創(chuàng)建一個新的 Socket 連接來轉(zhuǎn)發(fā)請求)。每經(jīng)過一個節(jié)點,一般都會產(chǎn)生一個新的 Socket 連接。
然而,每增加一個連接都會增加新的不確定性。從概率統(tǒng)計上來說,假如一次請求經(jīng)過 5 個節(jié)點,每個節(jié)點的可用性是 99.9% 的話,那么整個請求的可用性是:99.9% 的 5 次方,約等于 99.5%。
所以縮短請求路徑不僅可以增加可用性,同樣可以有效提升性能(減少中間節(jié)點可以減少數(shù)據(jù)的序列化與反序列化),并減少延時(可以減少網(wǎng)絡(luò)傳輸耗時)。
要縮短訪問路徑可以將多個相互有強依賴的應(yīng)用合并部署在一起,將遠程過程調(diào)用變成JVM內(nèi)部的方法調(diào)用。
2.4 依賴要盡量少
所謂依賴,指的是要完成一次用戶請求必須依賴的系統(tǒng)或者服務(wù)。
舉個例子,比如說你要展示秒殺頁面,而這個頁面必須強依賴商品信息、用戶信息,還有其他如優(yōu)惠券、成交列表等這些對秒殺不是非要不可的信息(弱依賴),這些弱依賴在緊急情況下就可以去掉。
要減少依賴,我們可以給系統(tǒng)進行分級,比如 0 級系統(tǒng)、1 級系統(tǒng)、2 級系統(tǒng)、3 級系統(tǒng),0 級系統(tǒng)如果是最重要的系統(tǒng),那么 0 級系統(tǒng)強依賴的系統(tǒng)也同樣是最重要的系統(tǒng),以此類推。
注意,0 級系統(tǒng)要盡量減少對 1 級系統(tǒng)的強依賴,防止重要的系統(tǒng)被不重要的系統(tǒng)拖垮。例如支付系統(tǒng)是 0 級系統(tǒng),而優(yōu)惠券是 1 級系統(tǒng)的話,在極端情況下可以把優(yōu)惠券給降級,防止支付系統(tǒng)被優(yōu)惠券這個 1 級系統(tǒng)給拖垮。
2.5 不要有單點
不能有單點,因為單點意味著沒有備份,風(fēng)險不可控,設(shè)計分布式系統(tǒng)的一個最重要的原則就是消除單點。
如何避免單點?—-> 避免將服務(wù)的狀態(tài)和機器綁定,即把服務(wù)無狀態(tài)化,這樣服務(wù)就可以在機器中隨意移動了。
如何那把服務(wù)的狀態(tài)和機器解耦呢?這里也有很多實現(xiàn)方式。例如把和機器相關(guān)的配置動態(tài)化,這些參數(shù)可以通過配置中心來動態(tài)推送,在服務(wù)啟動時動態(tài)拉取下來,我們在這些配置中心設(shè)置一些規(guī)則來方便地改變這些映射關(guān)系。
應(yīng)用無狀態(tài)化是有效避免單點的一種方式,但是像存儲服務(wù)本身很難無狀態(tài)化,因為數(shù)據(jù)要存儲在磁盤上,本身就要和機器綁定,那么這種場景一般要通過冗余多個備份的方式來解決單點問題。
3. 不同場景下的不同架構(gòu)案例
如果你想快速搭建一個簡單的秒殺系統(tǒng),只需要把你的商品購買頁面增加一個“定時上架”功能,僅在秒殺開始時才讓用戶看到購買按鈕,當(dāng)商品的庫存賣完了也就結(jié)束了。這就是當(dāng)時第一個版本的秒殺系統(tǒng)實現(xiàn)方式。
但隨著請求量的加大(比如從 1w/s 到了 10w/s 的量級),這個簡單的架構(gòu)很快就遇到了瓶頸,因此需要做架構(gòu)改造來提升系統(tǒng)性能。這些架構(gòu)改造包括:
把秒殺系統(tǒng)獨立出來單獨打造一個系統(tǒng),這樣可以有針對性地做優(yōu)化,例如這個獨立出來的系統(tǒng)就減少了店鋪裝修的功能,減少了頁面的復(fù)雜度;
在系統(tǒng)部署上也獨立做一個機器集群,這樣秒殺的大流量就不會影響到正常的商品購買集群的機器負載;
將熱點數(shù)據(jù)(如庫存數(shù)據(jù))單獨放到一個緩存系統(tǒng)中,以提高“讀性能”;
增加秒殺答題,防止有秒殺器搶單。
此時秒殺已經(jīng)成為了一個獨立的新系統(tǒng),另外核心的一些數(shù)據(jù)放到了緩存當(dāng)中,其他的關(guān)聯(lián)系統(tǒng)也都以獨立集群的方式進行部署。
fig1.jpg
但是這個架構(gòu)仍然無法支持超過100w/s的請求量,因此為了進一步提高秒殺系統(tǒng)的性能,又對架構(gòu)做了進一步的升級,比如:
對頁面進行徹底的動靜分離,使得用戶秒殺時不需要刷新整個頁面,而只需要點擊搶寶按鈕,借此把頁面刷新的數(shù)據(jù)降到最少;
在服務(wù)端對秒殺商品進行本地緩存,不需要再調(diào)用依賴系統(tǒng)的后臺服務(wù)獲取數(shù)據(jù),甚至不需要去公共的緩存集群中查詢數(shù)據(jù),這樣不僅可以減少系統(tǒng)調(diào)用,而且能夠避免壓垮公共緩存集群。
增加系統(tǒng)限流保護,防止最壞的情況發(fā)生
此時整個系統(tǒng)架構(gòu)變成了這個樣子,已經(jīng)對頁面進行了進一步的靜態(tài)化,秒殺過程當(dāng)中就不需要刷新整個頁面了,只需要向服務(wù)端請求很少的動態(tài)數(shù)據(jù)。而且最關(guān)鍵的詳情和交易系統(tǒng)都增加了本地緩存,來提前緩存秒殺商品的信息,熱點數(shù)據(jù)庫也做了獨立部署。
fig2.jpg
從前面的幾次升級來看,其實越到后面需要定制的地方越多,也就是越“不通用”。例如,把秒殺商品緩存在每臺機器的內(nèi)存中,這種方式顯然不適合太多的商品同時進行秒殺的情況,因為單機的內(nèi)存始終有限。所以要取得極致的性能,就要在其他地方(比如,通用性、易用性、成本等方面)有所犧牲。
4. 動靜分離的方案
秒殺系統(tǒng)需要讓請求效率足夠高 - 提高單次請求的效率,減少沒必要的請求。
4.1 何為動靜數(shù)據(jù)
將用戶請求的數(shù)據(jù)(如HTML)劃分為動態(tài)數(shù)據(jù)和靜態(tài)數(shù)據(jù)。而動態(tài)靜態(tài)數(shù)據(jù)的劃分,在于看頁面中輸出的數(shù)據(jù)是否和URL,瀏覽者,時間,地域相關(guān),以及是否含有Cookie等私密數(shù)據(jù)。
對很多媒體類的網(wǎng)站來說,無論誰來看文章,展示的數(shù)據(jù)都是一樣的,那么哪怕這是個動態(tài)頁面,它仍然是個典型的靜態(tài)數(shù)據(jù)。
訪問淘寶的首頁,每個人看到的頁面可能都是不一樣的,其中包含了很多根據(jù)訪問者個人信息進行的推薦,這些個性化的數(shù)據(jù)就稱為動態(tài)數(shù)據(jù)。
這里再強調(diào)一下,我們所說的靜態(tài)數(shù)據(jù),不能僅僅理解為傳統(tǒng)意義上完全存在磁盤上的 HTML 頁面,它也可能是經(jīng)過 Java 系統(tǒng)產(chǎn)生的頁面,但是它輸出的頁面本身不包含上面所說的那些因素。也就是所謂“動態(tài)”還是“靜態(tài)”,并不是說數(shù)據(jù)本身是否動靜,而是數(shù)據(jù)中是否含有和訪問者相關(guān)的個性化數(shù)據(jù)。
這樣做動靜分離的時候,我們就可以對分離出來的靜態(tài)數(shù)據(jù)做緩存,有了緩存以后,靜態(tài)數(shù)據(jù)的訪問效率肯定就提高了。
4.2 如何對靜態(tài)數(shù)據(jù)做緩存?
4.2.1 距離用戶最近
將靜態(tài)數(shù)據(jù)緩存到離用戶最近的地方。靜態(tài)數(shù)據(jù)就是那些相對不會變化的數(shù)據(jù),因此可以做緩存。常見的,我們可以緩存在:
用戶瀏覽器
CDN上
服務(wù)端的Cache中
4.2.2 靜態(tài)化改造要直接緩存HTTP連接
系統(tǒng)的靜態(tài)化改造是直接緩存HTTP連接而不僅僅是數(shù)據(jù)了。如下圖所示,Web代理服務(wù)器根據(jù)請求URL直接去除對應(yīng)的HTTP響應(yīng)頭和響應(yīng)體然后直接返回,這個響應(yīng)過程連HTTP協(xié)議都不用重新組裝,甚至連HTTP請求頭也不需要解析。
fig3.jpg
4.2.3 緩存語言
不同語言寫的cache軟件處理緩存數(shù)據(jù)的效率也各不相同。以Java為例,Java不擅長處理大量連接請求,每個連接消耗的內(nèi)存會比較多,Servlet容器解析HTTP協(xié)議比較慢。所以可以不在Java層做緩存,而是直接在Web服務(wù)器層上做,這樣就可以屏蔽Java的一些弱點;而相比起來,Web服務(wù)器(Nginx, Apache, Varnish)會更加擅長處理大并發(fā)的靜態(tài)文件請求。
4.3 靜態(tài)數(shù)據(jù)處理方案
以商品詳情頁為例:
4.3.1 URL唯一化
要緩存整個HTTP連接,以URL作為緩存的key
4.3.2 分離瀏覽者相關(guān)的因素
分離用戶的相關(guān)信息,是否登錄以及登錄身份等等。
4.3.3 分離時間因素
服務(wù)端輸出的是哪也通過動態(tài)請求獲取
4.3.4 異步化地域因素
詳情頁面上與地域相關(guān)的因素做成異步獲取的方式
4.3.5 去掉Cookie
服務(wù)端輸出的頁面包含的 Cookie 可以通過代碼軟件來刪除,如 Web 服務(wù)器 Varnish 可以通過 unset req.http.cookie 命令去掉 Cookie。注意,這里說的去掉 Cookie 并不是用戶端收到的頁面就不含 Cookie 了,而是說,在緩存的靜態(tài)數(shù)據(jù)中不含有 Cookie。
4.4 動態(tài)數(shù)據(jù)處理方案
4.4.1 ESI (Edge Side Includes)
在Web代理服務(wù)器上做動態(tài)內(nèi)容請求,并將請求插入到靜態(tài)頁面中,當(dāng)用戶拿到頁面時已經(jīng)是一個完整的頁面了。對服務(wù)端性能有影響,但是用戶體驗會比較好
4.4.2 CSI (Client Side Include)
單獨發(fā)出異步Javascript請求,向服務(wù)端獲取動態(tài)內(nèi)容。這種方式服務(wù)端性能更好,但是用戶端可能會有延時,體驗會差一些
4.5 動靜分離架構(gòu)方案
4.5.1 實體機單機部署
這種方案是將虛擬機改為實體機,以增大 Cache 的容量,并且采用了一致性 Hash 分組的方式來提升命中率。這里將 Cache 分成若干組,是希望能達到命中率和訪問熱點的平衡。Hash 分組越少,緩存的命中率肯定就會越高,但短板是也會使單個商品集中在一個分組中,容易導(dǎo)致 Cache 被擊穿,所以我們應(yīng)該適當(dāng)增加多個相同的分組,來平衡訪問熱點和命中率的問題。
Nginx+Cache+Java結(jié)構(gòu)實體機單機部署
fig4.jpg
這種部署方式有以下幾個優(yōu)點:
沒有網(wǎng)絡(luò)瓶頸,而且能使用大內(nèi)存;
既能提升命中率,又能減少 Gzip 壓縮;
減少 Cache 失效壓力,因為采用定時失效方式,例如只緩存 3 秒鐘,過期即自動失效。
這個方案中,雖然把通常只需要虛擬機或者容器運行的 Java 應(yīng)用換成實體機,優(yōu)勢很明顯,它會增加單機的內(nèi)存容量,但是一定程度上也造成了 CPU 的浪費,因為單個的 Java 進程很難用完整個實體機的 CPU。
另外就是,一個實體機上部署了 Java 應(yīng)用又作為 Cache 來使用,這造成了運維上的高復(fù)雜度,所以這是一個折中的方案。如果你的公司里,沒有更多的系統(tǒng)有類似需求,那么這樣做也比較合適,如果你們有多個業(yè)務(wù)系統(tǒng)都有靜態(tài)化改造的需求,那還是建議把 Cache 層單獨抽出來公用比較合理,如下面的方案 2 所示。
4.5.2 統(tǒng)一Cache層
所謂統(tǒng)一 Cache 層,就是將單機的 Cache 統(tǒng)一分離出來,形成一個單獨的 Cache 集群。統(tǒng)一 Cache 層是個更理想的可推廣方案,該方案的結(jié)構(gòu)圖如下:
fig5.jpg
統(tǒng)一Cache層,可以減少運維成本,也方便接入其他靜態(tài)化系統(tǒng),還有以下優(yōu)點:
單獨一個 Cache 層,可以減少多個應(yīng)用接入時使用 Cache 的成本。這樣接入的應(yīng)用只要維護自己的 Java 系統(tǒng)就好,不需要單獨維護 Cache,而只關(guān)心如何使用即可。
統(tǒng)一 Cache 的方案更易于維護,如后面加強監(jiān)控、配置的自動化,只需要一套解決方案就行,統(tǒng)一起來維護升級也比較方便。
可以共享內(nèi)存,最大化利用內(nèi)存,不同系統(tǒng)之間的內(nèi)存可以動態(tài)切換,從而能夠有效應(yīng)對各種攻擊。
這種方案也會帶來一些問題。比如:
Cache 層內(nèi)部交換網(wǎng)絡(luò)成為瓶頸;
緩存服務(wù)器的網(wǎng)卡也會是瓶頸;
機器少風(fēng)險較大,掛掉一臺就會影響很大一部分緩存數(shù)據(jù)。
要解決上面這些問題,可以再對 Cache 做 Hash 分組,即一組 Cache 緩存的內(nèi)容相同,這樣能夠避免熱點數(shù)據(jù)過度集中導(dǎo)致新的瓶頸產(chǎn)生。
4.5.3 使用CDN
在將整個系統(tǒng)做動靜分離后,我們自然會想到更進一步的方案,就是將 Cache 進一步前移到 CDN 上,因為 CDN 離用戶最近,效果會更好。
有幾個問題需要解決:
失效問題
前面我們也有提到過緩存時效的問題,不知道你有沒有理解,我再來解釋一下。談到靜態(tài)數(shù)據(jù)時,我說過一個關(guān)鍵詞叫“相對不變”,它的言外之意是“可能會變化”。比如一篇文章,現(xiàn)在不變,但如果你發(fā)現(xiàn)個錯別字,是不是就會變化了?如果你的緩存時效很長,那用戶端在很長一段時間內(nèi)看到的都是錯的。所以,這個方案中也是,我們需要保證 CDN 可以在秒級時間內(nèi),讓分布在全國各地的 Cache 同時失效,這對 CDN 的失效系統(tǒng)要求很高。
命中率問題
Cache 最重要的一個衡量指標(biāo)就是“高命中率”,不然 Cache 的存在就失去了意義。同樣,如果將數(shù)據(jù)全部放到全國的 CDN 上,必然導(dǎo)致 Cache 分散,而 Cache 分散又會導(dǎo)致訪問請求命中同一個 Cache 的可能性降低,那么命中率就成為一個問題。
發(fā)布更新問題
如果一個業(yè)務(wù)系統(tǒng)每周都有日常業(yè)務(wù)需要發(fā)布,那么發(fā)布系統(tǒng)必須足夠簡潔高效,而且你還要考慮有問題時快速回滾和排查問題的簡便性。
從前面的分析來看,將商品詳情系統(tǒng)放到全國的所有 CDN 節(jié)點上是不太現(xiàn)實的,因為存在失效問題、命中率問題以及系統(tǒng)的發(fā)布更新問題。那么是否可以選擇若干個節(jié)點來嘗試實施呢?答案是“可以”,但是這樣的節(jié)點需要滿足幾個條件:
靠近訪問量比較集中的地區(qū)
離主站相對較遠
節(jié)點到主站間的網(wǎng)絡(luò)比較好,比較穩(wěn)定
節(jié)點容量大,不會占用其他CDN太多的資源
基于上面幾個因素,選擇 CDN 的二級 Cache 比較合適,因為二級 Cache 數(shù)量偏少,容量也更大,讓用戶的請求先回源的 CDN 的二級 Cache 中,如果沒命中再回源站獲取數(shù)據(jù),部署方式如下圖所示:
fig6.jpg
使用 CDN 的二級 Cache 作為緩存,可以達到和當(dāng)前服務(wù)端靜態(tài)化 Cache 類似的命中率,因為節(jié)點數(shù)不多,Cache 不是很分散,訪問量也比較集中,這樣也就解決了命中率問題,同時能夠給用戶最好的訪問體驗,是當(dāng)前比較理想的一種 CDN 化方案。
5. 如何處理熱點數(shù)據(jù)
有一部分數(shù)據(jù)是會被大量用戶訪問的熱賣商品,這部分商品是需要特殊關(guān)注的,因為其會對系統(tǒng)產(chǎn)生一系列的影響。
首先,熱點請求會大量占用服務(wù)器處理資源,雖然這個熱點可能占總量的很小的一部分,然而卻可能搶占90%以上的服務(wù)器資源,如果這個熱點請求還是沒有價值的無效請求,那么對系統(tǒng)資源來說完全是浪費。
5.1 什么是熱點
5.1.1 熱點操作
例如大量的刷新頁面,大量添加購物車,零點大量的下單等。這些操作可以抽象為“讀請求”和“寫請求”,這兩種請求的處理方式大相徑庭,讀請求的優(yōu)化空間比較大,而寫請求的瓶頸一般都在存儲層,優(yōu)化的思路就是根據(jù)CAP理論做平衡。
5.1.2 熱點數(shù)據(jù)
熱點數(shù)據(jù)就是用戶的熱點請求對應(yīng)的數(shù)據(jù),又可以分為靜態(tài)熱點數(shù)據(jù)和動態(tài)熱點數(shù)據(jù)。
靜態(tài)熱點數(shù)據(jù),就是能夠提前預(yù)測的熱點數(shù)據(jù)。動態(tài)熱點數(shù)據(jù),就是不能被提前預(yù)測到的,系統(tǒng)在運行過程中臨時產(chǎn)生的熱點。
5.2 發(fā)現(xiàn)熱點數(shù)據(jù)
5.2.1 發(fā)現(xiàn)靜態(tài)熱點數(shù)據(jù)
如前面講的,靜態(tài)熱點數(shù)據(jù)可以通過商業(yè)手段,例如強制讓賣家通過報名參加的方式提前把熱點商品篩選出來,實現(xiàn)方式是通過一個運營系統(tǒng),把參加活動的商品數(shù)據(jù)進行打標(biāo),然后通過一個后臺系統(tǒng)對這些熱點商品進行預(yù)處理,如提前進行緩存。但是這種通過報名提前篩選的方式也會帶來新的問題,即增加賣家的使用成本,而且實時性較差,也不太靈活。
不過,除了提前報名篩選這種方式,你還可以通過技術(shù)手段提前預(yù)測,例如對買家每天訪問的商品進行大數(shù)據(jù)計算,然后統(tǒng)計出 TOP N 的商品,我們可以認為這些 TOP N 的商品就是熱點商品。
5.2.2 發(fā)現(xiàn)動態(tài)熱點數(shù)據(jù)
具體實現(xiàn)
構(gòu)建異步系統(tǒng),用來收集交易鏈路上各個環(huán)節(jié)中的中間件產(chǎn)品的熱點Key,例如Nginx、緩存、RPC服務(wù)框架
建立一個熱點上報和可以按照需求訂閱的熱點服務(wù)的下發(fā)規(guī)范。因為交易鏈路上各個系統(tǒng)(包括詳情,購物車,交易,優(yōu)惠,庫存等等)會有訪問上的時間差,需要將上游已經(jīng)發(fā)現(xiàn)的熱點透傳給下游系統(tǒng),提前做好保護。例如,對于大促高峰期,詳情系統(tǒng)是最早知道的。
將上游系統(tǒng)收集的熱點數(shù)據(jù)發(fā)送到熱點服務(wù)臺,讓下游系統(tǒng)提前知道信息,做熱電保護
fig7.jpg
我們通過部署在每臺機器上的 Agent 把日志匯總到聚合和分析集群中,然后把符合一定規(guī)則的熱點數(shù)據(jù),通過訂閱分發(fā)系統(tǒng)再推送到相應(yīng)的系統(tǒng)中。你可以是把熱點數(shù)據(jù)填充到 Cache 中,或者直接推送到應(yīng)用服務(wù)器的內(nèi)存中,還可以對這些數(shù)據(jù)進行攔截,總之下游系統(tǒng)可以訂閱這些數(shù)據(jù),然后根據(jù)自己的需求決定如何處理這些數(shù)據(jù)。
Tips:
熱點服務(wù)的后臺抓取熱點數(shù)據(jù)日志的方式最好采用異步的方式;可以保證通過性,不會影響業(yè)務(wù)系統(tǒng)和中間件產(chǎn)品的主流程。
熱點服務(wù)和中間件自身需要有熱電保護模塊,每個中間件和應(yīng)用和需要保護自己
熱點發(fā)現(xiàn)需要接近實時,因為只有接近實時才有意義,能及時對下游系統(tǒng)提供保護
5.3 如何處理熱點數(shù)據(jù)
5.3.1 優(yōu)化
緩存熱點數(shù)據(jù),如果熱點數(shù)據(jù)做了動靜分離,那么可以長期緩存靜態(tài)數(shù)據(jù)。
5.3.2 限制
保護機制,比如對商品的ASIN做一致性hash,然后根據(jù)hash做分桶,每個分桶處置一個處理隊列,通過這種方式將熱點商品限制在一個請求隊列當(dāng)中,防止因為某些熱點商品占用太多的服務(wù)器資源,而使得其他請求始終得不到服務(wù)器的處理資源。
5.3.3 隔離
將熱點數(shù)據(jù)隔離出來,針對熱點數(shù)據(jù)可以再做優(yōu)化
業(yè)務(wù)隔離 - 商業(yè)邏輯上運行上的隔離
系統(tǒng)隔離 - 運行時的隔離
數(shù)據(jù)隔離 - 單獨數(shù)據(jù)庫 Cache集群
6. 流量削峰
秒殺請求在時間上是高度集中于某一特定的時間點的,這樣一來會有一個特別高的流量峰值,它對資源的消耗是瞬時的。
但是對于秒殺這個場景來說,最終能夠搶到的商品的人數(shù)是固定的,并發(fā)讀越高,無效請求也就越多了。
從業(yè)務(wù)角度上來說,秒殺希望更多的人能夠參與進來,更多的人來刷新頁面,但是真正開始下單的時候,秒殺請求就不是越多越好了,可以設(shè)計一些規(guī)則,讓并發(fā)的請求更多的延緩,甚至我們可以過濾掉一些無效請求。
6.1 削峰的原因
我們知道服務(wù)器的處理資源是恒定的,你用或者不用它的處理能力都是一樣的,所以出現(xiàn)峰值的話,很容易導(dǎo)致忙到處理不過來,閑的時候卻又沒有什么要處理。但是由于要保證服務(wù)質(zhì)量,我們的很多處理資源只能按照忙的時候來預(yù)估,而這會導(dǎo)致資源的一個浪費。
削峰主要是為了能夠讓服務(wù)端處理變得更加平穩(wěn),也為了能夠節(jié)省服務(wù)器的資源成本。從秒殺這個場景來說,就是更多延緩用戶請求的發(fā)出,以便減少或者過濾掉一些無效請求,遵從請求數(shù)要盡量少的原則。
6.2 無損削峰方式
6.2.1 排隊
用消息隊列緩沖瞬時流量,將同步的直接調(diào)用轉(zhuǎn)換成異步的間接推送,中間通過一個隊列在一端承接瞬時的流量洪峰,在另外一端平滑地將信息推送出去。
fig8.jpg
但是如果流量峰值持續(xù)一段時間,超過了消息隊列的處理上限,還是會被壓垮的。
其他常見的排隊方式有:
利用線程池加鎖等待
先進先出、先進后出等常用的內(nèi)存排隊算法的實現(xiàn)
將請求序列化到文件當(dāng)中,然后再順序讀文件
6.2.2 答題
第一個目的是防止部分買家使用秒殺器在參加秒殺時作弊。2011 年秒殺非常火的時候,秒殺器也比較猖獗,因而沒有達到全民參與和營銷的目的,所以系統(tǒng)增加了答題來限制秒殺器。增加答題后,下單的時間基本控制在 2s 后,秒殺器的下單比例也大大下降。
第二個目的其實就是延緩請求,起到對請求流量進行削峰的作用,從而讓系統(tǒng)能夠更好地支持瞬時的流量高峰。這個重要的功能就是把峰值的下單請求拉長,從以前的 1s 之內(nèi)延長到 2s~10s。這樣一來,請求峰值基于時間分片了。這個時間的分片對服務(wù)端處理并發(fā)非常重要,會大大減輕壓力。而且,由于請求具有先后順序,靠后的請求到來時自然也就沒有庫存了,因此根本到不了最后的下單步驟,所以真正的并發(fā)寫就非常有限了。這種設(shè)計思路目前用得非常普遍,如當(dāng)年支付寶的“咻一咻”、微信的“搖一搖”都是類似的方式。
6.2.3 分層過濾
采用漏斗式的設(shè)計
fig9.jpg
假如請求分別經(jīng)過 CDN、前臺讀系統(tǒng)(如商品詳情系統(tǒng))、后臺系統(tǒng)(如交易系統(tǒng))和數(shù)據(jù)庫這幾層,那么:
大部分數(shù)據(jù)和流量在用戶瀏覽器或者 CDN 上獲取,這一層可以攔截大部分數(shù)據(jù)的讀取
經(jīng)過第二層(即前臺系統(tǒng))時數(shù)據(jù)(包括強一致性的數(shù)據(jù))盡量得走 Cache,過濾一些無效的請求
再到第三層后臺系統(tǒng),主要做數(shù)據(jù)的二次檢驗,對系統(tǒng)做好保護和限流,這樣數(shù)據(jù)量和請求就進一步減少
最后在數(shù)據(jù)層完成數(shù)據(jù)的強一致性校驗
分層過濾的核心思想是:在不同的層次盡可能地過濾掉無效請求,讓漏斗最末端的才是有效的請求。而達到這種效果,我們就必須對數(shù)據(jù)做分層的校驗。
分層校驗的基本原則有:
將動態(tài)請求的讀數(shù)據(jù)緩存在Web端,過濾掉無效的數(shù)據(jù)讀
對讀數(shù)據(jù)不做強一致性校驗,減少因為一致性校驗產(chǎn)生的瓶頸問題
對寫數(shù)據(jù)進行基于時間的合理分片,過濾掉過期的失效請求
對寫請求做限流保護,將超出系統(tǒng)承載能力的請求過濾掉
對寫數(shù)據(jù)進行強一致性校驗,只保留最后有效的數(shù)據(jù)
分層校驗的目的是:在讀系統(tǒng)中,盡量減少由于一致性校驗帶來的系統(tǒng)瓶頸,但是盡量將不影響性能的檢查條件提前,如用戶是否具有秒殺資格、商品狀態(tài)是否正常、用戶答題是否正確、秒殺是否已經(jīng)結(jié)束、是否非法請求、營銷等價物是否充足等;在寫數(shù)據(jù)系統(tǒng)中,主要對寫的數(shù)據(jù)(如“庫存”)做一致性檢查,最后在數(shù)據(jù)庫層保證數(shù)據(jù)的最終準(zhǔn)確性(如“庫存”不能減為負數(shù))。
7. 影響性能的因素
7.1 性能的定義
服務(wù)設(shè)備的不同對于性能的定義也是不一樣的,例如CPU主要看主頻,磁盤主要看IOPS(Input/ output Operations Per Second, 即每秒進行讀寫操作的次數(shù))。
關(guān)于秒殺,我們主要討論系統(tǒng)服務(wù)端的性能,一般使用QPS來衡量,還有一個影響和QPS息息相關(guān),即響應(yīng)時間(Response Time, RT),可以理解為服務(wù)器處理響應(yīng)的耗時。
正常情況下響應(yīng)時間越短,一秒鐘處理的請求數(shù)就會越多,這在單線程處理的情況下看起來是線性關(guān)系,即我們只要把每個請求的響應(yīng)時間降到最低,那么性能就會最高。而在多線程當(dāng)中,總QPS = (1000ms/ 響應(yīng)時間)x 線程數(shù),從這個角度上來看,性能和兩個因素相關(guān),一個是一次響應(yīng)的服務(wù)端的耗時,一個是處理請求的線程數(shù)。
7.1.1 響應(yīng)時間
對于大部分的Web系統(tǒng)而言,響應(yīng)時間一般是由CPU執(zhí)行時間和線程等待時間組成的,即服務(wù)器在處理一個請求時,一部分是CPU本身在做運算,還有一部分是各種等待。
理解了服務(wù)器處理請求的邏輯,估計你會說為什么我們不去減少這種等待時間。很遺憾,根據(jù)我們實際的測試發(fā)現(xiàn),減少線程等待時間對提升性能的影響沒有我們想象得那么大,它并不是線性的提升關(guān)系,這點在很多代理服務(wù)器(Proxy)上可以做驗證。
如果代理服務(wù)器本身沒有CPU消耗,我們在每次給代理服務(wù)器代理的請求加個延時,即增加響應(yīng)時間,但是這對代理服務(wù)器本身的吞吐量并沒有多大的影響,因為代理服務(wù)器本身的資源并沒有被消耗,可以通過增加代理服務(wù)器的處理線程數(shù),來彌補響應(yīng)時間對代理服務(wù)器的 QPS 的影響。
其實,真正對性能有影響的是 CPU 的執(zhí)行時間。這也很好理解,因為 CPU 的執(zhí)行真正消耗了服務(wù)器的資源。經(jīng)過實際的測試,如果減少 CPU 一半的執(zhí)行時間,就可以增加一倍的 QPS。
7.1.2 線程數(shù)
并不是線程數(shù)越多越好,總QPS就會越大,因為線程本身也消耗資源,會受到其他因素的制約。例如,線程越多系統(tǒng)的線程切換成本就會越高,而且每個線程都會耗費一定的內(nèi)存。
默認的配置一般為:
線程數(shù) = 2 x CPU核數(shù) + 1
還有一個根據(jù)最佳實踐得出來的公式為:
線程數(shù) = [(線程等待時間 + 線程CPU時間) / 線程CPU時間] x CPU數(shù)量
因此要提升性能,我們就要減少CPU的執(zhí)行時間,另外就是要設(shè)置一個合理的并發(fā)線程數(shù)量,通過這兩方面來顯著提升服務(wù)器的性能。
7.2 如何發(fā)現(xiàn)瓶頸
服務(wù)器會出現(xiàn)瓶頸的地方很多,例如CPU, 內(nèi)存, 磁盤以及網(wǎng)絡(luò)等可能都會導(dǎo)致瓶頸。另外不同的系統(tǒng)對于瓶頸的關(guān)注度不一樣,例如對緩存系統(tǒng)來說,制約的是內(nèi)存,而對存儲型的系統(tǒng)來說I/O 更容易出現(xiàn)瓶頸。
而對于秒殺,瓶頸更容易發(fā)生在CPU上。
那么,如何發(fā)現(xiàn) CPU 的瓶頸呢?其實有很多 CPU 診斷工具可以發(fā)現(xiàn) CPU 的消耗,最常用的就是 JProfiler 和 Yourkit 這兩個工具,它們可以列出整個請求中每個函數(shù)的 CPU 執(zhí)行時間,可以發(fā)現(xiàn)哪個函數(shù)消耗的 CPU 時間最多,以便你有針對性地做優(yōu)化。
當(dāng)然還有一些辦法也可以近似地統(tǒng)計 CPU 的耗時,例如通過 jstack 定時地打印調(diào)用棧,如果某些函數(shù)調(diào)用頻繁或者耗時較多,那么那些函數(shù)就會多次出現(xiàn)在系統(tǒng)調(diào)用棧里,這樣相當(dāng)于采樣的方式也能夠發(fā)現(xiàn)耗時較多的函數(shù)。
雖說秒殺系統(tǒng)的瓶頸大部分在 CPU,但這并不表示其他方面就一定不出現(xiàn)瓶頸。例如,如果海量請求涌過來,你的頁面又比較大,那么網(wǎng)絡(luò)就有可能出現(xiàn)瓶頸。
怎樣簡單地判斷 CPU 是不是瓶頸呢?一個辦法就是看當(dāng) QPS 達到極限時,你的服務(wù)器的 CPU 使用率是不是超過了 95%,如果沒有超過,那么表示 CPU 還有提升的空間,要么是有鎖限制,要么是有過多的本地 I/O 等待發(fā)生。
7.3 如何優(yōu)化系統(tǒng)
針對Java來說的:
7.3.1 減少編碼
Java的編碼運行比較慢,在很多場景下,只要涉及字符串的操作都會比較消耗CPU資源,不管是磁盤IO還是網(wǎng)絡(luò)IO,因為都需要將字符轉(zhuǎn)換成字節(jié),這個轉(zhuǎn)換必須編碼。
每個字符的編碼都需要查表,而這種查表的操作非常耗資源,所以減少字符到字節(jié)或者相反的轉(zhuǎn)換、減少字符編碼會非常有成效。減少編碼就可以大大提升性能。
那么如何才能減少編碼呢?例如,網(wǎng)頁輸出是可以直接進行流輸出的,即用 resp.getOutputStream() 函數(shù)寫數(shù)據(jù),把一些靜態(tài)的數(shù)據(jù)提前轉(zhuǎn)化成字節(jié),等到真正往外寫的時候再直接用 OutputStream() 函數(shù)寫,就可以減少靜態(tài)數(shù)據(jù)的編碼轉(zhuǎn)換。
7.3.2 減少序列化
序列化也是Java性能的一大天敵,減少Java當(dāng)中的序列化操作也能大大提升性能。又因為序列化往往是和編碼同時發(fā)生的,所以減少序列化也就減少了編碼。
序列化大部分是在 RPC 中發(fā)生的,因此避免或者減少 RPC 就可以減少序列化,當(dāng)然當(dāng)前的序列化協(xié)議也已經(jīng)做了很多優(yōu)化來提升性能。有一種新的方案,就是可以將多個關(guān)聯(lián)性比較強的應(yīng)用進行“合并部署”,而減少不同應(yīng)用之間的 RPC 也可以減少序列化的消耗。
所謂“合并部署”,就是把兩個原本在不同機器上的不同應(yīng)用合并部署到一臺機器上,當(dāng)然不僅僅是部署在一臺機器上,還要在同一個 Tomcat 容器中,且不能走本機的 Socket,這樣才能避免序列化的產(chǎn)生。
7.3.3 Java 秒殺場景的針對性優(yōu)化
Java 和通用的 Web 服務(wù)器(如 Nginx 或 Apache 服務(wù)器)相比,在處理大并發(fā)的 HTTP 請求時要弱一點,所以一般我們都會對大流量的 Web 系統(tǒng)做靜態(tài)化改造,讓大部分請求和數(shù)據(jù)直接在 Nginx 服務(wù)器或者 Web 代理服務(wù)器(如 Varnish、Squid 等)上直接返回(這樣可以減少數(shù)據(jù)的序列化與反序列化),而 Java 層只需處理少量數(shù)據(jù)的動態(tài)請求。針對這些請求,我們可以使用以下手段進行優(yōu)化:
直接使用 Servlet 處理請求。避免使用傳統(tǒng)的 MVC 框架,這樣可以繞過一大堆復(fù)雜且用處不大的處理邏輯,節(jié)省 1ms 時間(具體取決于你對 MVC 框架的依賴程度)。
直接輸出流數(shù)據(jù)。使用 resp.getOutputStream() 而不是 resp.getWriter() 函數(shù),可以省掉一些不變字符數(shù)據(jù)的編碼,從而提升性能;數(shù)據(jù)輸出時推薦使用 JSON 而不是模板引擎(一般都是解釋執(zhí)行)來輸出頁面。
7.3.4 并發(fā)讀優(yōu)化
也許有讀者會覺得這個問題很容易解決,無非就是放到 Tair 緩存里面。集中式緩存為了保證命中率一般都會采用一致性 Hash,所以同一個 key 會落到同一臺機器上。雖然單臺緩存機器也能支撐 30w/s 的請求,但還是遠不足以應(yīng)對像“大秒”這種級別的熱點商品。那么,該如何徹底解決單點的瓶頸呢?
答案是采用應(yīng)用層的 LocalCache,即在秒殺系統(tǒng)的單機上緩存商品相關(guān)的數(shù)據(jù)。
那么,又如何緩存(Cache)數(shù)據(jù)呢?你需要劃分成動態(tài)數(shù)據(jù)和靜態(tài)數(shù)據(jù)分別進行處理:
像商品中的“標(biāo)題”和“描述”這些本身不變的數(shù)據(jù),會在秒殺開始之前全量推送到秒殺機器上,并一直緩存到秒殺結(jié)束;
像庫存這類動態(tài)數(shù)據(jù),會采用“被動失效”的方式緩存一定時間(一般是數(shù)秒),失效后再去緩存拉取最新的數(shù)據(jù)。
還有關(guān)于一致性的問題,因為庫存是在不斷更新的,這就要用到前面介紹的讀數(shù)據(jù)的分層校驗原則了,讀的場景可以允許一定的臟數(shù)據(jù),因為這里的誤判只會導(dǎo)致少量原本無庫存的下單請求被誤認為有庫存,可以等到真正寫數(shù)據(jù)時再保證最終的一致性,通過在數(shù)據(jù)的高可用性和一致性之間的平衡,來解決高并發(fā)的數(shù)據(jù)讀取問題。
8. 減庫存設(shè)計的核心邏輯
不超賣是秒殺系統(tǒng)的前提。減庫存到底應(yīng)該是在下單階段還是付款階段呢?
8.1 減庫存的方式
8.1.1 下單減庫存
即當(dāng)買家下單之后,在商品的總庫存中減去買家購買的數(shù)量。這種方式控制最精確,下單時直接通過數(shù)據(jù)庫的事務(wù)機制控制商品庫存,這樣一定不會出現(xiàn)超賣的現(xiàn)象。但是有些人下完單以后并不會付款。
8.1.2 付款減庫存
即買家下單后,并不立即減庫存,而是等到有用戶付款后才真正減庫存,否則庫存一直保留給其他買家。但因為付款時才減庫存,如果并發(fā)比較高,有可能出現(xiàn)買家下單后付不了款的情況,因為可能商品已經(jīng)被其他人買走了。
8.1.3 預(yù)扣庫存
這種方式相對復(fù)雜一些,買家下單后,庫存為其保留一定的時間(如 10 分鐘),超過這個時間,庫存將會自動釋放,釋放后其他買家就可以繼續(xù)購買。在買家付款前,系統(tǒng)會校驗該訂單的庫存是否還有保留:如果沒有保留,則再次嘗試預(yù)扣;如果庫存不足(也就是預(yù)扣失敗)則不允許繼續(xù)付款;如果預(yù)扣成功,則完成付款并實際地減去庫存。
8.2 可能存在的問題
假如我們采用“下單減庫存”的方式,即用戶下單后就減去庫存,正常情況下,買家下單后付款的概率會很高,所以不會有太大問題。但是有一種場景例外,就是當(dāng)賣家參加某個活動時,此時活動的有效時間是商品的黃金售賣時間,如果有競爭對手通過惡意下單的方式將該賣家的商品全部下單,讓這款商品的庫存減為零,那么這款商品就不能正常售賣了。要知道,這些惡意下單的人是不會真正付款的,這正是“下單減庫存”方式的不足之處。
既然“下單減庫存”可能導(dǎo)致惡意下單,從而影響賣家的商品銷售,那么有沒有辦法解決呢?你可能會想,采用“付款減庫存”的方式是不是就可以了?的確可以。但是,“付款減庫存”又會導(dǎo)致另外一個問題:庫存超賣。
假如有 100 件商品,就可能出現(xiàn) 300 人下單成功的情況,因為下單時不會減庫存,所以也就可能出現(xiàn)下單成功數(shù)遠遠超過真正庫存數(shù)的情況,這尤其會發(fā)生在做活動的熱門商品上。這樣一來,就會導(dǎo)致很多買家下單成功但是付不了款,買家的購物體驗自然比較差。
那么,既然“下單減庫存”和“付款減庫存”都有缺點,我們能否把兩者相結(jié)合,將兩次操作進行前后關(guān)聯(lián)起來,下單時先預(yù)扣,在規(guī)定時間內(nèi)不付款再釋放庫存,即采用“預(yù)扣庫存”這種方式呢?
這種方案確實可以在一定程度上緩解上面的問題。但是否就徹底解決了呢?其實沒有!針對惡意下單這種情況,雖然把有效的付款時間設(shè)置為 10 分鐘,但是惡意買家完全可以在 10 分鐘后再次下單,或者采用一次下單很多件的方式把庫存減完。針對這種情況,解決辦法還是要結(jié)合安全和反作弊的措施來制止。
例如,給經(jīng)常下單不付款的買家進行識別打標(biāo)(可以在被打標(biāo)的買家下單時不減庫存)、給某些類目設(shè)置最大購買件數(shù)(例如,參加活動的商品一人最多只能買 3 件),以及對重復(fù)下單不付款的操作進行次數(shù)限制等。
針對“庫存超賣”這種情況,在 10 分鐘時間內(nèi)下單的數(shù)量仍然有可能超過庫存數(shù)量,遇到這種情況我們只能區(qū)別對待:對普通的商品下單數(shù)量超過庫存數(shù)量的情況,可以通過補貨來解決;但是有些賣家完全不允許庫存為負數(shù)的情況,那只能在買家付款時提示庫存不足。
8.3 大型秒殺中如何減庫存
對于一般業(yè)務(wù)系統(tǒng)而言,一般是預(yù)扣庫存的方案,超出有效付款時間訂單就會自動釋放。而對于秒殺場景,一般采用下單減庫存。
“下單減庫存”在數(shù)據(jù)一致性上,主要就是保證大并發(fā)請求時庫存數(shù)據(jù)不能為負數(shù),也就是要保證數(shù)據(jù)庫中的庫存字段值不能為負數(shù),一般我們有多種解決方案:一種是在應(yīng)用程序中通過事務(wù)來判斷,即保證減后庫存不能為負數(shù),否則就回滾;另一種辦法是直接設(shè)置數(shù)據(jù)庫的字段數(shù)據(jù)為無符號整數(shù),這樣減后庫存字段值小于零時會直接執(zhí)行 SQL 語句來報錯;再有一種就是使用 CASE WHEN 判斷語句,例如這樣的 SQL 語句:
UPDATEitemSETinventory=CASEWHENinventory>=xxxTHENinventory-xxxELSEinventoryEND
秒殺商品和普通商品的減庫存還是有些差異的,例如商品數(shù)量比較少,交易時間段也比較短,因此這里有一個大膽的假設(shè),即能否把秒殺商品減庫存直接放到緩存系統(tǒng)中實現(xiàn),也就是直接在緩存中減庫存或者在一個帶有持久化功能的緩存系統(tǒng)(如 Redis)中完成呢?
如果你的秒殺商品的減庫存邏輯非常單一,比如沒有復(fù)雜的 SKU 庫存和總庫存這種聯(lián)動關(guān)系的話,我覺得完全可以。但是如果有比較復(fù)雜的減庫存邏輯,或者需要使用事務(wù),你還是必須在數(shù)據(jù)庫中完成減庫存。
由于 MySQL 存儲數(shù)據(jù)的特點,同一數(shù)據(jù)在數(shù)據(jù)庫里肯定是一行存儲(MySQL),因此會有大量線程來競爭 InnoDB 行鎖,而并發(fā)度越高時等待線程會越多,TPS(Transaction Per Second,即每秒處理的消息數(shù))會下降,響應(yīng)時間(RT)會上升,數(shù)據(jù)庫的吞吐量就會嚴重受影響。
這就可能引發(fā)一個問題,就是單個熱點商品會影響整個數(shù)據(jù)庫的性能, 導(dǎo)致 0.01% 的商品影響 99.99% 的商品的售賣,這是我們不愿意看到的情況。一個解決思路是遵循前面介紹的原則進行隔離,把熱點商品放到單獨的熱點庫中。但是這無疑會帶來維護上的麻煩,比如要做熱點數(shù)據(jù)的動態(tài)遷移以及單獨的數(shù)據(jù)庫等。
而分離熱點商品到單獨的數(shù)據(jù)庫還是沒有解決并發(fā)鎖的問題,我們應(yīng)該怎么辦呢?要解決并發(fā)鎖的問題,有兩種辦法:
應(yīng)用層排隊
按照商品維度設(shè)置隊列順序執(zhí)行,這樣能減少同一臺機器對數(shù)據(jù)庫同一行記錄進行操作的并發(fā)度,同時也能控制單個商品占用數(shù)據(jù)庫連接的數(shù)量,防止熱點商品占用太多的數(shù)據(jù)庫連接。
數(shù)據(jù)庫排隊
應(yīng)用層只能做到單機的排隊,但是應(yīng)用機器數(shù)本身很多,這種排隊方式控制并發(fā)的能力仍然有限,所以如果能在數(shù)據(jù)庫層做全局排隊是最理想的。阿里的數(shù)據(jù)庫團隊開發(fā)了針對這種 MySQL 的 InnoDB 層上的補丁程序(patch),可以在數(shù)據(jù)庫層上對單行記錄做到并發(fā)排隊。
9. 如何設(shè)計兜底方案?
9.1 高可用建設(shè)應(yīng)該從哪里著手?
fig10.jpg
架構(gòu)階段 - 考慮系統(tǒng)的可擴展性和容錯性,要避免出現(xiàn)單點問題。例如多機房單元化部署,即使某個城市的某個機房出現(xiàn)整體故障,仍然不會影響整體網(wǎng)站的運轉(zhuǎn)。
編碼階段 - 保證代碼的健壯性,例如涉及到遠程調(diào)用的問題的時候,要設(shè)置合理的超時退出機制,防止被其他系統(tǒng)拖垮,也要對調(diào)用的返回結(jié)果集有預(yù)期,防止返回的結(jié)果超出程序處理的范圍。即對錯誤異常進行捕獲,對無法預(yù)料的錯誤要有默認處理結(jié)果。
測試階段 - 測試主要是保證測試用例的覆蓋度,保證最壞情況發(fā)生的時候,我們也有相應(yīng)的處理流程。
發(fā)布階段 - 要有緊急的回滾機制
運行階段 - 運行態(tài)是常態(tài),重要的是對系統(tǒng)的監(jiān)控要準(zhǔn)確及時,發(fā)現(xiàn)問題能夠準(zhǔn)確報警并且報警數(shù)據(jù)要準(zhǔn)確詳細,以便于排查問題。
故障發(fā)生 - 及時止損,例如由于程序問題導(dǎo)致商品價格錯誤,就要及時下架商品或者關(guān)閉購買鏈接,防止造成重大資產(chǎn)損失。
為什么系統(tǒng)的高可用建設(shè)要放到整個生命周期中全面考慮?因為我們在每個環(huán)節(jié)中都可能犯錯,而有些環(huán)節(jié)犯的錯,你在后面是無法彌補的。例如在架構(gòu)階段,你沒有消除單點問題,那么系統(tǒng)上線后,遇到突發(fā)流量把單點給掛了,你就只能干瞪眼,有時候想加機器都加不進去。所以高可用建設(shè)是一個系統(tǒng)工程,必須在每個環(huán)節(jié)都做好。
那么針對秒殺系統(tǒng),我們重點介紹在遇到大流量時,應(yīng)該從哪些方面來保障系統(tǒng)的穩(wěn)定運行,所以更多的是看如何針對運行階段進行處理,這就引出了接下來的內(nèi)容:降級、限流和拒絕服務(wù)。
9.2 降級
所謂“降級”,就是當(dāng)系統(tǒng)的容量達到一定程度時,限制或者關(guān)閉系統(tǒng)的某些非核心功能,從而把有限的資源保留給更核心的業(yè)務(wù)。它是一個有目的、有計劃的執(zhí)行過程,所以對降級我們一般需要有一套預(yù)案來配合執(zhí)行。如果我們把它系統(tǒng)化,就可以通過預(yù)案系統(tǒng)和開關(guān)系統(tǒng)來實現(xiàn)降級。
降級方案可以這樣設(shè)計:當(dāng)秒殺流量達到 5w/s 時,把成交記錄的獲取從展示 20 條降級到只展示 5 條。“從 20 改到 5”這個操作由一個開關(guān)來實現(xiàn),也就是設(shè)置一個能夠從開關(guān)系統(tǒng)動態(tài)獲取的系統(tǒng)參數(shù)。
這里,我給出開關(guān)系統(tǒng)的示意圖。它分為兩部分,一部分是開關(guān)控制臺,它保存了開關(guān)的具體配置信息,以及具體執(zhí)行開關(guān)所對應(yīng)的機器列表;另一部分是執(zhí)行下發(fā)開關(guān)數(shù)據(jù)的 Agent,主要任務(wù)就是保證開關(guān)被正確執(zhí)行,即使系統(tǒng)重啟后也會生效。
fig11.jpg
9.3 限流
如果說降級是犧牲了一部分次要的功能和用戶的體驗效果,那么限流就是更極端的一種保護措施了。限流就是當(dāng)系統(tǒng)容量達到瓶頸時,我們需要通過限制一部分流量來保護系統(tǒng),并做到既可以人工執(zhí)行開關(guān),也支持自動化保護的措施。
這里,我同樣給出了限流系統(tǒng)的示意圖??傮w來說,限流既可以是在客戶端限流,也可以是在服務(wù)端限流。此外,限流的實現(xiàn)方式既要支持 URL 以及方法級別的限流,也要支持基于 QPS 和線程的限流。
客戶端限流
好處可以限制請求的發(fā)出,通過減少發(fā)出無用請求從而減少對系統(tǒng)的消耗。缺點就是當(dāng)客戶端比較分散時,沒法設(shè)置合理的限流閾值:如果閾值設(shè)的太小,會導(dǎo)致服務(wù)端沒有達到瓶頸時客戶端已經(jīng)被限制;而如果設(shè)的太大,則起不到限制的作用。
服務(wù)端限流
好處是可以根據(jù)服務(wù)端的性能設(shè)置合理的閾值,而缺點就是被限制的請求都是無效的請求,處理這些無效的請求本身也會消耗服務(wù)器資源。
fig12.jpg
在限流的實現(xiàn)手段上來講,基于 QPS 和線程數(shù)的限流應(yīng)用最多,最大 QPS 很容易通過壓測提前獲取,例如我們的系統(tǒng)最高支持 1w QPS 時,可以設(shè)置 8000 來進行限流保護。線程數(shù)限流在客戶端比較有效,例如在遠程調(diào)用時我們設(shè)置連接池的線程數(shù),超出這個并發(fā)線程請求,就將線程進行排隊或者直接超時丟棄。
限流無疑會影響用戶的正常請求,所以必然會導(dǎo)致一部分用戶請求失敗,因此在系統(tǒng)處理這種異常時一定要設(shè)置超時時間,防止因被限流的請求不能 fast fail(快速失?。┒峡逑到y(tǒng)。
9.4 拒絕服務(wù)
當(dāng)系統(tǒng)負載達到一定閾值時,例如 CPU 使用率達到 90% 或者系統(tǒng) load 值達到 2*CPU 核數(shù)時,系統(tǒng)直接拒絕所有請求,這種方式是最暴力但也最有效的系統(tǒng)保護方式。例如秒殺系統(tǒng),我們在如下幾個環(huán)節(jié)設(shè)計過載保護:
在最前端的 Nginx 上設(shè)置過載保護,當(dāng)機器負載達到某個值時直接拒絕 HTTP 請求并返回 503 錯誤碼,在 Java 層同樣也可以設(shè)計過載保護。
拒絕服務(wù)可以說是一種不得已的兜底方案,用以防止最壞情況發(fā)生,防止因把服務(wù)器壓跨而長時間徹底無法提供服務(wù)。像這種系統(tǒng)過載保護雖然在過載時無法提供服務(wù),但是系統(tǒng)仍然可以運作,當(dāng)負載下降時又很容易恢復(fù),所以每個系統(tǒng)和每個環(huán)節(jié)都應(yīng)該設(shè)置這個兜底方案,對系統(tǒng)做最壞情況下的保護。
原文標(biāo)題:9. 如何設(shè)計兜底方案?
文章出處:【微信公眾號:馬哥Linux運維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7002瀏覽量
88938 -
數(shù)據(jù)緩存
+關(guān)注
關(guān)注
0文章
23瀏覽量
7055 -
HTTP協(xié)議
+關(guān)注
關(guān)注
0文章
61瀏覽量
9719
原文標(biāo)題:9. 如何設(shè)計兜底方案?
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論