秒殺業(yè)務(wù)架構(gòu)優(yōu)化之路

轉(zhuǎn)載自https://www.infoq.cn/article/flash-deal-architecture-optimization/?spm=a2c4e.10696291.0.0.37db19a4VHAdp5&aly_as=ouR3rGn

一、秒殺業(yè)務(wù)為什么難做

IM 系統(tǒng),例如 QQ 或者微博,每個(gè)人都讀自己的數(shù)據(jù)(好友列表、群列表、個(gè)人信息)。

微博系統(tǒng),每個(gè)人讀你關(guān)注的人的數(shù)據(jù),一個(gè)人讀多個(gè)人的數(shù)據(jù)。

秒殺系統(tǒng),庫存只有一份,所有人會(huì)在集中的時(shí)間讀和寫這些數(shù)據(jù),多個(gè)人讀一個(gè)數(shù)據(jù)。

例如小米手機(jī)每周二的秒殺,可能手機(jī)只有 1 萬部,但瞬時(shí)進(jìn)入的流量可能是幾百幾千萬。又例如 12306 搶票,票是有限的,庫存一份,瞬時(shí)流量非常多,都讀相同的庫存。讀寫沖突,鎖非常嚴(yán)重,這是秒殺業(yè)務(wù)難的地方。那我們?cè)趺磧?yōu)化秒殺業(yè)務(wù)的架構(gòu)呢?

二、優(yōu)化方向

優(yōu)化方向有兩個(gè):

  1. 將請(qǐng)求盡量攔截在系統(tǒng)上游(不要讓鎖沖突落到數(shù)據(jù)庫上去)。傳統(tǒng)秒殺系統(tǒng)之所以掛,請(qǐng)求都?jí)旱沽撕蠖藬?shù)據(jù)層,數(shù)據(jù)讀寫鎖沖突嚴(yán)重,并發(fā)高響應(yīng)慢,幾乎所有請(qǐng)求都超時(shí),流量雖大,下單成功的有效流量甚小。以 12306 為例,一趟火車其實(shí)只有 2000 張票,200w 個(gè)人來買,基本沒有人能買成功,請(qǐng)求有效率為 0。
  2. 充分利用緩存,秒殺買票,這是一個(gè)典型的讀多些少的應(yīng)用場(chǎng)景,大部分請(qǐng)求是車次查詢,票查詢,下單和支付才是寫請(qǐng)求。一趟火車其實(shí)只有 2000 張票,200w 個(gè)人來買,最多 2000 個(gè)人下單成功,其他人都是查詢庫存,寫比例只有 0.1%,讀比例占 99.9%,非常適合使用緩存來優(yōu)化。好,后續(xù)講講怎么個(gè)“將請(qǐng)求盡量攔截在系統(tǒng)上游”法,以及怎么個(gè)“緩存”法,講講細(xì)節(jié)。

三、常見秒殺架構(gòu)

常見的站點(diǎn)架構(gòu)基本是這樣的(特別是流量上億的站點(diǎn)架構(gòu)):

秒殺業(yè)務(wù)架構(gòu)優(yōu)化之路
  1. 瀏覽器端,最上層,會(huì)執(zhí)行到一些 JS 代碼
  2. 站點(diǎn)層,這一層會(huì)訪問后端數(shù)據(jù),拼 HTML 頁面返回給瀏覽器
  3. 服務(wù)層,向上游屏蔽底層數(shù)據(jù)細(xì)節(jié),提供數(shù)據(jù)訪問
  4. 數(shù)據(jù)層,最終的庫存是存在這里的,MySQL 是一個(gè)典型(當(dāng)然還有會(huì)緩存)

這個(gè)圖雖然簡(jiǎn)單,但能形象的說明大流量高并發(fā)的秒殺業(yè)務(wù)架構(gòu),大家要記得這一張圖。

后面細(xì)細(xì)解析各個(gè)層級(jí)怎么優(yōu)化。

