前言
如果問(wèn)起秒殺系統(tǒng)如何設(shè)計(jì)毁枯,相信都能說(shuō)出個(gè)123來(lái),但是如果細(xì)究其中的細(xì)節(jié)點(diǎn)叮称,很多人估計(jì)就無(wú)法很快的打上來(lái)了种玛。本文從六個(gè)方面藐鹤,來(lái)簡(jiǎn)要講一下秒殺系統(tǒng)要如何設(shè)計(jì),應(yīng)該主要哪些事情赂韵。
01 | 設(shè)計(jì)秒殺系統(tǒng)時(shí)應(yīng)該注意的5個(gè)架構(gòu)原則
說(shuō)起秒殺娱节,我想你肯定不陌生,這兩年祭示,從雙十一購(gòu)物到春節(jié)搶紅包肄满,再到 12306 搶火車票,“秒殺”的場(chǎng)景處處可見(jiàn)质涛。簡(jiǎn)單來(lái)說(shuō)稠歉,秒殺就是在同一個(gè)時(shí)刻有大量的請(qǐng)求爭(zhēng)搶購(gòu)買同一個(gè)商品并完成交易的過(guò)程,用技術(shù)的行話來(lái)說(shuō)就是大量的并發(fā)讀和并發(fā)寫蹂窖。不管是哪一門語(yǔ)言轧抗,并發(fā)都是程序員們最為頭疼的部分。
同樣瞬测,對(duì)于一個(gè)軟件而言也是這樣横媚,你可以很快增刪改查做出一個(gè)秒殺系統(tǒng),但是要讓它支持高并發(fā)訪問(wèn)就沒(méi)那么容易了月趟。比如說(shuō)灯蝴,如何讓系統(tǒng)面對(duì)百萬(wàn)級(jí)的請(qǐng)求流量不出故障?如何保證高并發(fā)情況下數(shù)據(jù)的一致性寫孝宗?完全靠堆服務(wù)器來(lái)解決嗎穷躁?這顯然不是最好的解決方案。
在我看來(lái)因妇,秒殺系統(tǒng)本質(zhì)上就是一個(gè)滿足大并發(fā)问潭、高性能和高可用的分布式系統(tǒng)。今天婚被,我們就來(lái)聊聊狡忙,如何在滿足一個(gè)良好架構(gòu)的分布式系統(tǒng)基礎(chǔ)上,針對(duì)秒殺這種業(yè)務(wù)做到極致的性能改進(jìn)址芯。
架構(gòu)原則:“4 要 1 不要
”如果你是一個(gè)架構(gòu)師灾茁,你首先要勾勒出一個(gè)輪廓,想一想如何構(gòu)建一個(gè)超大流量并發(fā)讀寫谷炸、高性能北专,以及高可用的系統(tǒng),這其中有哪些要素需要考慮旬陡。我把這些要素總結(jié)為“4 要 1 不要”拓颓。
** 1. 數(shù)據(jù)要盡量少**
所謂“數(shù)據(jù)要盡量少”,首先是指用戶請(qǐng)求的數(shù)據(jù)能少就少描孟。請(qǐng)求的數(shù)據(jù)包括上傳給系統(tǒng)的數(shù)據(jù)和系統(tǒng)返回給用戶的數(shù)據(jù)(通常就是網(wǎng)頁(yè))录粱。為啥“數(shù)據(jù)要盡量少”呢腻格?因?yàn)槭紫冗@些數(shù)據(jù)在網(wǎng)絡(luò)上傳輸需要時(shí)間画拾,其次不管是請(qǐng)求數(shù)據(jù)還是返回?cái)?shù)據(jù)都需要服務(wù)器做處理啥繁,而服務(wù)器在寫網(wǎng)絡(luò)時(shí)通常都要做壓縮和字符編碼,這些都非常消耗 CPU青抛,所以減少傳輸?shù)臄?shù)據(jù)量可以顯著減少 CPU 的使用旗闽。例如,我們可以簡(jiǎn)化秒殺頁(yè)面的大小蜜另,去掉不必要的頁(yè)面裝修效果适室,等等。其次举瑰,“數(shù)據(jù)要盡量少”還要求系統(tǒng)依賴的數(shù)據(jù)能少就少捣辆,包括系統(tǒng)完成某些業(yè)務(wù)邏輯需要讀取和保存的數(shù)據(jù),這些數(shù)據(jù)一般是和后臺(tái)服務(wù)以及數(shù)據(jù)庫(kù)打交道的此迅。調(diào)用其他服務(wù)會(huì)涉及數(shù)據(jù)的序列化和反序列化汽畴,而這也是 CPU 的一大殺手,同樣也會(huì)增加延時(shí)耸序。而且忍些,數(shù)據(jù)庫(kù)本身也容易成為一個(gè)瓶頸,所以和數(shù)據(jù)庫(kù)打交道越少越好坎怪,數(shù)據(jù)越簡(jiǎn)單罢坝、越小則越好。
** 2. 請(qǐng)求數(shù)要盡量少**
用戶請(qǐng)求的頁(yè)面返回后搅窿,瀏覽器渲染這個(gè)頁(yè)面還要包含其他的額外請(qǐng)求嘁酿,比如說(shuō),這個(gè)頁(yè)面依賴的 CSS/JavaScript男应、圖片闹司,以及 Ajax 請(qǐng)求等等都定義為“額外請(qǐng)求”,這些額外請(qǐng)求應(yīng)該盡量少殉了。因?yàn)闉g覽器每發(fā)出一個(gè)請(qǐng)求都多少會(huì)有一些消耗开仰,例如建立連接要做三次握手,有的時(shí)候有頁(yè)面依賴或者連接數(shù)限制薪铜,一些請(qǐng)求(例如 JavaScript)還需要串行加載等众弓。另外,如果不同請(qǐng)求的域名不一樣的話隔箍,還涉及這些域名的 DNS 解析谓娃,可能會(huì)耗時(shí)更久。所以你要記住的是蜒滩,減少請(qǐng)求數(shù)可以顯著減少以上這些因素導(dǎo)致的資源消耗滨达。例如奶稠,減少請(qǐng)求數(shù)最常用的一個(gè)實(shí)踐就是合并 CSS 和 JavaScript 文件,把多個(gè) JavaScript 文件合并成一個(gè)文件捡遍,在 URL 中用逗號(hào)隔開(kāi)(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ù)端仍然是單個(gè)文件各自存放,只是服務(wù)端會(huì)有一個(gè)組件解析這個(gè) URL画株,然后動(dòng)態(tài)把這些文件合并起來(lái)一起返回辆飘。
3. 路徑要盡量短
所謂“路徑”,就是用戶發(fā)出請(qǐng)求到返回?cái)?shù)據(jù)這個(gè)過(guò)程中谓传,需求經(jīng)過(guò)的中間的節(jié)點(diǎn)數(shù)蜈项。通常,這些節(jié)點(diǎn)可以表示為一個(gè)系統(tǒng)或者一個(gè)新的 Socket 連接(比如代理服務(wù)器只是創(chuàng)建一個(gè)新的 Socket 連接來(lái)轉(zhuǎn)發(fā)請(qǐng)求)续挟。每經(jīng)過(guò)一個(gè)節(jié)點(diǎn)紧卒,一般都會(huì)產(chǎn)生一個(gè)新的 Socket 連接。然而诗祸,每增加一個(gè)連接都會(huì)增加新的不確定性跑芳。從概率統(tǒng)計(jì)上來(lái)說(shuō),假如一次請(qǐng)求經(jīng)過(guò) 5 個(gè)節(jié)點(diǎn)贬媒,每個(gè)節(jié)點(diǎn)的可用性是 99.9% 的話聋亡,那么整個(gè)請(qǐng)求的可用性是:99.9% 的 5 次方,約等于 99.5%际乘。所以縮短請(qǐng)求路徑不僅可以增加可用性坡倔,同樣可以有效提升性能(減少中間節(jié)點(diǎn)可以減少數(shù)據(jù)的序列化與反序列化),并減少延時(shí)(可以減少網(wǎng)絡(luò)傳輸耗時(shí))脖含。要縮短訪問(wèn)路徑有一種辦法罪塔,就是多個(gè)相互強(qiáng)依賴的應(yīng)用合并部署在一起,把遠(yuǎn)程過(guò)程調(diào)用(RPC)變成 JVM 內(nèi)部之間的方法調(diào)用养葵。在《大型網(wǎng)站技術(shù)架構(gòu)演進(jìn)與性能優(yōu)化》一書中征堪,我也有一章介紹了這種技術(shù)的詳細(xì)實(shí)現(xiàn)。
4. 依賴要盡量少
所謂依賴关拒,指的是要完成一次用戶請(qǐng)求必須依賴的系統(tǒng)或者服務(wù)佃蚜,這里的依賴指的是強(qiáng)依賴。舉個(gè)例子着绊,比如說(shuō)你要展示秒殺頁(yè)面谐算,而這個(gè)頁(yè)面必須強(qiáng)依賴商品信息、用戶信息归露,還有其他如優(yōu)惠券洲脂、成交列表等這些對(duì)秒殺不是非要不可的信息(弱依賴),這些弱依賴在緊急情況下就可以去掉剧包。要減少依賴恐锦,我們可以給系統(tǒng)進(jìn)行分級(jí)往果,比如 0 級(jí)系統(tǒng)、1 級(jí)系統(tǒng)一铅、2 級(jí)系統(tǒng)陕贮、3 級(jí)系統(tǒng),0 級(jí)系統(tǒng)如果是最重要的系統(tǒng)馅闽,那么 0 級(jí)系統(tǒng)強(qiáng)依賴的系統(tǒng)也同樣是最重要的系統(tǒng)飘蚯,以此類推。注意福也,0 級(jí)系統(tǒng)要盡量減少對(duì) 1 級(jí)系統(tǒng)的強(qiáng)依賴,防止重要的系統(tǒng)被不重要的系統(tǒng)拖垮攀圈。例如支付系統(tǒng)是 0 級(jí)系統(tǒng)暴凑,而優(yōu)惠券是 1 級(jí)系統(tǒng)的話,在極端情況下可以把優(yōu)惠券給降級(jí)赘来,防止支付系統(tǒng)被優(yōu)惠券這個(gè) 1 級(jí)系統(tǒng)給拖垮现喳。
5. 不要有單點(diǎn)
系統(tǒng)中的單點(diǎn)可以說(shuō)是系統(tǒng)架構(gòu)上的一個(gè)大忌,因?yàn)閱吸c(diǎn)意味著沒(méi)有備份犬辰,風(fēng)險(xiǎn)不可控嗦篱,我們?cè)O(shè)計(jì)分布式系統(tǒng)最重要的原則就是“消除單點(diǎn)”。那如何避免單點(diǎn)呢幌缝?我認(rèn)為關(guān)鍵點(diǎn)是避免將服務(wù)的狀態(tài)和機(jī)器綁定灸促,即把服務(wù)無(wú)狀態(tài)化,這樣服務(wù)就可以在機(jī)器中隨意移動(dòng)涵卵。如何那把服務(wù)的狀態(tài)和機(jī)器解耦呢浴栽?這里也有很多實(shí)現(xiàn)方式。例如把和機(jī)器相關(guān)的配置動(dòng)態(tài)化轿偎,這些參數(shù)可以通過(guò)配置中心來(lái)動(dòng)態(tài)推送典鸡,在服務(wù)啟動(dòng)時(shí)動(dòng)態(tài)拉取下來(lái),我們?cè)谶@些配置中心設(shè)置一些規(guī)則來(lái)方便地改變這些映射關(guān)系坏晦。應(yīng)用無(wú)狀態(tài)化是有效避免單點(diǎn)的一種方式萝玷,但是像存儲(chǔ)服務(wù)本身很難無(wú)狀態(tài)化,因?yàn)閿?shù)據(jù)要存儲(chǔ)在磁盤上昆婿,本身就要和機(jī)器綁定球碉,那么這種場(chǎng)景一般要通過(guò)冗余多個(gè)備份的方式來(lái)解決單點(diǎn)問(wèn)題。前面介紹了這些設(shè)計(jì)上的一些原則挖诸,但是你有沒(méi)有發(fā)現(xiàn)汁尺,我一直說(shuō)的是“盡量”而不是“絕對(duì)”?我想你肯定會(huì)問(wèn)是不是請(qǐng)求最少就一定最好多律,我的答案是“不一定”痴突。我們?cè)?jīng)把有些 CSS 內(nèi)聯(lián)進(jìn)頁(yè)面里搂蜓,這樣做可以減少依賴一個(gè) CSS 的請(qǐng)求從而加快首頁(yè)的渲染,但是同樣也增大了頁(yè)面的大小辽装,又不符合“數(shù)據(jù)要盡量少”的原則帮碰,這種情況下我們?yōu)榱颂嵘灼恋匿秩舅俣龋话咽灼恋?HTML 依賴的 CSS 內(nèi)聯(lián)進(jìn)來(lái)拾积,其他 CSS 仍然放到文件中作為依賴加載殉挽,盡量實(shí)現(xiàn)首屏的打開(kāi)速度與整個(gè)頁(yè)面加載性能的平衡。所以說(shuō)拓巧,架構(gòu)是一種平衡的藝術(shù)斯碌,而最好的架構(gòu)一旦脫離了它所適應(yīng)的場(chǎng)景,一切都將是空談肛度。我希望你記住的是傻唾,這里所說(shuō)的幾點(diǎn)都只是一個(gè)個(gè)方向,你應(yīng)該盡量往這些方向上去努力承耿,但也要考慮平衡其他因素冠骄。
不同場(chǎng)景下的不同架構(gòu)案例
前面我說(shuō)了一些架構(gòu)上的原則,那么針對(duì)“秒殺”這個(gè)場(chǎng)景加袋,怎樣才是一個(gè)好的架構(gòu)呢凛辣?下面我以淘寶早期秒殺系統(tǒng)架構(gòu)的演進(jìn)為主線,來(lái)幫你梳理不同的請(qǐng)求體量下职烧,我認(rèn)為的最佳秒殺系統(tǒng)架構(gòu)扁誓。
前面我說(shuō)了一些架構(gòu)上的原則,那么針對(duì)“秒殺”這個(gè)場(chǎng)景阳堕,怎樣才是一個(gè)好的架構(gòu)呢跋理?下面我以淘寶早期秒殺系統(tǒng)架構(gòu)的演進(jìn)為主線,來(lái)幫你梳理不同的請(qǐng)求體量下恬总,我認(rèn)為的最佳秒殺系統(tǒng)架構(gòu)前普。如果你想快速搭建一個(gè)簡(jiǎn)單的秒殺系統(tǒng),只需要把你的商品購(gòu)買頁(yè)面增加一個(gè)“定時(shí)上架”功能壹堰,僅在秒殺開(kāi)始時(shí)才讓用戶看到購(gòu)買按鈕拭卿,當(dāng)商品的庫(kù)存賣完了也就結(jié)束了。這就是當(dāng)時(shí)第一個(gè)版本的秒殺系統(tǒng)實(shí)現(xiàn)方式贱纠。但隨著請(qǐng)求量的加大(比如從 1w/s 到了 10w/s 的量級(jí))峻厚,這個(gè)簡(jiǎn)單的架構(gòu)很快就遇到了瓶頸,因此需要做架構(gòu)改造來(lái)提升系統(tǒng)性能谆焊。
這些架構(gòu)改造包括:
- 把秒殺系統(tǒng)獨(dú)立出來(lái)單獨(dú)打造一個(gè)系統(tǒng)惠桃,這樣可以有針對(duì)性地做優(yōu)化,例如這個(gè)獨(dú)立出來(lái)的系統(tǒng)就減少了店鋪裝修的功能,減少了頁(yè)面的復(fù)雜度辜王;
- 在系統(tǒng)部署上也獨(dú)立做一個(gè)機(jī)器集群劈狐,這樣秒殺的大流量就不會(huì)影響到正常的商品購(gòu)買集群的機(jī)器負(fù)載;
- 將熱點(diǎn)數(shù)據(jù)(如庫(kù)存數(shù)據(jù))單獨(dú)放到一個(gè)緩存系統(tǒng)中呐馆,以提高“讀性能”肥缔;
- 增加秒殺答題,防止有秒殺器搶單汹来。
此時(shí)的系統(tǒng)架構(gòu)變成了下圖這個(gè)樣子续膳。最重要的就是,秒殺詳情成為了一個(gè)獨(dú)立的新系統(tǒng)收班,另外核心的一些數(shù)據(jù)放到了緩存(Cache)中坟岔,其他的關(guān)聯(lián)系統(tǒng)也都以獨(dú)立集群的方式進(jìn)行部署。
然而這個(gè)架構(gòu)仍然支持不了超過(guò) 100w/s 的請(qǐng)求量闺阱,所以為了進(jìn)一步提升秒殺系統(tǒng)的性能炮车,我們又對(duì)架構(gòu)做進(jìn)一步升級(jí),
比如:
- 對(duì)頁(yè)面進(jìn)行徹底的動(dòng)靜分離酣溃,使得用戶秒殺時(shí)不需要刷新整個(gè)頁(yè)面,而只需要點(diǎn)擊搶寶按鈕纪隙,借此把頁(yè)面刷新的數(shù)據(jù)降到最少赊豌;
- 在服務(wù)端對(duì)秒殺商品進(jìn)行本地緩存,不需要再調(diào)用依賴系統(tǒng)的后臺(tái)服務(wù)獲取數(shù)據(jù)绵咱,甚至不需要去公共的緩存集群中查詢數(shù)據(jù)碘饼,這樣不僅可以減少系統(tǒng)調(diào)用,而且能夠避免壓垮公共緩存集群悲伶。
- 增加系統(tǒng)限流保護(hù)艾恼,防止最壞情況發(fā)生。
經(jīng)過(guò)這些優(yōu)化麸锉,系統(tǒng)架構(gòu)變成了下圖中的樣子钠绍。在這里,我們對(duì)頁(yè)面進(jìn)行了進(jìn)一步的靜態(tài)化花沉,秒殺過(guò)程中不需要刷新整個(gè)頁(yè)面柳爽,而只需要向服務(wù)端請(qǐng)求很少的動(dòng)態(tài)數(shù)據(jù)。而且碱屁,最關(guān)鍵的詳情和交易系統(tǒng)都增加了本地緩存磷脯,來(lái)提前緩存秒殺商品的信息,熱點(diǎn)數(shù)據(jù)庫(kù)也做了獨(dú)立部署娩脾,等等赵誓。
從前面的幾次升級(jí)來(lái)看,其實(shí)越到后面需要定制的地方越多,也就是越“不通用”俩功。例如幻枉,把秒殺商品緩存在每臺(tái)機(jī)器的內(nèi)存中,這種方式顯然不適合太多的商品同時(shí)進(jìn)行秒殺的情況绑雄,因?yàn)閱螜C(jī)的內(nèi)存始終有限展辞。所以要取得極致的性能,就要在其他地方(比如万牺,通用性罗珍、易用性、成本等方面)有所犧牲脚粟。
02 | 如何才能做好動(dòng)靜分離覆旱?有哪些方案可選?
數(shù)據(jù)的動(dòng)靜分離核无。不知道你之前聽(tīng)過(guò)這個(gè)解決方案嗎扣唱?不管你有沒(méi)有聽(tīng)過(guò),我都建議你先停下來(lái)思考動(dòng)靜分離的價(jià)值团南。如果你的系統(tǒng)還沒(méi)有開(kāi)始應(yīng)用動(dòng)靜分離的方案噪沙,那你也可以想想為什么沒(méi)有,是之前沒(méi)有想到吐根,還是說(shuō)業(yè)務(wù)體量根本用不著正歼?不過(guò)我可以確信地說(shuō),如果你在一個(gè)業(yè)務(wù)飛速發(fā)展的公司里拷橘,并且你在深度參與公司內(nèi)類秒殺類系統(tǒng)的架構(gòu)或者開(kāi)發(fā)工作局义,那么你遲早會(huì)想到動(dòng)靜分離的方案。為什么冗疮?很簡(jiǎn)單萄唇,秒殺的場(chǎng)景中,對(duì)于系統(tǒng)的要求其實(shí)就三個(gè)字:快术幔、準(zhǔn)另萤、穩(wěn)。
那怎么才能“快”起來(lái)呢特愿?我覺(jué)得抽象起來(lái)講仲墨,就只有兩點(diǎn),一點(diǎn)是提高單次請(qǐng)求的效率揍障,一點(diǎn)是減少?zèng)]必要的請(qǐng)求目养。今天我們聊到的“動(dòng)靜分離”其實(shí)就是瞄著這個(gè)大方向去的。
不知道你是否還記得毒嫡,最早的秒殺系統(tǒng)其實(shí)是要刷新整體頁(yè)面的癌蚁,但后來(lái)秒殺的時(shí)候幻梯,你只要點(diǎn)擊“刷新?lián)寣殹卑粹o就夠了,這種變化的本質(zhì)就是動(dòng)靜分離,分離之后,客戶端大幅度減少了請(qǐng)求的數(shù)據(jù)量杉武。這不自然就“快”了嗎?
何為動(dòng)靜數(shù)據(jù)
那到底什么才是動(dòng)靜分離呢煞躬?所謂“動(dòng)靜分離”,其實(shí)就是把用戶請(qǐng)求的數(shù)據(jù)(如 HTML 頁(yè)面)劃分為“動(dòng)態(tài)數(shù)據(jù)”和“靜態(tài)數(shù)據(jù)”逸邦。
簡(jiǎn)單來(lái)說(shuō)恩沛,“動(dòng)態(tài)數(shù)據(jù)”和“靜態(tài)數(shù)據(jù)”的主要區(qū)別就是看頁(yè)面中輸出的數(shù)據(jù)是否和 URL、瀏覽者缕减、時(shí)間雷客、地域相關(guān),以及是否含有 Cookie 等私密數(shù)據(jù)桥狡。比如說(shuō):
很多媒體類的網(wǎng)站搅裙,某一篇文章的內(nèi)容不管是你訪問(wèn)還是我訪問(wèn),它都是一樣的裹芝。所以它就是一個(gè)典型的靜態(tài)數(shù)據(jù)部逮,但是它是個(gè)動(dòng)態(tài)頁(yè)面。
我們?nèi)绻F(xiàn)在訪問(wèn)淘寶的首頁(yè)嫂易,每個(gè)人看到的頁(yè)面可能都是不一樣的甥啄,淘寶首頁(yè)中包含了很多根據(jù)訪問(wèn)者特征推薦的信息,而這些個(gè)性化的數(shù)據(jù)就可以理解為動(dòng)態(tài)數(shù)據(jù)了炬搭。
這里再?gòu)?qiáng)調(diào)一下,我們所說(shuō)的靜態(tài)數(shù)據(jù)穆桂,不能僅僅理解為傳統(tǒng)意義上完全存在磁盤上的 HTML 頁(yè)面宫盔,它也可能是經(jīng)過(guò) Java 系統(tǒng)產(chǎn)生的頁(yè)面,但是它輸出的頁(yè)面本身不包含上面所說(shuō)的那些因素享完。也就是所謂“動(dòng)態(tài)”還是“靜態(tài)”灼芭,并不是說(shuō)數(shù)據(jù)本身是否動(dòng)靜,而是數(shù)據(jù)中是否含有和訪問(wèn)者相關(guān)的個(gè)性化數(shù)據(jù)般又。
還有一點(diǎn)要注意彼绷,就是頁(yè)面中“不包含”,指的是“頁(yè)面的 HTML 源碼中不含有”茴迁,這一點(diǎn)務(wù)必要清楚寄悯。理解了靜態(tài)數(shù)據(jù)和動(dòng)態(tài)數(shù)據(jù),我估計(jì)你很容易就能想明白“動(dòng)靜分離”這個(gè)方案的來(lái)龍去脈了堕义。分離了動(dòng)靜數(shù)據(jù)猜旬,我們就可以對(duì)分離出來(lái)的靜態(tài)數(shù)據(jù)做緩存,有了緩存之后,靜態(tài)數(shù)據(jù)的“訪問(wèn)效率”自然就提高了洒擦。
那么椿争,怎樣對(duì)靜態(tài)數(shù)據(jù)做緩存呢?我在這里總結(jié)了幾個(gè)重點(diǎn)熟嫩。
第一秦踪,你應(yīng)該把靜態(tài)數(shù)據(jù)緩存到離用戶最近的地方。靜態(tài)數(shù)據(jù)就是那些相對(duì)不會(huì)變化的數(shù)據(jù)掸茅,因此我們可以把它們緩存起來(lái)椅邓。緩存到哪里呢?常見(jiàn)的就三種倦蚪,用戶瀏覽器里希坚、CDN 上或者在服務(wù)端的 Cache 中。你應(yīng)該根據(jù)情況陵且,把它們盡量緩存到離用戶最近的地方裁僧。
第二,靜態(tài)化改造就是要直接緩存 HTTP 連接慕购。相較于普通的數(shù)據(jù)緩存而言聊疲,你肯定還聽(tīng)過(guò)系統(tǒng)的靜態(tài)化改造。靜態(tài)化改造是直接緩存 HTTP 連接而不是僅僅緩存數(shù)據(jù)沪悲,如下圖所示获洲,Web 代理服務(wù)器根據(jù)請(qǐng)求 URL,直接取出對(duì)應(yīng)的 HTTP 響應(yīng)頭和響應(yīng)體然后直接返回殿如,這個(gè)響應(yīng)過(guò)程簡(jiǎn)單得連 HTTP 協(xié)議都不用重新組裝贡珊,甚至連 HTTP 請(qǐng)求頭也不需要解析。
第三涉馁,讓誰(shuí)來(lái)緩存靜態(tài)數(shù)據(jù)也很重要门岔。不同語(yǔ)言寫的 Cache 軟件處理緩存數(shù)據(jù)的效率也各不相同。以 Java 為例烤送,因?yàn)?Java 系統(tǒng)本身也有其弱點(diǎn)(比如不擅長(zhǎng)處理大量連接請(qǐng)求寒随,每個(gè)連接消耗的內(nèi)存較多,Servlet 容器解析 HTTP 協(xié)議較慢)帮坚,所以你可以不在 Java 層做緩存妻往,而是直接在 Web 服務(wù)器層上做,這樣你就可以屏蔽 Java 語(yǔ)言層面的一些弱點(diǎn)试和;而相比起來(lái)讯泣,Web 服務(wù)器(如 Nginx、Apache灰署、Varnish)也更擅長(zhǎng)處理大并發(fā)的靜態(tài)文件請(qǐng)求判帮。
如何做動(dòng)靜分離的改造
理解了動(dòng)靜態(tài)數(shù)據(jù)的“why”和“what”局嘁,接下來(lái)我們就要看“how”了。我們?nèi)绾伟褎?dòng)態(tài)頁(yè)面改造成適合緩存的靜態(tài)頁(yè)面呢晦墙?其實(shí)也很簡(jiǎn)單悦昵,就是去除前面所說(shuō)的那幾個(gè)影響因素,把它們單獨(dú)分離出來(lái)晌畅,做動(dòng)靜分離但指。
下面,我以典型的商品詳情系統(tǒng)為例來(lái)詳細(xì)介紹抗楔。這里棋凳,你可以先打開(kāi)京東或者淘寶的商品詳情頁(yè),看看這個(gè)頁(yè)面里都有哪些動(dòng)靜數(shù)據(jù)连躏。我們從以下 5 個(gè)方面來(lái)分離出動(dòng)態(tài)內(nèi)容剩岳。
- URL 唯一化。商品詳情系統(tǒng)天然地就可以做到 URL 唯一化入热,比如每個(gè)商品都由 ID 來(lái)標(biāo)識(shí)拍棕,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作為唯一的 URL 標(biāo)識(shí)。為啥要 URL 唯一呢勺良?前面說(shuō)了我們是要緩存整個(gè) HTTP 連接绰播,那么以什么作為 Key 呢?就以 URL 作為緩存的 Key尚困,例如以 id=xxx 這個(gè)格式進(jìn)行區(qū)分蠢箩。
- 分離瀏覽者相關(guān)的因素。瀏覽者相關(guān)的因素包括是否已登錄事甜,以及登錄身份等谬泌,這些相關(guān)因素我們可以單獨(dú)拆分出來(lái),通過(guò)動(dòng)態(tài)請(qǐng)求來(lái)獲取逻谦。
- 分離時(shí)間因素呵萨。服務(wù)端輸出的時(shí)間也通過(guò)動(dòng)態(tài)請(qǐng)求獲取。
- 異步化地域因素跨跨。詳情頁(yè)面上與地域相關(guān)的因素做成異步方式獲取,當(dāng)然你也可以通過(guò)動(dòng)態(tài)請(qǐng)求方式獲取囱皿,只是這里通過(guò)異步獲取更合適勇婴。
- 去掉 Cookie。服務(wù)端輸出的頁(yè)面包含的 Cookie 可以通過(guò)代碼軟件來(lái)刪除嘱腥,如 Web 服務(wù)器 Varnish 可以通過(guò) unset req.http.cookie 命令去掉 Cookie耕渴。注意,這里說(shuō)的去掉 Cookie 并不是用戶端收到的頁(yè)面就不含 Cookie 了齿兔,而是說(shuō)橱脸,在緩存的靜態(tài)數(shù)據(jù)中不含有 Cookie础米。
分離出動(dòng)態(tài)內(nèi)容之后,如何組織這些內(nèi)容頁(yè)就變得非常關(guān)鍵了添诉。這里我要提醒你一點(diǎn)屁桑,因?yàn)檫@其中很多動(dòng)態(tài)內(nèi)容都會(huì)被頁(yè)面中的其他模塊用到,如判斷該用戶是否已登錄栏赴、用戶 ID 是否匹配等蘑斧,所以這個(gè)時(shí)候我們應(yīng)該將這些信息 JSON 化(用 JSON 格式組織這些數(shù)據(jù)),以方便前端獲取须眷。
前面我們介紹里用緩存的方式來(lái)處理靜態(tài)數(shù)據(jù)竖瘾。而動(dòng)態(tài)內(nèi)容的處理通常有兩種方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
ESI 方案(或者 SSI):即在 Web 代理服務(wù)器上做動(dòng)態(tài)內(nèi)容請(qǐng)求花颗,并將請(qǐng)求插入到靜態(tài)頁(yè)面中捕传,當(dāng)用戶拿到頁(yè)面時(shí)已經(jīng)是一個(gè)完整的頁(yè)面了。這種方式對(duì)服務(wù)端性能有些影響扩劝,但是用戶體驗(yàn)較好庸论。
CSI 方案。即單獨(dú)發(fā)起一個(gè)異步 JavaScript 請(qǐng)求今野,以向服務(wù)端獲取動(dòng)態(tài)內(nèi)容葡公。這種方式服務(wù)端性能更佳,但是用戶端頁(yè)面可能會(huì)延時(shí)条霜,體驗(yàn)稍差催什。
動(dòng)靜分離的幾種架構(gòu)方案
前面我們通過(guò)改造把靜態(tài)數(shù)據(jù)和動(dòng)態(tài)數(shù)據(jù)做了分離,那么如何在系統(tǒng)架構(gòu)上進(jìn)一步對(duì)這些動(dòng)態(tài)和靜態(tài)數(shù)據(jù)重新組合宰睡,再完整地輸出給用戶呢蒲凶?這就涉及對(duì)用戶請(qǐng)求路徑進(jìn)行合理的架構(gòu)了。根據(jù)架構(gòu)上的復(fù)雜度拆内,有 3 種方案可選:實(shí)體機(jī)單機(jī)部署旋圆;統(tǒng)一 Cache 層;上 CDN麸恍。
方案 1:實(shí)體機(jī)單機(jī)部署
這種方案是將虛擬機(jī)改為實(shí)體機(jī)灵巧,以增大 Cache 的容量,并且采用了一致性 Hash 分組的方式來(lái)提升命中率抹沪。這里將 Cache 分成若干組刻肄,是希望能達(dá)到命中率和訪問(wèn)熱點(diǎn)的平衡。Hash 分組越少融欧,緩存的命中率肯定就會(huì)越高敏弃,但短板是也會(huì)使單個(gè)商品集中在一個(gè)分組中,容易導(dǎo)致 Cache 被擊穿噪馏,所以我們應(yīng)該適當(dāng)增加多個(gè)相同的分組麦到,來(lái)平衡訪問(wèn)熱點(diǎn)和命中率的問(wèn)題绿饵。這里我給出了實(shí)體機(jī)單機(jī)部署方案的結(jié)構(gòu)圖,如下:
實(shí)體機(jī)單機(jī)部署有以下幾個(gè)優(yōu)點(diǎn):
沒(méi)有網(wǎng)絡(luò)瓶頸瓶颠,而且能使用大內(nèi)存拟赊;
既能提升命中率,又能減少 Gzip 壓縮步清;
減少 Cache 失效壓力要门,因?yàn)椴捎枚〞r(shí)失效方式,例如只緩存 3 秒鐘廓啊,過(guò)期即自動(dòng)失效欢搜。
這個(gè)方案中,雖然把通常只需要虛擬機(jī)或者容器運(yùn)行的 Java 應(yīng)用換成實(shí)體機(jī)谴轮,優(yōu)勢(shì)很明顯炒瘟,它會(huì)增加單機(jī)的內(nèi)存容量,但是一定程度上也造成了 CPU 的浪費(fèi)第步,因?yàn)閱蝹€(gè)的 Java 進(jìn)程很難用完整個(gè)實(shí)體機(jī)的 CPU疮装。
另外就是,一個(gè)實(shí)體機(jī)上部署了 Java 應(yīng)用又作為 Cache 來(lái)使用粘都,這造成了運(yùn)維上的高復(fù)雜度廓推,所以這是一個(gè)折中的方案。如果你的公司里翩隧,沒(méi)有更多的系統(tǒng)有類似需求樊展,那么這樣做也比較合適,如果你們有多個(gè)業(yè)務(wù)系統(tǒng)都有靜態(tài)化改造的需求堆生,那還是建議把 Cache 層單獨(dú)抽出來(lái)公用比較合理专缠,如下面的方案 2 所示。
方案 2:統(tǒng)一 Cache 層
所謂統(tǒng)一 Cache 層淑仆,就是將單機(jī)的 Cache 統(tǒng)一分離出來(lái)涝婉,形成一個(gè)單獨(dú)的 Cache 集群。統(tǒng)一 Cache 層是個(gè)更理想的可推廣方案蔗怠,該方案的結(jié)構(gòu)圖如下:
**將 Cache 層單獨(dú)拿出來(lái)統(tǒng)一管理可以減少運(yùn)維成本墩弯,同時(shí)也方便接入其他靜態(tài)化系統(tǒng)。此外寞射,它還有一些優(yōu)點(diǎn)最住。 **
單獨(dú)一個(gè) Cache 層,可以減少多個(gè)應(yīng)用接入時(shí)使用 Cache 的成本怠惶。這樣接入的應(yīng)用只要維護(hù)自己的 Java 系統(tǒng)就好,不需要單獨(dú)維護(hù) Cache轧粟,而只關(guān)心如何使用即可
統(tǒng)一 Cache 的方案更易于維護(hù)策治,如后面加強(qiáng)監(jiān)控脓魏、配置的自動(dòng)化,只需要一套解決方案就行通惫,統(tǒng)一起來(lái)維護(hù)升級(jí)也比較方便茂翔。
可以共享內(nèi)存,最大化利用內(nèi)存履腋,不同系統(tǒng)之間的內(nèi)存可以動(dòng)態(tài)切換珊燎,從而能夠有效應(yīng)對(duì)各種攻擊。
這種方案雖然維護(hù)上更方便了遵湖,但是也帶來(lái)了其他一些問(wèn)題悔政,比如緩存更加集中,導(dǎo)致:
Cache 層內(nèi)部交換網(wǎng)絡(luò)成為瓶頸延旧;
緩存服務(wù)器的網(wǎng)卡也會(huì)是瓶頸谋国;
機(jī)器少風(fēng)險(xiǎn)較大,掛掉一臺(tái)就會(huì)影響很大一部分緩存數(shù)據(jù)迁沫。
要解決上面這些問(wèn)題芦瘾,可以再對(duì) Cache 做 Hash 分組,即一組 Cache 緩存的內(nèi)容相同集畅,這樣能夠避免熱點(diǎn)數(shù)據(jù)過(guò)度集中導(dǎo)致新的瓶頸產(chǎn)生近弟。
方案 3:上 CDN
在將整個(gè)系統(tǒng)做動(dòng)靜分離后,我們自然會(huì)想到更進(jìn)一步的方案挺智,就是將 Cache 進(jìn)一步前移到 CDN 上祷愉,因?yàn)?CDN 離用戶最近,效果會(huì)更好逃贝。
但是要想這么做谣辞,有以下幾個(gè)問(wèn)題需要解決。
失效問(wèn)題沐扳。前面我們也有提到過(guò)緩存時(shí)效的問(wèn)題泥从,不知道你有沒(méi)有理解,我再來(lái)解釋一下沪摄。談到靜態(tài)數(shù)據(jù)時(shí)躯嫉,我說(shuō)過(guò)一個(gè)關(guān)鍵詞叫“相對(duì)不變”,它的言外之意是“可能會(huì)變化”杨拐。比如一篇文章祈餐,現(xiàn)在不變,但如果你發(fā)現(xiàn)個(gè)錯(cuò)別字哄陶,是不是就會(huì)變化了帆阳?如果你的緩存時(shí)效很長(zhǎng),那用戶端在很長(zhǎng)一段時(shí)間內(nèi)看到的都是錯(cuò)的屋吨。所以蜒谤,這個(gè)方案中也是山宾,我們需要保證 CDN 可以在秒級(jí)時(shí)間內(nèi),讓分布在全國(guó)各地的 Cache 同時(shí)失效鳍徽,這對(duì) CDN 的失效系統(tǒng)要求很高资锰。
命中率問(wèn)題。Cache 最重要的一個(gè)衡量指標(biāo)就是“高命中率”阶祭,不然 Cache 的存在就失去了意義绷杜。同樣,如果將數(shù)據(jù)全部放到全國(guó)的 CDN 上濒募,必然導(dǎo)致 Cache 分散鞭盟,而 Cache 分散又會(huì)導(dǎo)致訪問(wèn)請(qǐng)求命中同一個(gè) Cache 的可能性降低,那么命中率就成為一個(gè)問(wèn)題萨咳。
發(fā)布更新問(wèn)題懊缺。如果一個(gè)業(yè)務(wù)系統(tǒng)每周都有日常業(yè)務(wù)需要發(fā)布,那么發(fā)布系統(tǒng)必須足夠簡(jiǎn)潔高效培他,而且你還要考慮有問(wèn)題時(shí)快速回滾和排查問(wèn)題的簡(jiǎn)便性鹃两。
從前面的分析來(lái)看,將商品詳情系統(tǒng)放到全國(guó)的所有 CDN 節(jié)點(diǎn)上是不太現(xiàn)實(shí)的舀凛,因?yàn)榇嬖谑?wèn)題俊扳、命中率問(wèn)題以及系統(tǒng)的發(fā)布更新問(wèn)題。那么是否可以選擇若干個(gè)節(jié)點(diǎn)來(lái)嘗試實(shí)施呢猛遍?答案是“可以”馋记,但是這樣的節(jié)點(diǎn)**需要滿足幾個(gè)條件: **
靠近訪問(wèn)量比較集中的地區(qū);
離主站相對(duì)較遠(yuǎn)懊烤;
節(jié)點(diǎn)到主站間的網(wǎng)絡(luò)比較好梯醒,而且穩(wěn)定;
節(jié)點(diǎn)容量比較大腌紧,不會(huì)占用其他 CDN 太多的資源茸习。
最后,還有一點(diǎn)也很重要壁肋,那就是:節(jié)點(diǎn)不要太多号胚。基于上面幾個(gè)因素浸遗,選擇 CDN 的二級(jí) Cache 比較合適猫胁,因?yàn)槎?jí) Cache 數(shù)量偏少,容量也更大跛锌,讓用戶的請(qǐng)求先回源的 CDN 的二級(jí) Cache 中弃秆,如果沒(méi)命中再回源站獲取數(shù)據(jù)
部署方式如下圖所示:
使用 CDN 的二級(jí) Cache 作為緩存,可以達(dá)到和當(dāng)前服務(wù)端靜態(tài)化 Cache 類似的命中率,因?yàn)楣?jié)點(diǎn)數(shù)不多菠赚,Cache 不是很分散盼樟,訪問(wèn)量也比較集中,這樣也就解決了命中率問(wèn)題锈至,同時(shí)能夠給用戶最好的訪問(wèn)體驗(yàn),是當(dāng)前比較理想的一種 CDN 化方案译秦。
除此之外峡捡,CDN 化部署方案還有以下幾個(gè)特點(diǎn):
- 把整個(gè)頁(yè)面緩存在用戶瀏覽器中;
- 如果強(qiáng)制刷新整個(gè)頁(yè)面筑悴,也會(huì)請(qǐng)求 CDN们拙;
- 實(shí)際有效請(qǐng)求,只是用戶對(duì)“刷新?lián)寣殹卑粹o的點(diǎn)擊阁吝。
這樣就把 90% 的靜態(tài)數(shù)據(jù)緩存在了用戶端或者 CDN 上砚婆,當(dāng)真正秒殺時(shí),用戶只需要點(diǎn)擊特殊的“刷新?lián)寣殹卑粹o突勇,而不需要刷新整個(gè)頁(yè)面装盯。這樣一來(lái),系統(tǒng)只是向服務(wù)端請(qǐng)求很少的有效數(shù)據(jù)甲馋,而不需要重復(fù)請(qǐng)求大量的靜態(tài)數(shù)據(jù)埂奈。秒殺的動(dòng)態(tài)數(shù)據(jù)和普通詳情頁(yè)面的動(dòng)態(tài)數(shù)據(jù)相比更少,性能也提升了 3 倍以上定躏。
所以“搶寶”這種設(shè)計(jì)思路账磺,讓我們不用刷新頁(yè)面就能夠很好地請(qǐng)求到服務(wù)端最新的動(dòng)態(tài)數(shù)據(jù)。
03 | 二八原則:有針對(duì)性地處理好系統(tǒng)的“熱點(diǎn)數(shù)據(jù)”
什么是“熱點(diǎn)”
熱點(diǎn)分為熱點(diǎn)操作和熱點(diǎn)數(shù)據(jù)痊远。所謂“熱點(diǎn)操作”垮抗,例如大量的刷新頁(yè)面、大量的添加購(gòu)物車碧聪、雙十一零點(diǎn)大量的下單等都屬于此類操作冒版。對(duì)系統(tǒng)來(lái)說(shuō),這些操作可以抽象為“讀請(qǐng)求”和“寫請(qǐng)求”矾削,這兩種熱點(diǎn)請(qǐng)求的處理方式大相徑庭壤玫,讀請(qǐng)求的優(yōu)化空間要大一些,而寫請(qǐng)求的瓶頸一般都在存儲(chǔ)層哼凯,優(yōu)化的思路就是根據(jù) CAP 理論做平衡欲间,這個(gè)內(nèi)容我在“減庫(kù)存”一文再詳細(xì)介紹。
而“熱點(diǎn)數(shù)據(jù)”比較好理解断部,那就是用戶的熱點(diǎn)請(qǐng)求對(duì)應(yīng)的數(shù)據(jù)猎贴。而熱點(diǎn)數(shù)據(jù)又分為“靜態(tài)熱點(diǎn)數(shù)據(jù)”和“動(dòng)態(tài)熱點(diǎn)數(shù)據(jù)”
所謂“靜態(tài)熱點(diǎn)數(shù)據(jù)”,就是能夠提前預(yù)測(cè)的熱點(diǎn)數(shù)據(jù)。例如她渴,我們可以通過(guò)賣家報(bào)名的方式提前篩選出來(lái)达址,通過(guò)報(bào)名系統(tǒng)對(duì)這些熱點(diǎn)商品進(jìn)行打標(biāo)。另外趁耗,我們還可以通過(guò)大數(shù)據(jù)分析來(lái)提前發(fā)現(xiàn)熱點(diǎn)商品沉唠,比如我們分析歷史成交記錄、用戶的購(gòu)物車記錄苛败,來(lái)發(fā)現(xiàn)哪些商品可能更熱門满葛、更好賣,這些都是可以提前分析出來(lái)的熱點(diǎn)罢屈。
所謂“動(dòng)態(tài)熱點(diǎn)數(shù)據(jù)”嘀韧,就是不能被提前預(yù)測(cè)到的,系統(tǒng)在運(yùn)行過(guò)程中臨時(shí)產(chǎn)生的熱點(diǎn)缠捌。例如锄贷,賣家在抖音上做了廣告,然后商品一下就火了曼月,導(dǎo)致它在短時(shí)間內(nèi)被大量購(gòu)買谊却。
由于熱點(diǎn)操作是用戶的行為,我們不好改變十嘿,但能做一些限制和保護(hù)因惭,所以本文我主要針對(duì)熱點(diǎn)數(shù)據(jù)來(lái)介紹如何進(jìn)行優(yōu)化。
發(fā)現(xiàn)熱點(diǎn)數(shù)據(jù)
前面绩衷,我介紹了如何對(duì)單個(gè)秒殺商品的頁(yè)面數(shù)據(jù)進(jìn)行動(dòng)靜分離蹦魔,以便針對(duì)性地對(duì)靜態(tài)數(shù)據(jù)做優(yōu)化處理,那么另外一個(gè)關(guān)鍵的問(wèn)題來(lái)了:如何發(fā)現(xiàn)這些秒殺商品咳燕,或者更準(zhǔn)確地說(shuō)勿决,如何發(fā)現(xiàn)熱點(diǎn)商品呢?
你可能會(huì)說(shuō)“參加秒殺的商品就是秒殺商品啊”招盲,沒(méi)錯(cuò)低缩,關(guān)鍵是系統(tǒng)怎么知道哪些商品參加了秒殺活動(dòng)呢?所以曹货,你要有一個(gè)機(jī)制提前來(lái)區(qū)分普通商品和秒殺商品咆繁。
我們從發(fā)現(xiàn)靜態(tài)熱點(diǎn)和發(fā)現(xiàn)動(dòng)態(tài)熱點(diǎn)兩個(gè)方面來(lái)看一下。
發(fā)現(xiàn)靜態(tài)熱點(diǎn)數(shù)據(jù)
如前面講的顶籽,靜態(tài)熱點(diǎn)數(shù)據(jù)可以通過(guò)商業(yè)手段玩般,例如強(qiáng)制讓賣家通過(guò)報(bào)名參加的方式提前把熱點(diǎn)商品篩選出來(lái),實(shí)現(xiàn)方式是通過(guò)一個(gè)運(yùn)營(yíng)系統(tǒng)礼饱,把參加活動(dòng)的商品數(shù)據(jù)進(jìn)行打標(biāo)坏为,然后通過(guò)一個(gè)后臺(tái)系統(tǒng)對(duì)這些熱點(diǎn)商品進(jìn)行預(yù)處理究驴,如提前進(jìn)行緩存。但是這種通過(guò)報(bào)名提前篩選的方式也會(huì)帶來(lái)新的問(wèn)題匀伏,即增加賣家的使用成本洒忧,而且實(shí)時(shí)性較差,也不太靈活够颠。不過(guò)熙侍,除了提前報(bào)名篩選這種方式,你還可以通過(guò)技術(shù)手段提前預(yù)測(cè)履磨,例如對(duì)買家每天訪問(wèn)的商品進(jìn)行大數(shù)據(jù)計(jì)算核行,然后統(tǒng)計(jì)出 TOP N 的商品,我們可以認(rèn)為這些 TOP N 的商品就是熱點(diǎn)商品蹬耘。
發(fā)現(xiàn)動(dòng)態(tài)熱點(diǎn)數(shù)據(jù)
我們可以通過(guò)賣家報(bào)名或者大數(shù)據(jù)預(yù)測(cè)這些手段來(lái)提前預(yù)測(cè)靜態(tài)熱點(diǎn)數(shù)據(jù),但這其中有一個(gè)痛點(diǎn)减余,就是實(shí)時(shí)性較差综苔,如果我們的系統(tǒng)能在秒級(jí)內(nèi)自動(dòng)發(fā)現(xiàn)熱點(diǎn)商品那就完美了。能夠動(dòng)態(tài)地實(shí)時(shí)發(fā)現(xiàn)熱點(diǎn)不僅對(duì)秒殺商品位岔,對(duì)其他熱賣商品也同樣有價(jià)值如筛,所以我們需要想辦法實(shí)現(xiàn)熱點(diǎn)的動(dòng)態(tài)發(fā)現(xiàn)功能。
這里我給出一個(gè)動(dòng)態(tài)熱點(diǎn)發(fā)現(xiàn)系統(tǒng)的具體實(shí)現(xiàn)抒抬。
1朦拖、構(gòu)建一個(gè)異步的系統(tǒng)锥涕,它可以收集交易鏈路上各個(gè)環(huán)節(jié)中的中間件產(chǎn)品的熱點(diǎn) Key,如 Nginx、緩存银萍、RPC 服務(wù)框架等這些中間件(一些中間件產(chǎn)品本身已經(jīng)有熱點(diǎn)統(tǒng)計(jì)模塊)。
2秕衙、建立一個(gè)熱點(diǎn)上報(bào)和可以按照需求訂閱的熱點(diǎn)服務(wù)的下發(fā)規(guī)范吝沫,主要目的是通過(guò)交易鏈路上各個(gè)系統(tǒng)(包括詳情、購(gòu)物車纠屋、交易涂臣、優(yōu)惠、庫(kù)存售担、物流等)訪問(wèn)的時(shí)間差赁遗,把上游已經(jīng)發(fā)現(xiàn)的熱點(diǎn)透?jìng)鹘o下游系統(tǒng),提前做好保護(hù)族铆。比如岩四,對(duì)于大促高峰期,詳情系統(tǒng)是最早知道的骑素,在統(tǒng)一接入層上 Nginx 模塊統(tǒng)計(jì)的熱點(diǎn) URL炫乓。
3刚夺、將上游系統(tǒng)收集的熱點(diǎn)數(shù)據(jù)發(fā)送到熱點(diǎn)服務(wù)臺(tái),然后下游系統(tǒng)(如交易系統(tǒng))就會(huì)知道哪些商品會(huì)被頻繁調(diào)用末捣,然后做熱點(diǎn)保護(hù)侠姑。
這里我給出了一個(gè)圖,其中用戶訪問(wèn)商品時(shí)經(jīng)過(guò)的路徑有很多箩做,我們主要是依賴前面的導(dǎo)購(gòu)頁(yè)面(包括首頁(yè)莽红、搜索頁(yè)面、商品詳情邦邦、購(gòu)物車等)提前識(shí)別哪些商品的訪問(wèn)量高安吁,通過(guò)這些系統(tǒng)中的中間件來(lái)收集熱點(diǎn)數(shù)據(jù),并記錄到日志中燃辖。
我們通過(guò)部署在每臺(tái)機(jī)器上的 Agent 把日志匯總到聚合和分析集群中鬼店,然后把符合一定規(guī)則的熱點(diǎn)數(shù)據(jù),通過(guò)訂閱分發(fā)系統(tǒng)再推送到相應(yīng)的系統(tǒng)中黔龟。你可以是把熱點(diǎn)數(shù)據(jù)填充到 Cache 中妇智,或者直接推送到應(yīng)用服務(wù)器的內(nèi)存中,還可以對(duì)這些數(shù)據(jù)進(jìn)行攔截氏身,總之下游系統(tǒng)可以訂閱這些數(shù)據(jù)巍棱,然后根據(jù)自己的需求決定如何處理這些數(shù)據(jù)。
打造熱點(diǎn)發(fā)現(xiàn)系統(tǒng)時(shí)蛋欣,我根據(jù)以往經(jīng)驗(yàn)總結(jié)了幾點(diǎn)注意事項(xiàng)航徙。
1、這個(gè)熱點(diǎn)服務(wù)后臺(tái)抓取熱點(diǎn)數(shù)據(jù)日志最好采用異步方式陷虎,因?yàn)椤爱惒健币环矫姹阌诒WC通用性到踏,另一方面又不影響業(yè)務(wù)系統(tǒng)和中間件產(chǎn)品的主流程。
2尚猿、熱點(diǎn)服務(wù)發(fā)現(xiàn)和中間件自身的熱點(diǎn)保護(hù)模塊并存夭禽,每個(gè)中間件和應(yīng)用還需要保護(hù)自己。熱點(diǎn)服務(wù)臺(tái)提供熱點(diǎn)數(shù)據(jù)的收集和訂閱服務(wù)谊路,便于把各個(gè)系統(tǒng)的熱點(diǎn)數(shù)據(jù)透明出來(lái)讹躯。
3、熱點(diǎn)發(fā)現(xiàn)要做到接近實(shí)時(shí)(3s 內(nèi)完成熱點(diǎn)數(shù)據(jù)的發(fā)現(xiàn))缠劝,因?yàn)橹挥凶龅浇咏鼘?shí)時(shí)潮梯,動(dòng)態(tài)發(fā)現(xiàn)才有意義,才能實(shí)時(shí)地對(duì)下游系統(tǒng)提供保護(hù)惨恭。
處理熱點(diǎn)數(shù)據(jù)
處理熱點(diǎn)數(shù)據(jù)通常有幾種思路:一是優(yōu)化秉馏,二是限制,三是隔離脱羡。
先來(lái)說(shuō)說(shuō)優(yōu)化萝究。優(yōu)化熱點(diǎn)數(shù)據(jù)最有效的辦法就是緩存熱點(diǎn)數(shù)據(jù)免都,如果熱點(diǎn)數(shù)據(jù)做了動(dòng)靜分離,那么可以長(zhǎng)期緩存靜態(tài)數(shù)據(jù)帆竹。但是绕娘,緩存熱點(diǎn)數(shù)據(jù)更多的是“臨時(shí)”緩存,即不管是靜態(tài)數(shù)據(jù)還是動(dòng)態(tài)數(shù)據(jù)栽连,都用一個(gè)隊(duì)列短暫地緩存數(shù)秒鐘险领,由于隊(duì)列長(zhǎng)度有限,可以采用 LRU 淘汰算法替換秒紧。
再來(lái)說(shuō)說(shuō)限制绢陌。限制更多的是一種保護(hù)機(jī)制,限制的辦法也有很多熔恢,例如對(duì)被訪問(wèn)商品的 ID 做一致性 Hash脐湾,然后根據(jù) Hash 做分桶,每個(gè)分桶設(shè)置一個(gè)處理隊(duì)列叙淌,這樣可以把熱點(diǎn)商品限制在一個(gè)請(qǐng)求隊(duì)列里沥割,防止因某些熱點(diǎn)商品占用太多的服務(wù)器資源,而使其他請(qǐng)求始終得不到服務(wù)器的處理資源凿菩。
最后介紹一下隔離。秒殺系統(tǒng)設(shè)計(jì)的第一個(gè)原則就是將這種熱點(diǎn)數(shù)據(jù)隔離出來(lái)帜讲,不要讓 1% 的請(qǐng)求影響到另外的 99%衅谷,隔離出來(lái)后也更方便對(duì)這 1% 的請(qǐng)求做針對(duì)性的優(yōu)化。
具體到“秒殺”業(yè)務(wù)似将,我們可以在以下幾個(gè)層次實(shí)現(xiàn)隔離获黔。
業(yè)務(wù)隔離。把秒殺做成一種營(yíng)銷活動(dòng)在验,賣家要參加秒殺這種營(yíng)銷活動(dòng)需要單獨(dú)報(bào)名玷氏,從技術(shù)上來(lái)說(shuō),賣家報(bào)名后對(duì)我們來(lái)說(shuō)就有了已知熱點(diǎn)腋舌,因此可以提前做好預(yù)熱盏触。
系統(tǒng)隔離。系統(tǒng)隔離更多的是運(yùn)行時(shí)的隔離块饺,可以通過(guò)分組部署的方式和另外 99% 分開(kāi)赞辩。秒殺可以申請(qǐng)單獨(dú)的域名,目的也是讓請(qǐng)求落到不同的集群中授艰。
數(shù)據(jù)隔離辨嗽。秒殺所調(diào)用的數(shù)據(jù)大部分都是熱點(diǎn)數(shù)據(jù),比如會(huì)啟用單獨(dú)的 Cache 集群或者 MySQL 數(shù)據(jù)庫(kù)來(lái)放熱點(diǎn)數(shù)據(jù)淮腾,目的也是不想 0.01% 的數(shù)據(jù)有機(jī)會(huì)影響 99.99% 數(shù)據(jù)糟需。
04 | 流量削峰這事應(yīng)該怎么做屉佳?
為什么要削峰?
我們知道服務(wù)器的處理資源是恒定的洲押,你用或者不用它的處理能力都是一樣的武花,所以出現(xiàn)峰值的話,很容易導(dǎo)致忙到處理不過(guò)來(lái)诅诱,閑的時(shí)候卻又沒(méi)有什么要處理髓堪。但是由于要保證服務(wù)質(zhì)量,我們的很多處理資源只能按照忙的時(shí)候來(lái)預(yù)估娘荡,而這會(huì)導(dǎo)致資源的一個(gè)浪費(fèi)干旁。
這就好比因?yàn)榇嬖谠绺叻搴屯砀叻宓膯?wèn)題,所以有了錯(cuò)峰限行的解決方案炮沐。削峰的存在争群,一是可以讓服務(wù)端處理變得更加平穩(wěn),二是可以節(jié)省服務(wù)器的資源成本大年。針對(duì)秒殺這一場(chǎng)景换薄,削峰從本質(zhì)上來(lái)說(shuō)就是更多地延緩用戶請(qǐng)求的發(fā)出,以便減少和過(guò)濾掉一些無(wú)效請(qǐng)求翔试,它遵從“請(qǐng)求數(shù)要盡量少”的原則轻要。
今天,我就來(lái)介紹一下流量削峰的一些操作思路:排隊(duì)垦缅、答題冲泥、分層過(guò)濾。這幾種方式都是無(wú)損(即不會(huì)損失用戶的發(fā)出請(qǐng)求)的實(shí)現(xiàn)方案壁涎,當(dāng)然還有些有損的實(shí)現(xiàn)方案凡恍,包括我們后面要介紹的關(guān)于穩(wěn)定性的一些辦法,比如限流和機(jī)器負(fù)載保護(hù)等一些強(qiáng)制措施也能達(dá)到削峰保護(hù)的目的怔球,當(dāng)然這都是不得已的一些措施嚼酝,因此就不歸類到這里了
排隊(duì)
要對(duì)流量進(jìn)行削峰,最容易想到的解決方案就是用消息隊(duì)列來(lái)緩沖瞬時(shí)流量竟坛,把同步的直接調(diào)用轉(zhuǎn)換成異步的間接推送闽巩,中間通過(guò)一個(gè)隊(duì)列在一端承接瞬時(shí)的流量洪峰,在另一端平滑地將消息推送出去担汤。在這里又官,消息隊(duì)列就像“水庫(kù)”一樣, 攔蓄上游的洪水漫试,削減進(jìn)入下游河道的洪峰流量六敬,從而達(dá)到減免洪水災(zāi)害的目的。用消息隊(duì)列來(lái)緩沖瞬時(shí)流量的方案驾荣,如下圖所示:
但是外构,如果流量峰值持續(xù)一段時(shí)間達(dá)到了消息隊(duì)列的處理上限普泡,例如本機(jī)的消息積壓達(dá)到了存儲(chǔ)空間的上限,消息隊(duì)列同樣也會(huì)被壓垮审编,這樣雖然保護(hù)了下游的系統(tǒng)撼班,但是和直接把請(qǐng)求丟棄也沒(méi)多大的區(qū)別。就像遇到洪水爆發(fā)時(shí)垒酬,即使是有水庫(kù)恐怕也無(wú)濟(jì)于事
除了消息隊(duì)列砰嘁,類似的排隊(duì)方式還有很多,例如:
1勘究、利用線程池加鎖等待也是一種常用的排隊(duì)方式矮湘;
2、先進(jìn)先出口糕、先進(jìn)后出等常用的內(nèi)存排隊(duì)算法的實(shí)現(xiàn)方式缅阳;
3、把請(qǐng)求序列化到文件中景描,然后再順序地讀文件(例如基于 MySQL binlog 的同步機(jī)制)來(lái)恢復(fù)請(qǐng)求等方式十办。
答題
你是否還記得,最早期的秒殺只是純粹地刷新頁(yè)面和點(diǎn)擊購(gòu)買按鈕超棺,它是后來(lái)才增加了答題功能的向族。那么,為什么要增加答題功能呢棠绘?
這主要是為了增加購(gòu)買的復(fù)雜度件相,從而達(dá)到兩個(gè)目的。
第一個(gè)目的是防止部分買家使用秒殺器在參加秒殺時(shí)作弊弄唧。2011 年秒殺非常火的時(shí)候霍衫,秒殺器也比較猖獗候引,因而沒(méi)有達(dá)到全民參與和營(yíng)銷的目的,所以系統(tǒng)增加了答題來(lái)限制秒殺器敦跌。增加答題后澄干,下單的時(shí)間基本控制在 2s 后,秒殺器的下單比例也大大下降柠傍。答題頁(yè)面如下圖所示麸俘。
第二個(gè)目的其實(shí)就是延緩請(qǐng)求,起到對(duì)請(qǐng)求流量進(jìn)行削峰的作用惧笛,從而讓系統(tǒng)能夠更好地支持瞬時(shí)的流量高峰从媚。這個(gè)重要的功能就是把峰值的下單請(qǐng)求拉長(zhǎng),從以前的 1s 之內(nèi)延長(zhǎng)到 2s~10s患整。這樣一來(lái)拜效,請(qǐng)求峰值基于時(shí)間分片了喷众。這個(gè)時(shí)間的分片對(duì)服務(wù)端處理并發(fā)非常重要,會(huì)大大減輕壓力紧憾。而且到千,由于請(qǐng)求具有先后順序,靠后的請(qǐng)求到來(lái)時(shí)自然也就沒(méi)有庫(kù)存了赴穗,因此根本到不了最后的下單步驟憔四,所以真正的并發(fā)寫就非常有限了。這種設(shè)計(jì)思路目前用得非常普遍般眉,如當(dāng)年支付寶的“咻一咻”了赵、微信的“搖一搖”都是類似的方式。
這里煤篙,我重點(diǎn)說(shuō)一下秒殺答題的設(shè)計(jì)思路斟览。
如上圖所示,整個(gè)秒殺答題的邏輯主要分為 3 部分辑奈。
題庫(kù)生成模塊苛茂,這個(gè)部分主要就是生成一個(gè)個(gè)問(wèn)題和答案,其實(shí)題目和答案本身并不需要很復(fù)雜鸠窗,重要的是能夠防止由機(jī)器來(lái)算出結(jié)果妓羊,即防止秒殺器來(lái)答題。
題庫(kù)的推送模塊稍计,用于在秒殺答題前躁绸,把題目提前推送給詳情系統(tǒng)和交易系統(tǒng)。題庫(kù)的推送主要是為了保證每次用戶請(qǐng)求的題目是唯一的臣嚣,目的也是防止答題作弊净刮。
題目的圖片生成模塊,用于把題目生成為圖片格式硅则,并且在圖片里增加一些干擾因素淹父。這也同樣是為防止機(jī)器直接來(lái)答題,它要求只有人才能理解題目本身的含義怎虫。這里還要注意一點(diǎn)暑认,由于答題時(shí)網(wǎng)絡(luò)比較擁擠,我們應(yīng)該把題目的圖片提前推送到 CDN 上并且要進(jìn)行預(yù)熱大审,不然的話當(dāng)用戶真正請(qǐng)求題目時(shí)蘸际,圖片可能加載比較慢,從而影響答題的體驗(yàn)徒扶。
其實(shí)真正答題的邏輯比較簡(jiǎn)單粮彤,很好理解:當(dāng)用戶提交的答案和題目對(duì)應(yīng)的答案做比較,如果通過(guò)了就繼續(xù)進(jìn)行下一步的下單邏輯,否則就失敗驾诈。我們可以把問(wèn)題和答案用下面這樣的 key 來(lái)進(jìn)行 MD5 加密:
問(wèn)題 key:userId+itemId+question_Id+time+PK
答案 key:userId+itemId+answer+PK
驗(yàn)證的邏輯如下圖所示:
注意缠诅,這里面的驗(yàn)證邏輯,除了驗(yàn)證問(wèn)題的答案以外乍迄,還包括用戶本身身份的驗(yàn)證管引,例如是否已經(jīng)登錄、用戶的 Cookie 是否完整闯两、用戶是否重復(fù)頻繁提交等褥伴。
除了做正確性驗(yàn)證,我們還可以對(duì)提交答案的時(shí)間做些限制漾狼,例如從開(kāi)始答題到接受答案要超過(guò) 1s重慢,因?yàn)樾∮?1s 是人為操作的可能性很小,這樣也能防止機(jī)器答題的情況逊躁。
分層過(guò)濾
前面介紹的排隊(duì)和答題要么是少發(fā)請(qǐng)求似踱,要么對(duì)發(fā)出來(lái)的請(qǐng)求進(jìn)行緩沖,而針對(duì)秒殺場(chǎng)景還有一種方法稽煤,就是對(duì)請(qǐng)求進(jìn)行分層過(guò)濾核芽,從而過(guò)濾掉一些無(wú)效的請(qǐng)求。分層過(guò)濾其實(shí)就是采用“漏斗”式設(shè)計(jì)來(lái)處理請(qǐng)求的酵熙,如下圖所示轧简。
假如請(qǐng)求分別經(jīng)過(guò) CDN、前臺(tái)讀系統(tǒng)(如商品詳情系統(tǒng))匾二、后臺(tái)系統(tǒng)(如交易系統(tǒng))和數(shù)據(jù)庫(kù)這幾層哮独,那么:
大部分?jǐn)?shù)據(jù)和流量在用戶瀏覽器或者 CDN 上獲取,這一層可以攔截大部分?jǐn)?shù)據(jù)的讀炔烀辍皮璧;
經(jīng)過(guò)第二層(即前臺(tái)系統(tǒng))時(shí)數(shù)據(jù)(包括強(qiáng)一致性的數(shù)據(jù))盡量得走 Cache,過(guò)濾一些無(wú)效的請(qǐng)求分飞;
再到第三層后臺(tái)系統(tǒng)悴务,主要做數(shù)據(jù)的二次檢驗(yàn),對(duì)系統(tǒng)做好保護(hù)和限流浸须,這樣數(shù)據(jù)量和請(qǐng)求就進(jìn)一步減少惨寿;
最后在數(shù)據(jù)層完成數(shù)據(jù)的強(qiáng)一致性校驗(yàn)邦泄。
這樣就像漏斗一樣删窒,盡量把數(shù)據(jù)量和請(qǐng)求量一層一層地過(guò)濾和減少了。
分層過(guò)濾的核心思想是:在不同的層次盡可能地過(guò)濾掉無(wú)效請(qǐng)求顺囊,讓“漏斗”最末端的才是有效請(qǐng)求肌索。而要達(dá)到這種效果,我們就必須對(duì)數(shù)據(jù)做分層的校驗(yàn)特碳。
**分層校驗(yàn)的基本原則是: **
將動(dòng)態(tài)請(qǐng)求的讀數(shù)據(jù)緩存(Cache)在 Web 端诚亚,過(guò)濾掉無(wú)效的數(shù)據(jù)讀晕换;
對(duì)讀數(shù)據(jù)不做強(qiáng)一致性校驗(yàn),減少因?yàn)橐恢滦孕r?yàn)產(chǎn)生瓶頸的問(wèn)題站宗;
對(duì)寫數(shù)據(jù)進(jìn)行基于時(shí)間的合理分片闸准,過(guò)濾掉過(guò)期的失效請(qǐng)求;
對(duì)寫請(qǐng)求做限流保護(hù)梢灭,將超出系統(tǒng)承載能力的請(qǐng)求過(guò)濾掉夷家;
對(duì)寫數(shù)據(jù)進(jìn)行強(qiáng)一致性校驗(yàn),只保留最后有效的數(shù)據(jù)敏释。
05 | 影響性能的因素有哪些库快?又該如何提高系統(tǒng)的性能?
影響性能的因素
那么钥顽,哪些因素對(duì)性能有影響呢义屏?在回答這個(gè)問(wèn)題之前,我們先定義一下“性能”蜂大,服務(wù)設(shè)備不同對(duì)性能的定義也是不一樣的闽铐,例如 CPU 主要看主頻、磁盤主要看 IOPS(Input/Output Operations Per Second县爬,即每秒進(jìn)行讀寫操作的次數(shù))阳啥。
而今天我們討論的主要是系統(tǒng)服務(wù)端性能,一般用 QPS(Query Per Second财喳,每秒請(qǐng)求數(shù))來(lái)衡量察迟,還有一個(gè)影響和 QPS 也息息相關(guān),那就是響應(yīng)時(shí)間(Response Time耳高,RT)扎瓶,它可以理解為服務(wù)器處理響應(yīng)的耗時(shí)。
正常情況下響應(yīng)時(shí)間(RT)越短泌枪,一秒鐘處理的請(qǐng)求數(shù)(QPS)自然也就會(huì)越多概荷,這在單線程處理的情況下看起來(lái)是線性的關(guān)系,即我們只要把每個(gè)請(qǐng)求的響應(yīng)時(shí)間降到最低碌燕,那么性能就會(huì)最高误证。
但是你可能想到響應(yīng)時(shí)間總有一個(gè)極限,不可能無(wú)限下降修壕,所以又出現(xiàn)了另外一個(gè)維度愈捅,即通過(guò)多線程,來(lái)處理請(qǐng)求慈鸠。這樣理論上就變成了“總 QPS =(1000ms / 響應(yīng)時(shí)間)× 線程數(shù)量”蓝谨,這樣性能就和兩個(gè)因素相關(guān)了,一個(gè)是一次響應(yīng)的服務(wù)端耗時(shí),一個(gè)是處理請(qǐng)求的線程數(shù)譬巫。
首先咖楣,我們先來(lái)看看響應(yīng)時(shí)間和 QPS 有啥關(guān)系。
對(duì)于大部分的 Web 系統(tǒng)而言芦昔,響應(yīng)時(shí)間一般都是由 CPU 執(zhí)行時(shí)間和線程等待時(shí)間(比如 RPC诱贿、IO 等待、Sleep咕缎、Wait 等)組成瘪松,即服務(wù)器在處理一個(gè)請(qǐng)求時(shí),一部分是 CPU 本身在做運(yùn)算锨阿,還有一部分是在各種等待宵睦。
如果代理服務(wù)器本身沒(méi)有 CPU 消耗,我們?cè)诿看谓o代理服務(wù)器代理的請(qǐng)求加個(gè)延時(shí)墅诡,即增加響應(yīng)時(shí)間壳嚎,但是這對(duì)代理服務(wù)器本身的吞吐量并沒(méi)有多大的影響,因?yàn)榇矸?wù)器本身的資源并沒(méi)有被消耗末早,可以通過(guò)增加代理服務(wù)器的處理線程數(shù)烟馅,來(lái)彌補(bǔ)響應(yīng)時(shí)間對(duì)代理服務(wù)器的 QPS 的影響。
其實(shí)然磷,真正對(duì)性能有影響的是 CPU 的執(zhí)行時(shí)間郑趁。這也很好理解,因?yàn)?CPU 的執(zhí)行真正消耗了服務(wù)器的資源姿搜。經(jīng)過(guò)實(shí)際的測(cè)試寡润,如果減少 CPU 一半的執(zhí)行時(shí)間,就可以增加一倍的 QPS舅柜。
也就是說(shuō)梭纹,我們應(yīng)該致力于減少 CPU 的執(zhí)行時(shí)間。
其次致份,我們?cè)賮?lái)看看線程數(shù)對(duì) QPS 的影響变抽。
單看“總 QPS”的計(jì)算公式,你會(huì)覺(jué)得線程數(shù)越多 QPS 也就會(huì)越高氮块,但這會(huì)一直正確嗎绍载?顯然不是,線程數(shù)不是越多越好滔蝉,因?yàn)榫€程本身也消耗資源击儡,也受到其他因素的制約。例如锰提,線程越多系統(tǒng)的線程切換成本就會(huì)越高曙痘,而且每個(gè)線程也都會(huì)耗費(fèi)一定內(nèi)存。
那么立肘,設(shè)置什么樣的線程數(shù)最合理呢边坤?其實(shí)很多多線程的場(chǎng)景都有一個(gè)默認(rèn)配置,即“線程數(shù) = 2 * CPU 核數(shù) + 1”谅年。除去這個(gè)配置茧痒,還有一個(gè)根據(jù)最佳實(shí)踐得出來(lái)的公式:
線程數(shù) = [(線程等待時(shí)間 + 線程 CPU 時(shí)間) / 線程 CPU 時(shí)間] × CPU 數(shù)量
當(dāng)然,最好的辦法是通過(guò)性能測(cè)試來(lái)發(fā)現(xiàn)最佳的線程數(shù)融蹂。
換句話說(shuō)旺订,要提升性能我們就要減少 CPU 的執(zhí)行時(shí)間,另外就是要設(shè)置一個(gè)合理的并發(fā)線程數(shù)超燃,通過(guò)這兩方面來(lái)顯著提升服務(wù)器的性能区拳。
現(xiàn)在,你知道了如何來(lái)快速提升性能意乓,那接下來(lái)你估計(jì)會(huì)問(wèn)樱调,我應(yīng)該怎么發(fā)現(xiàn)系統(tǒng)哪里最消耗 CPU 資源呢?
如何發(fā)現(xiàn)瓶頸
就服務(wù)器而言届良,會(huì)出現(xiàn)瓶頸的地方有很多笆凌,例如 CPU、內(nèi)存士葫、磁盤以及網(wǎng)絡(luò)等都可能會(huì)導(dǎo)致瓶頸乞而。此外,不同的系統(tǒng)對(duì)瓶頸的關(guān)注度也不一樣慢显,例如對(duì)緩存系統(tǒng)而言爪模,制約它的是內(nèi)存,而對(duì)存儲(chǔ)型系統(tǒng)來(lái)說(shuō) I/O 更容易是瓶頸荚藻。
我們定位的場(chǎng)景是秒殺呻右,它的瓶頸更多地發(fā)生在 CPU 上。
怎樣簡(jiǎn)單地判斷 CPU 是不是瓶頸呢鞋喇?一個(gè)辦法就是看當(dāng) QPS 達(dá)到極限時(shí)声滥,你的服務(wù)器的 CPU 使用率是不是超過(guò)了 95%,如果沒(méi)有超過(guò)侦香,那么表示 CPU 還有提升的空間落塑,要么是有鎖限制,要么是有過(guò)多的本地 I/O 等待發(fā)生罐韩。
如何優(yōu)化系統(tǒng)
對(duì) Java 系統(tǒng)來(lái)說(shuō)憾赁,可以優(yōu)化的地方很多,這里我重點(diǎn)說(shuō)一下比較有效的幾種手段散吵,供你參考龙考,它們是:減少編碼蟆肆、減少序列化、Java 極致優(yōu)化晦款、并發(fā)讀優(yōu)化炎功。接下來(lái),我們分別來(lái)看一下缓溅。
1. 減少編碼
Java 的編碼運(yùn)行比較慢蛇损,這是 Java 的一大硬傷。在很多場(chǎng)景下坛怪,只要涉及字符串的操作(如輸入輸出操作淤齐、I/O 操作)都比較耗 CPU 資源,不管它是磁盤 I/O 還是網(wǎng)絡(luò) I/O袜匿,因?yàn)槎夹枰獙⒆址D(zhuǎn)換成字節(jié)更啄,而這個(gè)轉(zhuǎn)換必須編碼。每個(gè)字符的編碼都需要查表居灯,而這種查表的操作非常耗資源锈死,所以減少字符到字節(jié)或者相反的轉(zhuǎn)換、減少字符編碼會(huì)非常有成效穆壕。減少編碼就可以大大提升性能
2. 減少序列化
序列化也是 Java 性能的一大天敵待牵,減少 Java 中的序列化操作也能大大提升性能。又因?yàn)樾蛄谢呛途幋a同時(shí)發(fā)生的喇勋,所以減少序列化也就減少了編碼缨该。
序列化大部分是在 RPC 中發(fā)生的,因此避免或者減少 RPC 就可以減少序列化川背,當(dāng)然當(dāng)前的序列化協(xié)議也已經(jīng)做了很多優(yōu)化來(lái)提升性能贰拿。有一種新的方案,就是可以將多個(gè)關(guān)聯(lián)性比較強(qiáng)的應(yīng)用進(jìn)行“合并部署”熄云,而減少不同應(yīng)用之間的 RPC 也可以減少序列化的消耗膨更。
所謂“合并部署”,就是把兩個(gè)原本在不同機(jī)器上的不同應(yīng)用合并部署到一臺(tái)機(jī)器上缴允,當(dāng)然不僅僅是部署在一臺(tái)機(jī)器上荚守,還要在同一個(gè) Tomcat 容器中,且不能走本機(jī)的 Socket练般,這樣才能避免序列化的產(chǎn)生矗漾。
另外針對(duì)秒殺場(chǎng)景,我們還可以做得更極致一些薄料,接下來(lái)我們來(lái)看第 3 點(diǎn):Java 極致優(yōu)化敞贡。
3. Java 極致優(yōu)化
Java 和通用的 Web 服務(wù)器(如 Nginx 或 Apache 服務(wù)器)相比,在處理大并發(fā)的 HTTP 請(qǐng)求時(shí)要弱一點(diǎn)摄职,所以一般我們都會(huì)對(duì)大流量的 Web 系統(tǒng)做靜態(tài)化改造誊役,讓大部分請(qǐng)求和數(shù)據(jù)直接在 Nginx 服務(wù)器或者 Web 代理服務(wù)器(如 Varnish获列、Squid 等)上直接返回(這樣可以減少數(shù)據(jù)的序列化與反序列化),而 Java 層只需處理少量數(shù)據(jù)的動(dòng)態(tài)請(qǐng)求蛔垢。針對(duì)這些請(qǐng)求击孩,我們可以使用以下手段進(jìn)行優(yōu)化:
1、直接使用 Servlet 處理請(qǐng)求。避免使用傳統(tǒng)的 MVC 框架,這樣可以繞過(guò)一大堆復(fù)雜且用處不大的處理邏輯凉馆,節(jié)省 1ms 時(shí)間(具體取決于你對(duì) MVC 框架的依賴程度)
2价涝、直接輸出流數(shù)據(jù)。使用 resp.getOutputStream() 而不是 resp.getWriter() 函數(shù)验烧,可以省掉一些不變字符數(shù)據(jù)的編碼板驳,從而提升性能;數(shù)據(jù)輸出時(shí)推薦使用 JSON 而不是模板引擎(一般都是解釋執(zhí)行)來(lái)輸出頁(yè)面碍拆。
4. 并發(fā)讀優(yōu)化
也許有讀者會(huì)覺(jué)得這個(gè)問(wèn)題很容易解決若治,無(wú)非就是放到 Tair 緩存里面。集中式緩存為了保證命中率一般都會(huì)采用一致性 Hash感混,所以同一個(gè) key 會(huì)落到同一臺(tái)機(jī)器上端幼。雖然單臺(tái)緩存機(jī)器也能支撐 30w/s 的請(qǐng)求,但還是遠(yuǎn)不足以應(yīng)對(duì)像“大秒”這種級(jí)別的熱點(diǎn)商品弧满。那么婆跑,該如何徹底解決單點(diǎn)的瓶頸呢?
答案是采用應(yīng)用層的 LocalCache庭呜,即在秒殺系統(tǒng)的單機(jī)上緩存商品相關(guān)的數(shù)據(jù)滑进。
那么,又如何緩存(Cache)數(shù)據(jù)呢募谎?你需要?jiǎng)澐殖蓜?dòng)態(tài)數(shù)據(jù)和靜態(tài)數(shù)據(jù)分別進(jìn)行處理:
1扶关、像商品中的“標(biāo)題”和“描述”這些本身不變的數(shù)據(jù),會(huì)在秒殺開(kāi)始之前全量推送到秒殺機(jī)器上数冬,并一直緩存到秒殺結(jié)束节槐;
2、像庫(kù)存這類動(dòng)態(tài)數(shù)據(jù)拐纱,會(huì)采用“被動(dòng)失效”的方式緩存一定時(shí)間(一般是數(shù)秒)疯淫,失效后再去緩存拉取最新的數(shù)據(jù)。
06 | 秒殺系統(tǒng)“減庫(kù)存”設(shè)計(jì)的核心邏輯
千萬(wàn)不要超賣戳玫,這是大前提熙掺。
減庫(kù)存有哪幾種方式
在正常的電商平臺(tái)購(gòu)物場(chǎng)景中,用戶的實(shí)際購(gòu)買過(guò)程一般分為兩步:下單和付款咕宿。你想買一臺(tái) iPhone 手機(jī)币绩,在商品頁(yè)面點(diǎn)了“立即購(gòu)買”按鈕蜡秽,核對(duì)信息之后點(diǎn)擊“提交訂單”,這一步稱為下單操作缆镣。下單之后芽突,你只有真正完成付款操作才能算真正購(gòu)買,也就是俗話說(shuō)的“落袋為安”董瞻。
那如果你是架構(gòu)師寞蚌,你會(huì)在哪個(gè)環(huán)節(jié)完成減庫(kù)存的操作呢?總結(jié)來(lái)說(shuō)钠糊,減庫(kù)存操作一般有如下幾個(gè)方式:
下單減庫(kù)存挟秤,即當(dāng)買家下單后,在商品的總庫(kù)存中減去買家購(gòu)買數(shù)量抄伍。下單減庫(kù)存是最簡(jiǎn)單的減庫(kù)存方式艘刚,也是控制最精確的一種,下單時(shí)直接通過(guò)數(shù)據(jù)庫(kù)的事務(wù)機(jī)制控制商品庫(kù)存截珍,這樣一定不會(huì)出現(xiàn)超賣的情況攀甚。但是你要知道,有些人下完單可能并不會(huì)付款岗喉。
付款減庫(kù)存秋度,即買家下單后,并不立即減庫(kù)存钱床,而是等到有用戶付款后才真正減庫(kù)存静陈,否則庫(kù)存一直保留給其他買家。但因?yàn)楦犊顣r(shí)才減庫(kù)存诞丽,如果并發(fā)比較高鲸拥,有可能出現(xiàn)買家下單后付不了款的情況,因?yàn)榭赡苌唐芬呀?jīng)被其他人買走了
預(yù)扣庫(kù)存僧免,這種方式相對(duì)復(fù)雜一些刑赶,買家下單后,庫(kù)存為其保留一定的時(shí)間(如 10 分鐘)懂衩,超過(guò)這個(gè)時(shí)間撞叨,庫(kù)存將會(huì)自動(dòng)釋放,釋放后其他買家就可以繼續(xù)購(gòu)買浊洞。在買家付款前牵敷,系統(tǒng)會(huì)校驗(yàn)該訂單的庫(kù)存是否還有保留:如果沒(méi)有保留,則再次嘗試預(yù)扣法希;如果庫(kù)存不足(也就是預(yù)扣失敿喜汀)則不允許繼續(xù)付款;如果預(yù)扣成功苫亦,則完成付款并實(shí)際地減去庫(kù)存毛肋。
以上這幾種減庫(kù)存的方式都會(huì)存在一些問(wèn)題怨咪,下面我們一起來(lái)看下。
減庫(kù)存可能存在的問(wèn)題
由于購(gòu)物過(guò)程中存在兩步或者多步的操作润匙,因此在不同的操作步驟中減庫(kù)存诗眨,就會(huì)存在一些可能被惡意買家利用的漏洞,例如發(fā)生惡意下單的情況孕讳。
假如我們采用“下單減庫(kù)存”的方式匠楚,即用戶下單后就減去庫(kù)存,正常情況下厂财,買家下單后付款的概率會(huì)很高芋簿,所以不會(huì)有太大問(wèn)題。但是有一種場(chǎng)景例外蟀苛,就是當(dāng)賣家參加某個(gè)活動(dòng)時(shí)益咬,此時(shí)活動(dòng)的有效時(shí)間是商品的黃金售賣時(shí)間逮诲,如果有競(jìng)爭(zhēng)對(duì)手通過(guò)惡意下單的方式將該賣家的商品全部下單帜平,讓這款商品的庫(kù)存減為零,那么這款商品就不能正常售賣了梅鹦。要知道裆甩,這些惡意下單的人是不會(huì)真正付款的,這正是“下單減庫(kù)存”方式的不足之處齐唆。
既然“下單減庫(kù)存”可能導(dǎo)致惡意下單嗤栓,從而影響賣家的商品銷售,那么有沒(méi)有辦法解決呢箍邮?你可能會(huì)想茉帅,采用“付款減庫(kù)存”的方式是不是就可以了?的確可以锭弊。但是堪澎,“付款減庫(kù)存”又會(huì)導(dǎo)致另外一個(gè)問(wèn)題:庫(kù)存超賣。
假如有 100 件商品味滞,就可能出現(xiàn) 300 人下單成功的情況樱蛤,因?yàn)橄聠螘r(shí)不會(huì)減庫(kù)存,所以也就可能出現(xiàn)下單成功數(shù)遠(yuǎn)遠(yuǎn)超過(guò)真正庫(kù)存數(shù)的情況剑鞍,這尤其會(huì)發(fā)生在做活動(dòng)的熱門商品上昨凡。這樣一來(lái),就會(huì)導(dǎo)致很多買家下單成功但是付不了款蚁署,買家的購(gòu)物體驗(yàn)自然比較差便脊。
可以看到,不管是“下單減庫(kù)存”還是“付款減庫(kù)存”光戈,都會(huì)導(dǎo)致商品庫(kù)存不能完全和實(shí)際售賣情況對(duì)應(yīng)起來(lái)的情況就轧,看來(lái)要把商品準(zhǔn)確地賣出去還真是不容易爸ず肌!
那么妒御,既然“下單減庫(kù)存”和“付款減庫(kù)存”都有缺點(diǎn)解愤,我們能否把兩者相結(jié)合,將兩次操作進(jìn)行前后關(guān)聯(lián)起來(lái)乎莉,下單時(shí)先預(yù)扣送讲,在規(guī)定時(shí)間內(nèi)不付款再釋放庫(kù)存,即采用“預(yù)扣庫(kù)存”這種方式呢惋啃?
這種方案確實(shí)可以在一定程度上緩解上面的問(wèn)題哼鬓。但是否就徹底解決了呢?其實(shí)沒(méi)有边灭!針對(duì)惡意下單這種情況异希,雖然把有效的付款時(shí)間設(shè)置為 10 分鐘,但是惡意買家完全可以在 10 分鐘后再次下單绒瘦,或者采用一次下單很多件的方式把庫(kù)存減完称簿。針對(duì)這種情況,解決辦法還是要結(jié)合安全和反作弊的措施來(lái)制止惰帽。
例如憨降,給經(jīng)常下單不付款的買家進(jìn)行識(shí)別打標(biāo)(可以在被打標(biāo)的買家下單時(shí)不減庫(kù)存)、給某些類目設(shè)置最大購(gòu)買件數(shù)(例如该酗,參加活動(dòng)的商品一人最多只能買 3 件)授药,以及對(duì)重復(fù)下單不付款的操作進(jìn)行次數(shù)限制等。
針對(duì)“庫(kù)存超賣”這種情況呜魄,在 10 分鐘時(shí)間內(nèi)下單的數(shù)量仍然有可能超過(guò)庫(kù)存數(shù)量悔叽,遇到這種情況我們只能區(qū)別對(duì)待:對(duì)普通的商品下單數(shù)量超過(guò)庫(kù)存數(shù)量的情況,可以通過(guò)補(bǔ)貨來(lái)解決爵嗅;但是有些賣家完全不允許庫(kù)存為負(fù)數(shù)的情況娇澎,那只能在買家付款時(shí)提示庫(kù)存不足。
大型秒殺中如何減庫(kù)存操骡?
目前來(lái)看九火,業(yè)務(wù)系統(tǒng)中最常見(jiàn)的就是預(yù)扣庫(kù)存方案,像你在買機(jī)票册招、買電影票時(shí)岔激,下單后一般都有個(gè)“有效付款時(shí)間”,超過(guò)這個(gè)時(shí)間訂單自動(dòng)釋放是掰,這都是典型的預(yù)扣庫(kù)存方案虑鼎。而具體到秒殺這個(gè)場(chǎng)景,應(yīng)該采用哪種方案比較好呢?
由于參加秒殺的商品炫彩,一般都是“搶到就是賺到”匾七,所以成功下單后卻不付款的情況比較少,再加上賣家對(duì)秒殺商品的庫(kù)存有嚴(yán)格限制江兢,所以秒殺商品采用“下單減庫(kù)存”更加合理昨忆。另外,理論上由于“下單減庫(kù)存”比“預(yù)扣庫(kù)存”以及涉及第三方支付的“付款減庫(kù)存”在邏輯上更為簡(jiǎn)單杉允,所以性能上更占優(yōu)勢(shì)邑贴。
“下單減庫(kù)存”在數(shù)據(jù)一致性上,主要就是保證大并發(fā)請(qǐng)求時(shí)庫(kù)存數(shù)據(jù)不能為負(fù)數(shù)拢驾,也就是要保證數(shù)據(jù)庫(kù)中的庫(kù)存字段值不能為負(fù)數(shù),一般我們有多種解決方案:一種是在應(yīng)用程序中通過(guò)事務(wù)來(lái)判斷改基,即保證減后庫(kù)存不能為負(fù)數(shù)繁疤,否則就回滾;另一種辦法是直接設(shè)置數(shù)據(jù)庫(kù)的字段數(shù)據(jù)為無(wú)符號(hào)整數(shù)秕狰,這樣減后庫(kù)存字段值小于零時(shí)會(huì)直接執(zhí)行 SQL 語(yǔ)句來(lái)報(bào)錯(cuò)稠腊;再有一種就是使用 CASE WHEN 判斷語(yǔ)句,例如這樣的 SQL 語(yǔ)句:
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
秒殺減庫(kù)存的極致優(yōu)化
在交易環(huán)節(jié)中封恰,“庫(kù)存”是個(gè)關(guān)鍵數(shù)據(jù)麻养,也是個(gè)熱點(diǎn)數(shù)據(jù)褐啡,因?yàn)榻灰椎母鱾€(gè)環(huán)節(jié)中都可能涉及對(duì)庫(kù)存的查詢诺舔。但是,我在前面介紹分層過(guò)濾時(shí)提到過(guò)备畦,秒殺中并不需要對(duì)庫(kù)存有精確的一致性讀低飒,把庫(kù)存數(shù)據(jù)放到緩存(Cache)中,可以大大提升讀性能懂盐。解決大并發(fā)讀問(wèn)題褥赊,可以采用 LocalCache(即在秒殺系統(tǒng)的單機(jī)上緩存商品相關(guān)的數(shù)據(jù))和對(duì)數(shù)據(jù)進(jìn)行分層過(guò)濾的方式,但是像減庫(kù)存這種大并發(fā)寫無(wú)論如何還是避免不了莉恼,這也是秒殺場(chǎng)景下最為核心的一個(gè)技術(shù)難題拌喉。
因此,這里我想專門來(lái)說(shuō)一下秒殺場(chǎng)景下減庫(kù)存的極致優(yōu)化思路俐银,包括如何在緩存中減庫(kù)存以及如何在數(shù)據(jù)庫(kù)中減庫(kù)存尿背。
秒殺商品和普通商品的減庫(kù)存還是有些差異的,例如商品數(shù)量比較少捶惜,交易時(shí)間段也比較短田藐,因此這里有一個(gè)大膽的假設(shè),即能否把秒殺商品減庫(kù)存直接放到緩存系統(tǒng)中實(shí)現(xiàn),也就是直接在緩存中減庫(kù)存或者在一個(gè)帶有持久化功能的緩存系統(tǒng)(如 Redis)中完成呢汽久?
如果你的秒殺商品的減庫(kù)存邏輯非常單一鹤竭,比如沒(méi)有復(fù)雜的 SKU 庫(kù)存和總庫(kù)存這種聯(lián)動(dòng)關(guān)系的話,我覺(jué)得完全可以景醇。但是如果有比較復(fù)雜的減庫(kù)存邏輯臀稚,或者需要使用事務(wù),你還是必須在數(shù)據(jù)庫(kù)中完成減庫(kù)存三痰。
由于 MySQL 存儲(chǔ)數(shù)據(jù)的特點(diǎn)烁涌,同一數(shù)據(jù)在數(shù)據(jù)庫(kù)里肯定是一行存儲(chǔ)(MySQL),因此會(huì)有大量線程來(lái)競(jìng)爭(zhēng) InnoDB 行鎖酒觅,而并發(fā)度越高時(shí)等待線程會(huì)越多撮执,TPS(Transaction Per Second,即每秒處理的消息數(shù))會(huì)下降舷丹,響應(yīng)時(shí)間(RT)會(huì)上升抒钱,數(shù)據(jù)庫(kù)的吞吐量就會(huì)嚴(yán)重受影響。
這就可能引發(fā)一個(gè)問(wèn)題颜凯,就是單個(gè)熱點(diǎn)商品會(huì)影響整個(gè)數(shù)據(jù)庫(kù)的性能谋币, 導(dǎo)致 0.01% 的商品影響 99.99% 的商品的售賣,這是我們不愿意看到的情況症概。一個(gè)解決思路是遵循前面介紹的原則進(jìn)行隔離蕾额,把熱點(diǎn)商品放到單獨(dú)的熱點(diǎn)庫(kù)中。但是這無(wú)疑會(huì)帶來(lái)維護(hù)上的麻煩彼城,比如要做熱點(diǎn)數(shù)據(jù)的動(dòng)態(tài)遷移以及單獨(dú)的數(shù)據(jù)庫(kù)等诅蝶。
而分離熱點(diǎn)商品到單獨(dú)的數(shù)據(jù)庫(kù)還是沒(méi)有解決并發(fā)鎖的問(wèn)題,我們應(yīng)該怎么辦呢募壕?要解決并發(fā)鎖的問(wèn)題调炬,有兩種辦法:
應(yīng)用層做排隊(duì)。按照商品維度設(shè)置隊(duì)列順序執(zhí)行舱馅,這樣能減少同一臺(tái)機(jī)器對(duì)數(shù)據(jù)庫(kù)同一行記錄進(jìn)行操作的并發(fā)度缰泡,同時(shí)也能控制單個(gè)商品占用數(shù)據(jù)庫(kù)連接的數(shù)量,防止熱點(diǎn)商品占用太多的數(shù)據(jù)庫(kù)連接代嗤。
數(shù)據(jù)庫(kù)層做排隊(duì)棘钞。應(yīng)用層只能做到單機(jī)的排隊(duì),但是應(yīng)用機(jī)器數(shù)本身很多干毅,這種排隊(duì)方式控制并發(fā)的能力仍然有限宜猜,所以如果能在數(shù)據(jù)庫(kù)層做全局排隊(duì)是最理想的。阿里的數(shù)據(jù)庫(kù)團(tuán)隊(duì)開(kāi)發(fā)了針對(duì)這種 MySQL 的 InnoDB 層上的補(bǔ)丁程序(patch)溶锭,可以在數(shù)據(jù)庫(kù)層上對(duì)單行記錄做到并發(fā)排隊(duì)宝恶。
你可能有疑問(wèn)了,排隊(duì)和鎖競(jìng)爭(zhēng)不都是要等待嗎,有啥區(qū)別垫毙?
如果熟悉 MySQL 的話霹疫,你會(huì)知道 InnoDB 內(nèi)部的死鎖檢測(cè),以及 MySQL Server 和 InnoDB 的切換會(huì)比較消耗性能综芥,淘寶的 MySQL 核心團(tuán)隊(duì)還做了很多其他方面的優(yōu)化丽蝎,如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的補(bǔ)丁程序,配合在 SQL 里面加提示(hint)膀藐,在事務(wù)里不需要等待應(yīng)用層提交(COMMIT)屠阻,而在數(shù)據(jù)執(zhí)行完最后一條 SQL 后,直接根據(jù) TARGET_AFFECT_ROW 的結(jié)果進(jìn)行提交或回滾额各,可以減少網(wǎng)絡(luò)等待時(shí)間(平均約 0.7ms)国觉。據(jù)我所知,目前阿里 MySQL 團(tuán)隊(duì)已經(jīng)將包含這些補(bǔ)丁程序的 MySQL 開(kāi)源虾啦。
另外麻诀,數(shù)據(jù)更新問(wèn)題除了前面介紹的熱點(diǎn)隔離和排隊(duì)處理之外,還有些場(chǎng)景(如對(duì)商品的 lastmodifytime 字段的)更新會(huì)非常頻繁傲醉,在某些場(chǎng)景下這些多條 SQL 是可以合并的蝇闭,一定時(shí)間內(nèi)只要執(zhí)行最后一條 SQL 就行了,以便減少對(duì)數(shù)據(jù)庫(kù)的更新操作硬毕。
本文內(nèi)容取自許令波老師《如何設(shè)計(jì)一個(gè)秒殺系統(tǒng)》這一課程的部分內(nèi)容呻引。