當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]每到節(jié)假日期間,一二線城市返鄉(xiāng)、外出游玩的人們幾乎都面臨著一個(gè)問(wèn)題:搶火車(chē)票!雖然現(xiàn)在大多數(shù)情況下都能訂到票,但是放票瞬間即無(wú)票的場(chǎng)景,相信大家都深有體會(huì)。尤其是春節(jié)期間,大家不僅使用12306,還會(huì)考慮“智行”和其他的搶票軟件,全國(guó)上下幾億人在這段時(shí)間都在搶票?!?2306服務(wù)...

12306搶票:極限并發(fā)帶來(lái)的思考


每到節(jié)假日期間,一二線城市返鄉(xiāng)、外出游玩的人們幾乎都面臨著一個(gè)問(wèn)題:搶火車(chē)票!雖然現(xiàn)在大多數(shù)情況下都能訂到票,但是放票瞬間即無(wú)票的場(chǎng)景,相信大家都深有體會(huì)。尤其是春節(jié)期間,大家不僅使用12306,還會(huì)考慮“智行”和其他的搶票軟件,全國(guó)上下幾億人在這段時(shí)間都在搶票。“12306服務(wù)”承受著這個(gè)世界上任何秒殺系統(tǒng)都無(wú)法超越的QPS,上百萬(wàn)的并發(fā)再正常不過(guò)了!筆者專(zhuān)門(mén)研究了一下“12306”的服務(wù)端架構(gòu),學(xué)習(xí)到了其系統(tǒng)設(shè)計(jì)上很多亮點(diǎn),在這里和大家分享一下并模擬一個(gè)例子:如何在100萬(wàn)人同時(shí)搶1萬(wàn)張火車(chē)票時(shí),系統(tǒng)提供正常、穩(wěn)定的服務(wù)。

大型高并發(fā)系統(tǒng)架構(gòu)12306搶票:極限并發(fā)帶來(lái)的思考


高并發(fā)的系統(tǒng)架構(gòu)都會(huì)采用分布式集群部署,服務(wù)上層有著層層負(fù)載均衡,并提供各種容災(zāi)手段(雙火機(jī)房、節(jié)點(diǎn)容錯(cuò)、服務(wù)器災(zāi)備等)保證系統(tǒng)的高可用,流量也會(huì)根據(jù)不同的負(fù)載能力和配置策略均衡到不同的服務(wù)器上。下邊是一個(gè)簡(jiǎn)單的示意圖:
12306搶票:極限并發(fā)帶來(lái)的思考


負(fù)載均衡簡(jiǎn)介
上圖中描述了用戶(hù)請(qǐng)求到服務(wù)器經(jīng)歷了三層的負(fù)載均衡,下邊分別簡(jiǎn)單介紹一下這三種負(fù)載均衡:
  • OSPF(開(kāi)放式最短鏈路優(yōu)先)是一個(gè)內(nèi)部網(wǎng)關(guān)協(xié)議(Interior Gateway Protocol,簡(jiǎn)稱(chēng)IGP)。OSPF通過(guò)路由器之間通告網(wǎng)絡(luò)接口的狀態(tài)來(lái)建立鏈路狀態(tài)數(shù)據(jù)庫(kù),生成最短路徑樹(shù),OSPF會(huì)自動(dòng)計(jì)算路由接口上的Cost值,但也可以通過(guò)手工指定該接口的Cost值,手工指定的優(yōu)先于自動(dòng)計(jì)算的值。OSPF計(jì)算的Cost,同樣是和接口帶寬成反比,帶寬越高,Cost值越小。到達(dá)目標(biāo)相同Cost值的路徑,可以執(zhí)行負(fù)載均衡,最多6條鏈路同時(shí)執(zhí)行負(fù)載均衡。

  • LVS(Linux VirtualServer),它是一種集群(Cluster)技術(shù),采用IP負(fù)載均衡技術(shù)和基于內(nèi)容請(qǐng)求分發(fā)技術(shù)。調(diào)度器具有很好的吞吐率,將請(qǐng)求均衡地轉(zhuǎn)移到不同的服務(wù)器上執(zhí)行,且調(diào)度器自動(dòng)屏蔽掉服務(wù)器的故障,從而將一組服務(wù)器構(gòu)成一個(gè)高性能的、高可用的虛擬服務(wù)器。

  • Nginx想必大家都很熟悉了,是一款非常高性能的http代理/反向代理服務(wù)器,服務(wù)開(kāi)發(fā)中也經(jīng)常使用它來(lái)做負(fù)載均衡。Nginx實(shí)現(xiàn)負(fù)載均衡的方式主要有三種:輪詢(xún)、加權(quán)輪詢(xún)、ip hash輪詢(xún),下面我們就針對(duì)Nginx的加權(quán)輪詢(xún)做專(zhuān)門(mén)的配置和測(cè)試。