四、各層次優(yōu)化細(xì)節(jié)

第一層,客戶端怎么優(yōu)化(瀏覽器層,APP 層)

問大家一個(gè)問題,大家都玩過微信的搖一搖搶紅包對(duì)吧,每次搖一搖,就會(huì)往后端發(fā)送請(qǐng)求么?回顧我們下單搶票的場(chǎng)景,點(diǎn)擊了“查詢”按鈕之后,系統(tǒng)那個(gè)卡呀,進(jìn)度條漲的慢呀,作為用戶,我會(huì)不自覺的再去點(diǎn)擊“查詢”,對(duì)么?繼續(xù)點(diǎn),繼續(xù)點(diǎn),點(diǎn)點(diǎn)點(diǎn)……有用么?平白無故的增加了系統(tǒng)負(fù)載,一個(gè)用戶點(diǎn) 5 次,80% 的請(qǐng)求是這么多出來的,怎么整?

  • 產(chǎn)品層面,用戶點(diǎn)擊“查詢”或者“購票”后,按鈕置灰,禁止用戶重復(fù)提交請(qǐng)求;
  • JS 層面,限制用戶在 x 秒之內(nèi)只能提交一次請(qǐng)求;

APP 層面,可以做類似的事情,雖然你瘋狂的在搖微信,其實(shí) x 秒才向后端發(fā)起一次請(qǐng)求。這就是所謂的“將請(qǐng)求盡量攔截在系統(tǒng)上游”,越上游越好,瀏覽器層,APP 層就給攔住,這樣就能擋住 80%+ 的請(qǐng)求,這種辦法只能攔住普通用戶(但 99% 的用戶是普通用戶)對(duì)于群內(nèi)的高端程序員是攔不住的。

FireBug 一抓包,HTTP 長(zhǎng)啥樣都知道,JS 是萬萬攔不住程序員寫 for 循環(huán),調(diào)用 HTTP 接口的,這部分請(qǐng)求怎么處理?

第二層,站點(diǎn)層面的請(qǐng)求攔截

怎么攔截?怎么防止程序員寫 for 循環(huán)調(diào)用,有去重依據(jù)么?IP?cookie-id?…想復(fù)雜了,這類業(yè)務(wù)都需要登錄,用 uid 即可。在站點(diǎn)層面,對(duì) uid 進(jìn)行請(qǐng)求計(jì)數(shù)和去重,甚至不需要統(tǒng)一存儲(chǔ)計(jì)數(shù),直接站點(diǎn)層內(nèi)存存儲(chǔ)(這樣計(jì)數(shù)會(huì)不準(zhǔn),但最簡(jiǎn)單)。一個(gè) uid,5 秒只準(zhǔn)透過 1 個(gè)請(qǐng)求,這樣又能攔住 99% 的 for 循環(huán)請(qǐng)求。

5s 只透過一個(gè)請(qǐng)求,其余的請(qǐng)求怎么辦?緩存,頁面緩存,同一個(gè) uid,限制訪問頻度,做頁面緩存,x 秒內(nèi)到達(dá)站點(diǎn)層的請(qǐng)求,均返回同一頁面。同一個(gè) item 的查詢,例如車次,做頁面緩存,x 秒內(nèi)到達(dá)站點(diǎn)層的請(qǐng)求,均返回同一頁面。如此限流,既能保證用戶有良好的用戶體驗(yàn)(沒有返回 404)又能保證系統(tǒng)的健壯性(利用頁面緩存,把請(qǐng)求攔截在站點(diǎn)層了)。

頁面緩存不一定要保證所有站點(diǎn)返回一致的頁面,直接放在每個(gè)站點(diǎn)的內(nèi)存也是可以的。優(yōu)點(diǎn)是簡(jiǎn)單,壞處是 HTTP 請(qǐng)求落到不同的站點(diǎn),返回的車票數(shù)據(jù)可能不一樣,這是站點(diǎn)層的請(qǐng)求攔截與緩存優(yōu)化。

