前段時(shí)間,媒體公布“12306”成為全球大票務(wù)交易系統(tǒng)。
那些年熬夜刷的12306經(jīng)過多年迭代,承受著這個(gè)世界上任何秒殺系統(tǒng)都無法超越的 QPS,上百萬的并發(fā)再正常不過。那么,系統(tǒng)如何在 100 萬人同時(shí)搶 1 萬張火車票時(shí),提供穩(wěn)定的服務(wù)?
高并發(fā)的系統(tǒng)架構(gòu)都會(huì)采用分布式集群部署,服務(wù)上層有著層層負(fù)載均衡,并提供各種容災(zāi)手段(雙火機(jī)房、節(jié)點(diǎn)容錯(cuò)、服務(wù)器災(zāi)備等)保證系統(tǒng)的高可用,流量也會(huì)根據(jù)不同的負(fù)載能力和配置策略均衡到不同的服務(wù)器上。
每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
用戶秒殺流量通過層層的負(fù)載均衡,均勻到了不同的服務(wù)器上,即使如此,集群中的單機(jī)所承受的 QPS 也是非常高的。如何將單機(jī)性能優(yōu)化到極致呢?
通常訂票系統(tǒng)要處理生成訂單、減扣庫存、用戶支付這三個(gè)基本的階段。
系統(tǒng)要做的事情是要保證火車票訂單不超賣、不少賣,每張售賣的車票都必須支付才有效,還要保證系統(tǒng)承受極高的并發(fā)。
這三個(gè)階段的先后順序如何分配才合理?
下單減庫存
每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
當(dāng)用戶并發(fā)請(qǐng)求到達(dá)服務(wù)端時(shí),首先創(chuàng)建訂單,然后扣除庫存,等待用戶支付。
這種順序是一般人首先會(huì)想到的解決方案,這種情況下也能保證訂單不會(huì)超賣,因?yàn)閯?chuàng)建訂單之后就會(huì)減庫存,這是一個(gè)原子操作。
會(huì)產(chǎn)生一些問題:
支付減庫存
每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
如果等待用戶支付了訂單在減庫存,第一感覺就是不會(huì)少賣。但是這是并發(fā)架構(gòu)的大忌,因?yàn)樵跇O限并發(fā)情況下,用戶可能會(huì)創(chuàng)建很多訂單。
當(dāng)庫存減為零的時(shí)候很多用戶發(fā)現(xiàn)搶到的訂單支付不了了,這也就是所謂的“超賣”。也不能避免并發(fā)操作數(shù)據(jù)庫磁盤 IO。
每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
從上邊兩種方案的考慮,我們可以得出結(jié)論:只要?jiǎng)?chuàng)建訂單,就要頻繁操作數(shù)據(jù)庫 IO。
那么有沒有一種不需要直接操作數(shù)據(jù)庫 IO 的方案呢,這就是預(yù)扣庫存。先扣除了庫存,保證不超賣,然后異步生成用戶訂單,這樣響應(yīng)給用戶的速度就會(huì)快很多;那么怎么保證不少賣呢?用戶拿到了訂單,不支付怎么辦?
我們都知道現(xiàn)在訂單都有有效期,比如說用戶五分鐘內(nèi)不支付,訂單就失效了,訂單一旦失效,就會(huì)加入新的庫存,這也是現(xiàn)在很多網(wǎng)上零售企業(yè)保證商品不少賣采用的方案。
訂單的生成是異步的,一般都會(huì)放到 MQ、Kafka 這樣的即時(shí)消費(fèi)隊(duì)列中處理,訂單量比較少的情況下,生成訂單非???,用戶幾乎不用排隊(duì)。
從上面的分析可知,顯然預(yù)扣庫存的方案最合理。我們進(jìn)一步分析扣庫存的細(xì)節(jié),這里還有很大的優(yōu)化
空間,庫存存在哪里?怎樣保證高并發(fā)下,正確的扣庫存,還能快速的響應(yīng)用戶請(qǐng)求?
在單機(jī)低并發(fā)情況下實(shí)現(xiàn)扣庫存通常:
每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
為了保證扣庫存和生成訂單的原子性,需要采用事務(wù)處理,然后取庫存判斷、減庫存,最后提交事務(wù),整個(gè)流程有很多 IO,對(duì)數(shù)據(jù)庫的操作又是阻塞的。
這種方式根本不適合高并發(fā)的秒殺系統(tǒng)。接下來我們對(duì)單機(jī)扣庫存的方案做優(yōu)化:本地扣庫存。
我們把一定的庫存量分配到本地機(jī)器,直接在內(nèi)存中減庫存,然后按照之前的邏輯異步創(chuàng)建訂單。
改進(jìn)過之后的單機(jī)系統(tǒng)是這樣的:
每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
這樣就避免了對(duì)數(shù)據(jù)庫頻繁的 IO 操作,只在內(nèi)存中做運(yùn)算,極大的提高了單機(jī)抗并發(fā)的能力。
但是百萬的用戶請(qǐng)求量單機(jī)是無論如何也抗不住的,雖然 Nginx 處理網(wǎng)絡(luò)請(qǐng)求使用 Epoll 模型,c10k 的問題在業(yè)界早已得到了解決。
但是 Linux 系統(tǒng)下,一切資源皆文件,網(wǎng)絡(luò)請(qǐng)求也是這樣,大量的文件描述符會(huì)使操作系統(tǒng)瞬間失去響應(yīng)。
上面我們提到了 Nginx 的加權(quán)均衡策略,不妨假設(shè)將 100W 的用戶請(qǐng)求量平均均衡到 100 臺(tái)服務(wù)器上,這樣單機(jī)所承受的并發(fā)量就小了很多。
然后我們每臺(tái)機(jī)器本地庫存 100 張火車票,100 臺(tái)服務(wù)器上的總庫存還是 1 萬,這樣保證了庫存訂單不超賣,以下是集群架構(gòu):
每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
問題接踵而至,在高并發(fā)情況下,現(xiàn)在還無法保證系統(tǒng)的高可用,假如這 100 臺(tái)服務(wù)器上有兩三臺(tái)機(jī)器因?yàn)榭覆蛔〔l(fā)的流量或者其他的原因宕機(jī)了。那么這些服務(wù)器上的訂單就賣不出去了,這就造成了訂單的少賣。
要解決這個(gè)問題,我們需要對(duì)總訂單量做統(tǒng)一的管理,這就是接下來的容錯(cuò)方案。服務(wù)器不僅要在本地減庫存,另外要遠(yuǎn)程統(tǒng)一減庫存。
有了遠(yuǎn)程統(tǒng)一減庫存的操作,我們就可以根據(jù)機(jī)器負(fù)載情況,為每臺(tái)機(jī)器分配一些多余的“Buffer 庫存”用來防止機(jī)器中有機(jī)器宕機(jī)的情況。
結(jié)合下面架構(gòu)圖具體分析:
每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
采用 Redis 存儲(chǔ)統(tǒng)一庫存,因?yàn)?Redis 的性能非常高,號(hào)稱單機(jī) QPS 能抗 10W 的并發(fā)。
在本地減庫存以后,如果本地有訂單,我們?cè)偃フ?qǐng)求 Redis 遠(yuǎn)程減庫存,本地減庫存和遠(yuǎn)程減庫存都成功了,才返回給用戶搶票成功的提示,這樣也能有效的保證訂單不會(huì)超賣。
當(dāng)機(jī)器中有機(jī)器宕機(jī)時(shí),因?yàn)槊總€(gè)機(jī)器上有預(yù)留的 Buffer 余票,所以宕機(jī)機(jī)器上的余票依然能夠在其他機(jī)器上得到彌補(bǔ),保證了不少賣。
Buffer 余票設(shè)置多少合適呢,理論上 Buffer 設(shè)置的越多,系統(tǒng)容忍宕機(jī)的機(jī)器數(shù)量就越多,但是 Buffer 設(shè)置的太大也會(huì)對(duì) Redis 造成一定的影響。
雖然 Redis 內(nèi)存數(shù)據(jù)庫抗并發(fā)能力非常高,請(qǐng)求依然會(huì)走一次網(wǎng)絡(luò) IO,其實(shí)搶票過程中對(duì) Redis 的請(qǐng)求次數(shù)是本地庫存和 Buffer 庫存的總量。
因?yàn)楫?dāng)本地庫存不足時(shí),系統(tǒng)直接返回用戶“已售罄”的信息提示,就不會(huì)再走統(tǒng)一扣庫存的邏輯。
這在一定程度上也避免了巨大的網(wǎng)絡(luò)請(qǐng)求量把 Redis 壓跨,所以 Buffer 值設(shè)置多少,需要架構(gòu)師對(duì)系統(tǒng)的負(fù)載能力做認(rèn)真的考量。
Go 語言原生為并發(fā)設(shè)計(jì),我采用 Go 語言給大家演示一下單機(jī)搶票的具體流程。
初始化工作
Go 包中的 Init 函數(shù)先于 Main 函數(shù)執(zhí)行,在這個(gè)階段主要做一些準(zhǔn)備性工作。
我們系統(tǒng)需要做的準(zhǔn)備工作有:初始化本地庫存、初始化遠(yuǎn)程 Redis 存儲(chǔ)統(tǒng)一庫存的 Hash 鍵值、初始化 Redis 連接池。
另外還需要初始化一個(gè)大小為 1 的 Int 類型 Chan,目的是實(shí)現(xiàn)分布式鎖的功能。
也可以直接使用讀寫鎖或者使用 Redis 等其他的方式避免資源競爭,但使用 Channel 更加高效,這就是 Go 語言的哲學(xué):不要通過共享內(nèi)存來通信,而要通過通信來共享內(nèi)存。
Redis 庫使用的是 Redigo,代碼實(shí)現(xiàn):
本地扣庫存和統(tǒng)一扣庫存
本地扣庫存邏輯非常簡單,用戶請(qǐng)求過來,添加銷量,然后對(duì)比銷量是否大于本地庫存,返回 Bool 值:
注意這里對(duì)共享數(shù)據(jù) LocalSalesVolume 的操作是要使用鎖來實(shí)現(xiàn)的,但是因?yàn)楸镜乜蹘齑婧徒y(tǒng)一扣庫存是一個(gè)原子性操作,所以在最上層使用 Channel 來實(shí)現(xiàn),這塊后邊會(huì)講。
統(tǒng)一扣庫存操作 Redis,因?yàn)?Redis 是單線程的,而我們要實(shí)現(xiàn)從中取數(shù)據(jù),寫數(shù)據(jù)并計(jì)算一些列步驟,我們要配合 Lua 腳本打包命令,保證操作的原子性:
使用 Hash 結(jié)構(gòu)存儲(chǔ)總庫存和總銷量的信息,用戶請(qǐng)求過來時(shí),判斷總銷量是否大于庫存,然后返回相關(guān)的 Bool 值。
在啟動(dòng)服務(wù)之前,需要初始化 Redis 的初始庫存信息:
響應(yīng)用戶信息
我們開啟一個(gè) HTTP 服務(wù),監(jiān)聽在一個(gè)端口上:
做完了所有的初始化工作,接下來 handleReq 的邏輯非常清晰,判斷是否搶票成功,返回給用戶信息就可以了。
前面提到扣庫存時(shí)要考慮競態(tài)條件,這里使用 Channel 避免并發(fā)的讀寫,保證了請(qǐng)求的高效順序執(zhí)行。我們將接口的返回信息寫入到了 ./stat.log 文件方便做壓測(cè)統(tǒng)計(jì)。
單機(jī)服務(wù)測(cè)壓
開啟服務(wù),我們使用 AB 壓測(cè)工具進(jìn)行測(cè)試:
本地低配 Mac 的壓測(cè)信息:
根據(jù)指標(biāo)顯示,單機(jī)每秒就能處理 4000+ 的請(qǐng)求,正常服務(wù)器都是多核配置,處理 1W+ 的請(qǐng)求根本沒有問題。
而且查看日志發(fā)現(xiàn)整個(gè)服務(wù)過程中,請(qǐng)求都很正常,流量均勻,Redis 也很正常:
秒殺系統(tǒng)是非常復(fù)雜的,本文僅簡單介紹模擬了一下單機(jī)如何優(yōu)化到高性能,集群如何避免單點(diǎn)故障,保證訂單不超賣、不少賣的一些策略。
還涉及完整的訂單系統(tǒng)還有訂單進(jìn)度的查看,定時(shí)的從總庫存同步余票和庫存信息展示給用戶,以及用戶在訂單有效期內(nèi)不支付,釋放訂單,補(bǔ)充到庫存等等。
總之,負(fù)載均衡,分而治之,每臺(tái)機(jī)器處理好自己的請(qǐng)求,將自己的性能發(fā)揮到極致。合理的使用并發(fā)和異步,合理的壓榨 CPU,讓其發(fā)揮出應(yīng)有的價(jià)值。
文章標(biāo)題:每秒100萬請(qǐng)求,“12306”的架構(gòu)到底有多牛?
瀏覽地址:http://jinyejixie.com/news/102273.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站改版、搜索引擎優(yōu)化、動(dòng)態(tài)網(wǎng)站、定制開發(fā)、微信公眾號(hào)、定制網(wǎng)站
廣告
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源:
創(chuàng)新互聯(lián)