Nginx加權(quán)輪詢(xún)的演示
Nginx實(shí)現(xiàn)負(fù)載均衡通過(guò)upstream模塊實(shí)現(xiàn),其中加權(quán)輪詢(xún)的配置是可以給相關(guān)的服務(wù)加上一個(gè)權(quán)重值,配置的時(shí)候可能根據(jù)服務(wù)器的性能、負(fù)載能力設(shè)置相應(yīng)的負(fù)載。下面是一個(gè)加權(quán)輪詢(xún)負(fù)載的配置,我將在本地的監(jiān)聽(tīng)3001-3004端口,分別配置1,2,3,4的權(quán)重:#配置負(fù)載均衡
????upstream?load_rule?{
???????server?127.0.0.1:3001?weight=1;
???????server?127.0.0.1:3002?weight=2;
???????server?127.0.0.1:3003?weight=3;
???????server?127.0.0.1:3004?weight=4;
????}
????...
????server?{
????listen???????80;
????server_name??load_balance.com?www.load_balance.com;
????location?/?{
???????proxy_pass?http://load_rule;
????}
}

我在本地/etc/hosts目錄下配置了www.load_balance.com的虛擬域名地址,接下來(lái)使用Go語(yǔ)言開(kāi)啟四個(gè)http端口監(jiān)聽(tīng)服務(wù),下面是監(jiān)聽(tīng)在3001端口的Go程序,其他幾個(gè)只需要修改端口即可:package?main

import?(
?"net/http"
?"os"
?"strings"
)

func?main()?{
?http.HandleFunc("/buy/ticket",?handleReq)
?http.ListenAndServe(":3001",?nil)
}

//處理請(qǐng)求函數(shù),根據(jù)請(qǐng)求將響應(yīng)結(jié)果信息寫(xiě)入日志
func?handleReq(w?http.ResponseWriter,?r?*http.Request)?{
?failedMsg?:=??"handle?in?port:"
?writeLog(failedMsg,?"./stat.log")
}

//寫(xiě)入日志
func?writeLog(msg?string,?logPath?string)?{
?fd,?_?:=?os.OpenFile(logPath,?os.O_RDWR|os.O_CREATE|os.O_APPEND,?0644)
?defer?fd.Close()
?content?:=?strings.Join([]string{msg,?"\r\n"},?"3001")
?buf?:=?[]byte(content)
?fd.Write(buf)
}

我將請(qǐng)求的端口日志信息寫(xiě)到了./stat.log文件當(dāng)中,然后使用ab壓測(cè)工具做壓測(cè):ab?-n?1000?-c?100?http://www.load_balance.com/buy/ticket統(tǒng)計(jì)日志中的結(jié)果,3001-3004端口分別得到了100、200、300、400的請(qǐng)求量,這和我在Nginx中配置的權(quán)重占比很好的吻合在了一起,并且負(fù)載后的流量非常的均勻、隨機(jī)。具體的實(shí)現(xiàn)大家可以參考nginx的upsteam模塊實(shí)現(xiàn)源碼,這里推薦一篇文章:https://www.kancloud.cn/digest/understandingnginx/202607
秒殺搶購(gòu)系統(tǒng)選型12306搶票:極限并發(fā)帶來(lái)的思考