好,這個(gè)方式攔住了寫 for 循環(huán)發(fā) HTTP 請(qǐng)求的程序員,有些高端程序員(黑客)控制了 10w 個(gè)肉雞,手里有 10w 個(gè) uid,同時(shí)發(fā)請(qǐng)求(先不考慮實(shí)名制的問題,小米搶手機(jī)不需要實(shí)名制),這下怎么辦,站點(diǎn)層按照 uid 限流攔不住了。

第三層 服務(wù)層來攔截(反正就是不要讓請(qǐng)求落到數(shù)據(jù)庫上去)

服務(wù)層怎么攔截?大哥,我是服務(wù)層,我清楚的知道小米只有 1 萬部手機(jī),我清楚的知道一列火車只有 2000 張車票,我透 10w 個(gè)請(qǐng)求去數(shù)據(jù)庫有什么意義呢?沒錯(cuò),請(qǐng)求隊(duì)列!

對(duì)于寫請(qǐng)求,做請(qǐng)求隊(duì)列,每次只透有限的寫請(qǐng)求去數(shù)據(jù)層(下訂單,支付這樣的寫業(yè)務(wù)):

  • 1w 部手機(jī),只透 1w 個(gè)下單請(qǐng)求去 db:
  • 3k 張火車票,只透 3k 個(gè)下單請(qǐng)求去 db。

如果均成功再放下一批,如果庫存不夠則隊(duì)列里的寫請(qǐng)求全部返回“已售完”。

對(duì)于讀請(qǐng)求,怎么優(yōu)化?Cache 抗,不管是 memcached 還是 redis,單機(jī)抗個(gè)每秒 10w 應(yīng)該都是沒什么問題的。如此限流,只有非常少的寫請(qǐng)求,和非常少的讀緩存 mis 的請(qǐng)求會(huì)透到數(shù)據(jù)層去,又有 99.9% 的請(qǐng)求被攔住了。

當(dāng)然,還有業(yè)務(wù)規(guī)則上的一些優(yōu)化。回想 12306 所做的,分時(shí)分段售票,原來統(tǒng)一 10 點(diǎn)賣票,現(xiàn)在 8 點(diǎn),8 點(diǎn)半,9 點(diǎn),... 每隔半個(gè)小時(shí)放出一批:將流量攤勻。

其次,數(shù)據(jù)粒度的優(yōu)化:你去購票,對(duì)于余票查詢這個(gè)業(yè)務(wù),票剩了 58 張,還是 26 張,你真的關(guān)注么,其實(shí)我們只關(guān)心有票和無票?流量大的時(shí)候,做一個(gè)粗粒度的 “有票”“無票”緩存即可。

第三,一些業(yè)務(wù)邏輯的異步:例如 下單業(yè)務(wù)與 支付業(yè)務(wù)的分離。這些優(yōu)化都是結(jié)合 業(yè)務(wù) 來的,我之前分享過一個(gè)觀點(diǎn)“一切脫離業(yè)務(wù)的架構(gòu)設(shè)計(jì)都是耍流氓”架構(gòu)的優(yōu)化也要針對(duì)業(yè)務(wù)。

最后是數(shù)據(jù)庫層

瀏覽器攔截了 80%,站點(diǎn)層攔截了 99.9% 并做了頁面緩存,服務(wù)層又做了寫請(qǐng)求隊(duì)列與數(shù)據(jù)緩存,每次透到數(shù)據(jù)庫層的請(qǐng)求都是可控的。db 基本就沒什么壓力了,閑庭信步,單機(jī)也能扛得住,還是那句話,庫存是有限的,小米的產(chǎn)能有限,透這么多請(qǐng)求來數(shù)據(jù)庫沒有意義。

全部透到數(shù)據(jù)庫,100w 個(gè)下單,0 個(gè)成功,請(qǐng)求有效率 0%。透 3k 到數(shù)據(jù),全部成功,請(qǐng)求有效率 100%。