回到我們最初提到的問(wèn)題中來(lái):火車(chē)票秒殺系統(tǒng)如何在高并發(fā)情況下提供正常、穩(wěn)定的服務(wù)呢?
從上面的介紹我們知道用戶(hù)秒殺流量通過(guò)層層的負(fù)載均衡,均勻到了不同的服務(wù)器上,即使如此,集群中的單機(jī)所承受的QPS也是非常高的。如何將單機(jī)性能優(yōu)化到極致呢?要解決這個(gè)問(wèn)題,我們就要想明白一件事:通常訂票系統(tǒng)要處理生成訂單、減扣庫(kù)存、用戶(hù)支付這三個(gè)基本的階段,我們系統(tǒng)要做的事情是要保證火車(chē)票訂單不超賣(mài)、不少賣(mài),每張售賣(mài)的車(chē)票都必須支付才有效,還要保證系統(tǒng)承受極高的并發(fā)。這三個(gè)階段的先后順序改怎么分配才更加合理呢?我們來(lái)分析一下:
下單減庫(kù)存
當(dāng)用戶(hù)并發(fā)請(qǐng)求到達(dá)服務(wù)端時(shí),首先創(chuàng)建訂單,然后扣除庫(kù)存,等待用戶(hù)支付。這種順序是我們一般人首先會(huì)想到的解決方案,這種情況下也能保證訂單不會(huì)超賣(mài),因?yàn)閯?chuàng)建訂單之后就會(huì)減庫(kù)存,這是一個(gè)原子操作。但是這樣也會(huì)產(chǎn)生一些問(wèn)題,第一就是在極限并發(fā)情況下,任何一個(gè)內(nèi)存操作的細(xì)節(jié)都至關(guān)影響性能,尤其像創(chuàng)建訂單這種邏輯,一般都需要存儲(chǔ)到磁盤(pán)數(shù)據(jù)庫(kù)的,對(duì)數(shù)據(jù)庫(kù)的壓力是可想而知的;第二是如果用戶(hù)存在惡意下單的情況,只下單不支付這樣庫(kù)存就會(huì)變少,會(huì)少賣(mài)很多訂單,雖然服務(wù)端可以限制IP和用戶(hù)的購(gòu)買(mǎi)訂單數(shù)量,這也不算是一個(gè)好方法。
12306搶票:極限并發(fā)帶來(lái)的思考
支付減庫(kù)存
如果等待用戶(hù)支付了訂單在減庫(kù)存,第一感覺(jué)就是不會(huì)少賣(mài)。但是這是并發(fā)架構(gòu)的大忌,因?yàn)樵跇O限并發(fā)情況下,用戶(hù)可能會(huì)創(chuàng)建很多訂單,當(dāng)庫(kù)存減為零的時(shí)候很多用戶(hù)發(fā)現(xiàn)搶到的訂單支付不了了,這也就是所謂的“超賣(mài)”。也不能避免并發(fā)操作數(shù)據(jù)庫(kù)磁盤(pán)IO。
12306搶票:極限并發(fā)帶來(lái)的思考
預(yù)扣庫(kù)存
從上邊兩種方案的考慮,我們可以得出結(jié)論:只要?jiǎng)?chuàng)建訂單,就要頻繁操作數(shù)據(jù)庫(kù)IO。那么有沒(méi)有一種不需要直接操作數(shù)據(jù)庫(kù)IO的方案呢,這就是預(yù)扣庫(kù)存。先扣除了庫(kù)存,保證不超賣(mài),然后異步生成用戶(hù)訂單,這樣響應(yīng)給用戶(hù)的速度就會(huì)快很多;那么怎么保證不少賣(mài)呢?用戶(hù)拿到了訂單,不支付怎么辦?我們都知道現(xiàn)在訂單都有有效期,比如說(shuō)用戶(hù)五分鐘內(nèi)不支付,訂單就失效了,訂單一旦失效,就會(huì)加入新的庫(kù)存,這也是現(xiàn)在很多網(wǎng)上零售企業(yè)保證商品不少賣(mài)采用的方案。訂單的生成是異步的,一般都會(huì)放到MQ、Kafka這樣的即時(shí)消費(fèi)隊(duì)列中處理,訂單量比較少的情況下,生成訂單非??欤脩?hù)幾乎不用排隊(duì)。
12306搶票:極限并發(fā)帶來(lái)的思考


扣庫(kù)存的藝術(shù)
從上面的分析可知,顯然預(yù)扣庫(kù)存的方案最合理。我們進(jìn)一步分析扣庫(kù)存的細(xì)節(jié),這里還有很大的優(yōu)化空間,庫(kù)存存在哪里?怎樣保證高并發(fā)下,正確的扣庫(kù)存,還能快速的響應(yīng)用戶(hù)請(qǐng)求?
在單機(jī)低并發(fā)情況下,我們實(shí)現(xiàn)扣庫(kù)存通常是這樣的:
12306搶票:極限并發(fā)帶來(lái)的思考


為了保證扣庫(kù)存和生成訂單的原子性,需要采用事務(wù)處理,然后取庫(kù)存判斷、減庫(kù)存,最后提交事務(wù),整個(gè)流程有很多IO,對(duì)數(shù)據(jù)庫(kù)的操作又是阻塞的。這種方式根本不適合高并發(fā)的秒殺系統(tǒng)。
接下來(lái)我們對(duì)單機(jī)扣庫(kù)存的方案做優(yōu)化:本地扣庫(kù)存。我們把一定的庫(kù)存量分配到本地機(jī)器,直接在內(nèi)存中減庫(kù)存,然后按照之前的邏輯異步創(chuàng)建訂單。改進(jìn)過(guò)之后的單機(jī)系統(tǒng)是這樣的:
12306搶票:極限并發(fā)帶來(lái)的思考


這樣就避免了對(duì)數(shù)據(jù)庫(kù)頻繁的IO操作,只在內(nèi)存中做運(yùn)算,極大的提高了單機(jī)抗并發(fā)的能力。但是百萬(wàn)的用戶(hù)請(qǐng)求量單機(jī)是無(wú)論如何也抗不住的,雖然Nginx處理網(wǎng)絡(luò)請(qǐng)求使用epoll模型,c10k的問(wèn)題在業(yè)界早已得到了解決。但是Linux系統(tǒng)下,一切資源皆文件,網(wǎng)絡(luò)請(qǐng)求也是這樣,大量的文件描述符會(huì)使操作系統(tǒng)瞬間失去響應(yīng)。上面我們提到了Nginx的加權(quán)均衡策略,我們不妨假設(shè)將100W的用戶(hù)請(qǐng)求量平均均衡到100臺(tái)服務(wù)器上,這樣單機(jī)所承受的并發(fā)量就小了很多。然后我們每臺(tái)機(jī)器本地庫(kù)存100張火車(chē)票,100臺(tái)服務(wù)器上的總庫(kù)存還是1萬(wàn),這樣保證了庫(kù)存訂單不超賣(mài),下面是我們描述的集群架構(gòu):
12306搶票:極限并發(fā)帶來(lái)的思考


問(wèn)題接踵而至,在高并發(fā)情況下,現(xiàn)在我們還無(wú)法保證系統(tǒng)的高可用,假如這100臺(tái)服務(wù)器上有兩三臺(tái)機(jī)器因?yàn)榭覆蛔〔l(fā)的流量或者其他的原因宕機(jī)了。那么這些服務(wù)器上的訂單就賣(mài)不出去了,這就造成了訂單的少賣(mài)。要解決這個(gè)問(wèn)題,我們需要對(duì)總訂單量做統(tǒng)一的管理,這就是接下來(lái)的容錯(cuò)方案。服務(wù)器不僅要在本地減庫(kù)存,另外要遠(yuǎn)程統(tǒng)一減庫(kù)存。有了遠(yuǎn)程統(tǒng)一減庫(kù)存的操作,我們就可以根據(jù)機(jī)器負(fù)載情況,為每臺(tái)機(jī)器分配一些多余的“buffer庫(kù)存”用來(lái)防止機(jī)器中有機(jī)器宕機(jī)的情況。我們結(jié)合下面架構(gòu)圖具體分析一下:
12306搶票:極限并發(fā)帶來(lái)的思考