五、總結(jié)

上文應(yīng)該描述的非常清楚了,沒什么總結(jié)了,對(duì)于秒殺系統(tǒng),再次重復(fù)下我個(gè)人經(jīng)驗(yàn)的兩個(gè)架構(gòu)優(yōu)化思路:

  1. 盡量將請(qǐng)求攔截在系統(tǒng)上游(越上游越好);
  2. 讀多寫少的常用多使用緩存(緩存抗讀壓力);

瀏覽器和 APP:做限速。 站點(diǎn)層:按照 uid 做限速,做頁面緩存。 服務(wù)層:按照業(yè)務(wù)做寫請(qǐng)求隊(duì)列控制流量,做數(shù)據(jù)緩存。 數(shù)據(jù)層:閑庭信步。 以及結(jié)合業(yè)務(wù)做優(yōu)化

六、Q&A

問題 1、按你的架構(gòu),其實(shí)壓力最大的反而是站點(diǎn)層,假設(shè)真實(shí)有效的請(qǐng)求數(shù)有 1000 萬,不太可能限制請(qǐng)求連接數(shù)吧,那么這部分的壓力怎么處理?

答:每秒鐘的并發(fā)可能沒有 1kw,假設(shè)有 1kw,解決方案 2 個(gè):

  1. 站點(diǎn)層是可以通過加機(jī)器擴(kuò)容的,最不濟(jì) 1k 臺(tái)機(jī)器來唄。
  2. 如果機(jī)器不夠,拋棄請(qǐng)求,拋棄 50%(50% 直接返回稍后再試),原則是要保護(hù)系統(tǒng),不能讓所有用戶都失敗。

問題 2、“控制了 10w 個(gè)肉雞,手里有 10w 個(gè) uid,同時(shí)發(fā)請(qǐng)求” 這個(gè)問題怎么解決哈?

答:上面說了,服務(wù)層寫請(qǐng)求隊(duì)列控制

問題 3: 限制訪問頻次的緩存,是否也可以用于搜索?例如 A 用戶搜索了“手機(jī)”,B 用戶搜索“手機(jī)”,優(yōu)先使用 A 搜索后生成的緩存頁面?

答:這個(gè)是可以的,這個(gè)方法也經(jīng)常用在 “動(dòng)態(tài)”運(yùn)營(yíng)活動(dòng)頁,例如短時(shí)間推送 4kw 用戶 app-push 運(yùn)營(yíng)活動(dòng),做頁面緩存。

問題 4:如果隊(duì)列處理失敗,如何處理?肉雞把隊(duì)列被撐爆了怎么辦?

答:處理失敗返回下單失敗,讓用戶再試。隊(duì)列成本很低,爆了很難吧。最壞的情況下,緩存了若干請(qǐng)求之后,后續(xù)請(qǐng)求都直接返回“無票”(隊(duì)列里已經(jīng)有 100w 請(qǐng)求了,都等著,再接受請(qǐng)求也沒有意義了)。

問題 5:站點(diǎn)層過濾的話,是把 uid 請(qǐng)求數(shù)單獨(dú)保存到各個(gè)站點(diǎn)的內(nèi)存中么?如果是這樣的話,怎么處理多臺(tái)服務(wù)器集群經(jīng)過負(fù)載均衡器將相同用戶的響應(yīng)分布到不同服務(wù)器的情況呢?還是說將站點(diǎn)層的過濾放到負(fù)載均衡前?

答:可以放在內(nèi)存,這樣的話看似一臺(tái)服務(wù)器限制了 5s 一個(gè)請(qǐng)求,全局來說(假設(shè)有 10 臺(tái)機(jī)器),其實(shí)是限制了 5s 10 個(gè)請(qǐng)求,解決辦法:

  1. 加大限制(這是建議的方案,最簡(jiǎn)單)
  2. 在 nginx 層做 7 層均衡,讓一個(gè) uid 的請(qǐng)求盡量落到同一個(gè)機(jī)器上