我們采用Redis存儲(chǔ)統(tǒng)一庫(kù)存,因?yàn)镽edis的性能非常高,號(hào)稱(chēng)單機(jī)QPS能抗10W的并發(fā)。在本地減庫(kù)存以后,如果本地有訂單,我們?cè)偃フ?qǐng)求Redis遠(yuǎn)程減庫(kù)存,本地減庫(kù)存和遠(yuǎn)程減庫(kù)存都成功了,才返回給用戶(hù)搶票成功的提示,這樣也能有效的保證訂單不會(huì)超賣(mài)。當(dāng)機(jī)器中有機(jī)器宕機(jī)時(shí),因?yàn)槊總€(gè)機(jī)器上有預(yù)留的buffer余票,所以宕機(jī)機(jī)器上的余票依然能夠在其他機(jī)器上得到彌補(bǔ),保證了不少賣(mài)。buffer余票設(shè)置多少合適呢,理論上buffer設(shè)置的越多,系統(tǒng)容忍宕機(jī)的機(jī)器數(shù)量就越多,但是buffer設(shè)置的太大也會(huì)對(duì)redis造成一定的影響。雖然Redis內(nèi)存數(shù)據(jù)庫(kù)抗并發(fā)能力非常高,請(qǐng)求依然會(huì)走一次網(wǎng)絡(luò)IO,其實(shí)搶票過(guò)程中對(duì)redis的請(qǐng)求次數(shù)是本地庫(kù)存和buffer庫(kù)存的總量,因?yàn)楫?dāng)本地庫(kù)存不足時(shí),系統(tǒng)直接返回用戶(hù)“已售罄”的信息提示,就不會(huì)再走統(tǒng)一扣庫(kù)存的邏輯,這在一定程度上也避免了巨大的網(wǎng)絡(luò)請(qǐng)求量把Redis壓跨,所以buffer值設(shè)置多少,需要架構(gòu)師對(duì)系統(tǒng)的負(fù)載能力做認(rèn)真的考量。

代碼演示12306搶票:極限并發(fā)帶來(lái)的思考


Go語(yǔ)言原生為并發(fā)設(shè)計(jì),我采用Go語(yǔ)言給大家演示一下單機(jī)搶票的具體流程。
初始化工作
Go包中的init函數(shù)先于main函數(shù)執(zhí)行,在這個(gè)階段主要做一些準(zhǔn)備性工作。我們系統(tǒng)需要做的準(zhǔn)備工作有:初始化本地庫(kù)存、初始化遠(yuǎn)程Redis存儲(chǔ)統(tǒng)一庫(kù)存的hash鍵值、初始化Redis連接池;另外還需要初始化一個(gè)大小為1的int類(lèi)型chan,目的是實(shí)現(xiàn)分布式鎖的功能,也可以直接使用讀寫(xiě)鎖或者使用Redis等其他的方式避免資源競(jìng)爭(zhēng),但使用channel更加高效,這就是Go語(yǔ)言的哲學(xué):不要通過(guò)共享內(nèi)存來(lái)通信,而要通過(guò)通信來(lái)共享內(nèi)存。Redis庫(kù)使用的是redigo,下面是代碼實(shí)現(xiàn):...
//localSpike包結(jié)構(gòu)體定義
package?localSpike

type?LocalSpike?struct?{
?LocalInStock?????int64
?LocalSalesVolume?int64
}
...
//remoteSpike對(duì)hash結(jié)構(gòu)的定義和Redis連接池
package?remoteSpike
//遠(yuǎn)程訂單存儲(chǔ)健值
type?RemoteSpikeKeys?struct?{
?SpikeOrderHashKey?string?//Redis中秒殺訂單hash結(jié)構(gòu)key
?TotalInventoryKey?string?//hash結(jié)構(gòu)中總訂單庫(kù)存key
?QuantityOfOrderKey?string?//hash結(jié)構(gòu)中已有訂單數(shù)量key
}

//初始化Redis連接池
func?NewPool()?*redis.Pool?{
?return?
本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專(zhuān)欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
關(guān)閉
關(guān)閉