問題 6:服務(wù)層過濾的話,隊(duì)列是服務(wù)層統(tǒng)一的一個(gè)隊(duì)列?還是每個(gè)提供服務(wù)的服務(wù)器各一個(gè)隊(duì)列?如果是統(tǒng)一的一個(gè)隊(duì)列的話,需不需要在各個(gè)服務(wù)器提交的請(qǐng)求入隊(duì)列前進(jìn)行鎖控制?

答:可以不用統(tǒng)一一個(gè)隊(duì)列,這樣的話每個(gè)服務(wù)透過更少量的請(qǐng)求(總票數(shù) / 服務(wù)個(gè)數(shù)),這樣簡(jiǎn)單。統(tǒng)一一個(gè)隊(duì)列又復(fù)雜了。

問題 7:秒殺之后的支付完成,以及未支付取消占位,如何對(duì)剩余庫存做及時(shí)的控制更新 ?

答:數(shù)據(jù)庫里一個(gè)狀態(tài),未支付。如果超過時(shí)間,例如 45 分鐘,庫存會(huì)重新會(huì)恢復(fù)(大家熟知的“回倉”),給我們搶票的啟示是,開動(dòng)秒殺后,45 分鐘之后再試試看,說不定又有票喲。

問題 8:不同的用戶 瀏覽同一個(gè)商品 落在不同的緩存實(shí)例 顯示的庫存完全不一樣 請(qǐng)問老師怎么做緩存數(shù)據(jù)一致 或者是允許臟讀?

答:目前的架構(gòu)設(shè)計(jì),請(qǐng)求落到不同的站點(diǎn)上,數(shù)據(jù)可能不一致(頁面緩存不一樣),這個(gè)業(yè)務(wù)場(chǎng)景能接受。但數(shù)據(jù)庫層面真實(shí)數(shù)據(jù)是沒問題的。

問題 9:就算處于業(yè)務(wù)把優(yōu)化考慮 “3k 張火車票,只透 3k 個(gè)下單請(qǐng)求去 db”那這 3k 個(gè)訂單就不會(huì)發(fā)生擁堵了嗎?

答:(1)數(shù)據(jù)庫抗 3k 個(gè)寫請(qǐng)求還是 ok 的;(2)可以數(shù)據(jù)拆分;(3)如果 3k 扛不住,服務(wù)層可以控制透過去的并發(fā)數(shù)量,根據(jù)壓測(cè)情況來吧,3k 只是舉例;

問題 10:如果在站點(diǎn)層或者服務(wù)層處理后臺(tái)失敗的話,需不需要考慮對(duì)這批處理失敗的請(qǐng)求做重放?還是就直接丟棄?

答:別重放了,返回用戶查詢失敗或者下單失敗吧,架構(gòu)設(shè)計(jì)原則之一是“fail fast”。

問題 11:對(duì)于大型系統(tǒng)的秒殺,比如 12306,同時(shí)進(jìn)行的秒殺活動(dòng)很多,如何分流?

答:垂直拆分

問題 12:額外又想到一個(gè)問題。這套流程做成同步還是異步的?如果是同步的話,應(yīng)該還存在會(huì)有響應(yīng)反饋慢的情況。但如果是異步的話,如何控制能夠?qū)㈨憫?yīng)結(jié)果返回正確的請(qǐng)求方?

答:用戶層面肯定是同步的(用戶的 HTTP 請(qǐng)求是夯住的),服務(wù)層面可以同步可以異步。

問題 13:秒殺群提問:減庫存是在那個(gè)階段減呢?如果是下單鎖庫存的話,大量惡意用戶下單鎖庫存而不支付如何處理呢?

答:數(shù)據(jù)庫層面寫請(qǐng)求量很低,還好,下單不支付,等時(shí)間過完再“回倉”,之前提過了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容