“12306”是如何支撐百萬QPS的?

每到節(jié)假日期間欣范,一二線城市返鄉(xiāng)变泄、外出游玩的人們幾乎都面臨著一個(gè)問題:搶火車票!雖然現(xiàn)在大多數(shù)情況下都能訂到票恼琼,但是放票瞬間即無票的場景妨蛹,相信大家都深有體會(huì)。

尤其是春節(jié)期間晴竞,大家不僅使用 12306蛙卤,還會(huì)考慮“智行”和其他的搶票軟件,全國上下幾億人在這段時(shí)間都在搶票噩死〔眩“12306 服務(wù)”承受著這個(gè)世界上任何秒殺系統(tǒng)都無法超越的 QPS,上百萬的并發(fā)再正常不過了已维!

筆者專門研究了一下“12306”的服務(wù)端架構(gòu)行嗤,學(xué)習(xí)到了其系統(tǒng)設(shè)計(jì)上很多亮點(diǎn),在這里和大家分享一下并模擬一個(gè)例子:如何在 100 萬人同時(shí)搶 1 萬張火車票時(shí)衣摩,系統(tǒng)提供正常昂验、穩(wěn)定的服務(wù)。

Github代碼地址:

https://github.com/GuoZhaoran/spikeSystem

1. 大型高并發(fā)系統(tǒng)架構(gòu)

高并發(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è)簡單的示意圖:

1.1 負(fù)載均衡簡介

上圖中描述了用戶請求到服務(wù)器經(jīng)歷了三層的負(fù)載均衡酌予,下邊分別簡單介紹一下這三種負(fù)載均衡磺箕。

①OSPF(開放式最短鏈路優(yōu)先)是一個(gè)內(nèi)部網(wǎng)關(guān)協(xié)議(Interior Gateway Protocol,簡稱?IGP)

OSPF 通過路由器之間通告網(wǎng)絡(luò)接口的狀態(tài)來建立鏈路狀態(tài)數(shù)據(jù)庫抛虫,生成最短路徑樹松靡,OSPF 會(huì)自動(dòng)計(jì)算路由接口上的 Cost 值,但也可以通過手工指定該接口的 Cost 值建椰,手工指定的優(yōu)先于自動(dòng)計(jì)算的值雕欺。OSPF 計(jì)算的 Cost,同樣是和接口帶寬成反比,帶寬越高屠列,Cost 值越小啦逆。到達(dá)目標(biāo)相同 Cost 值的路徑,可以執(zhí)行負(fù)載均衡笛洛,最多 6 條鏈路同時(shí)執(zhí)行負(fù)載均衡夏志。

②LVS (Linux Virtual Server)

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

③Nginx

想必大家都很熟悉了浦旱,是一款非常高性能的 HTTP 代理/反向代理服務(wù)器,服務(wù)開發(fā)中也經(jīng)常使用它來做負(fù)載均衡九杂。Nginx 實(shí)現(xiàn)負(fù)載均衡的方式主要有三種:輪詢颁湖、加權(quán)輪詢、IP Hash 輪詢例隆。

下面我們就針對 Nginx 的加權(quán)輪詢做專門的配置和測試甥捺。

1.2 Nginx 加權(quán)輪詢的演示

Nginx 實(shí)現(xiàn)負(fù)載均衡通過 Upstream 模塊實(shí)現(xiàn),其中加權(quán)輪詢的配置是可以給相關(guān)的服務(wù)加上一個(gè)權(quán)重值镀层,配置的時(shí)候可能根據(jù)服務(wù)器的性能镰禾、負(fù)載能力設(shè)置相應(yīng)的負(fù)載。

下面是一個(gè)加權(quán)輪詢負(fù)載的配置唱逢,我將在本地的監(jiān)聽 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 的虛擬域名地址。

接下來使用 Go 語言開啟四個(gè) HTTP 端口監(jiān)聽服務(wù)痪枫,下面是監(jiān)聽在 3001 端口的 Go 程序织堂,其他幾個(gè)只需要修改端口即可:

package?main

import?(

????"net/http"

????"os"

????"strings"

)

func?main()?{

????http.HandleFunc("/buy/ticket",?handleReq)

????http.ListenAndServe(":3001",?nil)

}

//處理請求函數(shù),根據(jù)請求將響應(yīng)結(jié)果信息寫入日志

func?handleReq(w?http.ResponseWriter,?r?*http.Request)?{

????failedMsg?:=??"handle?in?port:"

????writeLog(failedMsg,?"./stat.log")

}

//寫入日志

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)

}

我將請求的端口日志信息寫到了 ./stat.log 文件當(dāng)中,然后使用 AB 壓測工具做壓測:

ab?-n?1000?-c?100?http://www.load_balance.com/buy/ticket

統(tǒng)計(jì)日志中的結(jié)果奶陈,3001-3004 端口分別得到了 100易阳、200、300吃粒、400?的請求量潦俺。?這和我在 Nginx 中配置的權(quán)重占比很好的吻合在了一起,并且負(fù)載后的流量非常的均勻、隨機(jī)黑竞。

具體的實(shí)現(xiàn)大家可以參考 Nginx 的 Upsteam 模塊實(shí)現(xiàn)源碼捕发,這里推薦一篇文章《Nginx 中 Upstream 機(jī)制的負(fù)載均衡》:

https://www.kancloud.cn/digest/understandingnginx/202607

2. 秒殺搶購系統(tǒng)選型

回到我們最初提到的問題中來:火車票秒殺系統(tǒng)如何在高并發(fā)情況下提供正常、穩(wěn)定的服務(wù)呢很魂?

從上面的介紹我們知道用戶秒殺流量通過層層的負(fù)載均衡扎酷,均勻到了不同的服務(wù)器上,即使如此遏匆,集群中的單機(jī)所承受的 QPS 也是非常高的法挨。如何將單機(jī)性能優(yōu)化到極致呢?

要解決這個(gè)問題幅聘,我們就要想明白一件事:?通常訂票系統(tǒng)要處理生成訂單凡纳、減扣庫存、用戶支付這三個(gè)基本的階段帝蒿。?我們系統(tǒng)要做的事情是要保證火車票訂單不超賣荐糜、不少賣,每張售賣的車票都必須支付才有效葛超,還要保證系統(tǒng)承受極高的并發(fā)暴氏。

這三個(gè)階段的先后順序該怎么分配才更加合理呢?我們來分析一下:

2.1 下單減庫存

當(dāng)用戶并發(fā)請求到達(dá)服務(wù)端時(shí)绣张,首先創(chuàng)建訂單答渔,然后扣除庫存,等待用戶支付侥涵。?這種順序是我們一般人首先會(huì)想到的解決方案沼撕,這種情況下也能保證訂單不會(huì)超賣,因?yàn)閯?chuàng)建訂單之后就會(huì)減庫存芜飘,這是一個(gè)原子操作务豺。

但是這樣也會(huì)產(chǎn)生一些問題:

在極限并發(fā)情況下,任何一個(gè)內(nèi)存操作的細(xì)節(jié)都至關(guān)影響性能燃箭,尤其像創(chuàng)建訂單這種邏輯冲呢,一般都需要存儲(chǔ)到磁盤數(shù)據(jù)庫的,對數(shù)據(jù)庫的壓力是可想而知的招狸。

如果用戶存在惡意下單的情況敬拓,只下單不支付這樣庫存就會(huì)變少,會(huì)少賣很多訂單裙戏,雖然服務(wù)端可以限制 IP 和用戶的購買訂單數(shù)量乘凸,這也不算是一個(gè)好方法。

2.2 支付減庫存

如果等待用戶支付了訂單在減庫存累榜,第一感覺就是不會(huì)少賣营勤。但是這是并發(fā)架構(gòu)的大忌灵嫌,因?yàn)樵跇O限并發(fā)情況下,用戶可能會(huì)創(chuàng)建很多訂單葛作。

當(dāng)庫存減為零的時(shí)候很多用戶發(fā)現(xiàn)搶到的訂單支付不了了寿羞,這也就是所謂的“超賣”。也不能避免并發(fā)操作數(shù)據(jù)庫磁盤 IO赂蠢。

2.3 預(yù)扣庫存

從上邊兩種方案的考慮绪穆,我們可以得出結(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ì)。

3. 扣庫存的藝術(shù)

從上面的分析可知烛缔,顯然預(yù)扣庫存的方案最合理馏段。我們進(jìn)一步分析扣庫??的細(xì)節(jié),這里還有很大的優(yōu)化空間践瓷,庫存存在哪里院喜?怎樣保證高并發(fā)下,正確的扣庫存晕翠,還能快速的響應(yīng)用戶請求喷舀?

在單機(jī)低并發(fā)情況下,我們實(shí)現(xiàn)扣庫存通常是這樣的:

為了保證扣庫存和生成訂單的原子性,需要采用事務(wù)處理硫麻,然后取庫存判斷爸邢、減庫存,最后提交事務(wù)拿愧,整個(gè)流程有很多 IO杠河,對數(shù)據(jù)庫的操作又是阻塞的。這種方式根本不適合高并發(fā)的秒殺系統(tǒng)赶掖。

接下來我們對單機(jī)扣庫存的方案做優(yōu)化:本地扣庫存感猛。我們把一定的庫存量分配到本地機(jī)器,直接在內(nèi)存中減庫存奢赂,然后按照之前的邏輯異步創(chuàng)建訂單陪白。改進(jìn)過之后的單機(jī)系統(tǒng)是這樣的:

這樣就避免了對數(shù)據(jù)庫頻繁的 IO 操作,只在內(nèi)存中做運(yùn)算膳灶,極大的提高了單機(jī)抗并發(fā)的能力咱士。

但是百萬的用戶請求量單機(jī)是無論如何也抗不住的,雖然 Nginx 處理網(wǎng)絡(luò)請求使用 Epoll 模型轧钓,c10k 的問題在業(yè)界早已得到了解決序厉。?但是 Linux 系統(tǒng)下,一切資源皆文件毕箍,網(wǎng)絡(luò)請求也是這樣弛房,大量的文件描述符會(huì)使操作系統(tǒng)瞬間失去響應(yīng)。

上面我們提到了 Nginx 的加權(quán)均衡策略而柑,我們不妨假設(shè)將 100W 的用戶請求量平均均衡到 100 臺(tái)服務(wù)器上文捶,這樣單機(jī)所承受的并發(fā)量就小了很多。然后我們每臺(tái)機(jī)器本地庫存 100 張火車票媒咳,100?臺(tái)服務(wù)器上的總庫存還是 1 萬粹排,這樣保證了庫存訂單不超賣,下面是我們描述的集群架構(gòu):

問題接踵而至涩澡,在高并發(fā)情況下顽耳,現(xiàn)在我們還無法保證系統(tǒng)的高可用,假如這 100?臺(tái)服務(wù)器上有兩三臺(tái)機(jī)器因?yàn)榭覆蛔〔l(fā)的流量或者其他的原因宕機(jī)了妙同。?那么這些服務(wù)器上的訂單就賣不出去了射富,這就造成了訂單的少賣。

要解決這個(gè)問題粥帚,我們需要對總訂單量做統(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)圖具體分析一下:

我們采用 Redis 存儲(chǔ)統(tǒng)一庫存懊亡,因?yàn)?Redis 的性能非常高,號稱單機(jī) QPS 能抗 10W 的并發(fā)乎串。?在本地減庫存以后店枣,如果本地有訂單,我們再去請求 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ì)對 Redis 造成一定的影響。?雖然 Redis 內(nèi)存數(shù)據(jù)庫抗并發(fā)能力非常高啃勉,請求依然會(huì)走一次網(wǎng)絡(luò) IO忽舟,其實(shí)搶票過程中對 Redis 的請求次數(shù)是本地庫存和 Buffer 庫存的總量。因?yàn)楫?dāng)本地庫存不足時(shí)淮阐,系統(tǒng)直接返回用戶“已售罄”的信息提示萧诫,就不會(huì)再走統(tǒng)一扣庫存的邏輯。這在一定程度上也避免了巨大的網(wǎng)絡(luò)請求量把 Redis 壓跨枝嘶,所以 Buffer 值設(shè)置多少,需要架構(gòu)師對系統(tǒng)的負(fù)載能力做認(rèn)真的考量哑诊。

4. 代碼演示

Go 語言原生為并發(fā)設(shè)計(jì)群扶,我采用 Go 語言給大家演示一下單機(jī)搶票的具體流程。

4.1 初始化工作

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):

...

//localSpike包結(jié)構(gòu)體定義

package?localSpike

type?LocalSpike?struct?{

????LocalInStock?????int64

????LocalSalesVolume?int64

}

...

//remoteSpike對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)中總訂單庫存key

????QuantityOfOrderKey?string???//hash結(jié)構(gòu)中已有訂單數(shù)量key

}

//初始化redis連接池

func?NewPool()?*redis.Pool?{

????return?&redis.Pool{

????????MaxIdle:???10000,

????????MaxActive:?12000,?//?max?number?of?connections

????????Dial:?func()?(redis.Conn,?error)?{

????????????c,?err?:=?redis.Dial("tcp",?":6379")

????????????if?err?!=?nil?{

????????????????panic(err.Error())

????????????}

????????????return?c,?err

????????},

????}

}

...

func?init()?{

????localSpike?=?localSpike2.LocalSpike{

????????LocalInStock:?????150,

????????LocalSalesVolume:?0,

????}

????remoteSpike?=?remoteSpike2.RemoteSpikeKeys{

????????SpikeOrderHashKey:??"ticket_hash_key",

????????TotalInventoryKey:??"ticket_total_nums",

????????QuantityOfOrderKey:?"ticket_sold_nums",

????}

????redisPool?=?remoteSpike2.NewPool()

????done?=?make(chan?int,?1)

????done?<-?1

}

4.2 本地扣庫存和統(tǒng)一扣庫存

本地扣庫存邏輯非常簡單,用戶請求過來仇奶,添加銷量貌嫡,然后對比銷量是否大于本地庫存,返回 Bool 值:

package?localSpike

//本地扣庫存,返回bool值

func?(spike?*LocalSpike)?LocalDeductionStock()?bool{

????spike.LocalSalesVolume?=?spike.LocalSalesVolume?+?1

????return?spike.LocalSalesVolume?<?spike.LocalInStock

}

注意這里對共享數(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 腳本打包命令,保證操作的原子性:

package?remoteSpike

......

const?LuaScript?=?`

????????local?ticket_key?=?KEYS[1]

????????local?ticket_total_key?=?ARGV[1]

????????local?ticket_sold_key?=?ARGV[2]

????????local?ticket_total_nums?=?tonumber(redis.call('HGET',?ticket_key,?ticket_total_key))

????????local?ticket_sold_nums?=?tonumber(redis.call('HGET',?ticket_key,?ticket_sold_key))

????????--?查看是否還有余票,增加訂單數(shù)量,返回結(jié)果值

???????if(ticket_total_nums?>=?ticket_sold_nums)?then

????????????return?redis.call('HINCRBY',?ticket_key,?ticket_sold_key,?1)

????????end

????????return?0

`

//遠(yuǎn)端統(tǒng)一扣庫存

func?(RemoteSpikeKeys?*RemoteSpikeKeys)?RemoteDeductionStock(conn?redis.Conn)?bool?{

????lua?:=?redis.NewScript(1,?LuaScript)

????result,?err?:=?redis.Int(lua.Do(conn,?RemoteSpikeKeys.SpikeOrderHashKey,?RemoteSpikeKeys.TotalInventoryKey,?RemoteSpikeKeys.QuantityOfOrderKey))

????if?err?!=?nil?{

????????return?false

????}

????return?result?!=?0

}

我們使用 Hash 結(jié)構(gòu)存儲(chǔ)總庫存和總銷量的信息点晴,用戶請求過來時(shí)感凤,判斷總銷量是否大于庫存,然后返回相關(guān)的 Bool 值粒督。

在啟動(dòng)服務(wù)之前陪竿,我們需要初始化 Redis 的初始庫存信息:

hmset?ticket_hash_key?"ticket_total_nums"?10000?"ticket_sold_nums"?0

4.3 響應(yīng)用戶信息

我們開啟一個(gè) HTTP 服務(wù),監(jiān)聽在一個(gè)端口上:

package?main

...

func?main()?{

????http.HandleFunc("/buy/ticket",?handleReq)

????http.ListenAndServe(":3005",?nil)

}

上面我們做完了所有的初始化工作屠橄,接下來 handleReq 的邏輯非常清晰族跛,判斷是否搶票成功,返回給用戶信息就可以了锐墙。

package?main

//處理請求函數(shù),根據(jù)請求將響應(yīng)結(jié)果信息寫入日志

func?handleReq(w?http.ResponseWriter,?r?*http.Request)?{

????redisConn?:=?redisPool.Get()

????LogMsg?:=?""

????<-done

????//全局讀寫鎖

????if?localSpike.LocalDeductionStock()?&&?remoteSpike.RemoteDeductionStock(redisConn)?{

????????util.RespJson(w,?1,??"搶票成功",?nil)

????????LogMsg?=?LogMsg?+?"result:1,localSales:"?+?strconv.FormatInt(localSpike.LocalSalesVolume,?10)

????}?else?{

????????util.RespJson(w,?-1,?"已售罄",?nil)

????????LogMsg?=?LogMsg?+?"result:0,localSales:"?+?strconv.FormatInt(localSpike.LocalSalesVolume,?10)

????}

????done?<-?1

????//將搶票狀態(tài)寫入到log中

????writeLog(LogMsg,?"./stat.log")

}

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"},?"")

????buf?:=?[]byte(content)

????fd.Write(buf)

}

前邊提到我們扣庫存時(shí)要考慮競態(tài)條件礁哄,我們這里是使用 Channel 避免并發(fā)的讀寫,保證了請求的高效順序執(zhí)行溪北。?我們將接口的返回信息寫入到了 ./stat.log 文件方便做壓測統(tǒng)計(jì)桐绒。

4.4 單機(jī)服務(wù)壓測

開啟服務(wù),我們使用 AB 壓測工具進(jìn)行測試:

ab?-n?10000?-c?100?http://127.0.0.1:3005/buy/ticket

下面是我本地低配 Mac 的壓測信息:

This?is?ApacheBench,?Version?2.3?<$Revision:?1826891?$>

Copyright?1996?Adam?Twiss,?Zeus?Technology?Ltd,?http://www.zeustech.net/

Licensed?to?The?Apache?Software?Foundation,?http://www.apache.org/

Benchmarking?127.0.0.1?(be?patient)

Completed?1000?requests

Completed?2000?requests

Completed?3000?requests

Completed?4000?requests

Completed?5000?requests

Completed?6000?requests

Completed?7000?requests

Completed?8000?requests

Completed?9000?requests

Completed?10000?requests

Finished?10000?requests

Server?Software:

Server?Hostname:????????127.0.0.1

Server?Port:????????????3005

Document?Path:??????????/buy/ticket

Document?Length:????????29?bytes

Concurrency?Level:??????100

Time?taken?for?tests:???2.339?seconds

Complete?requests:??????10000

Failed?requests:????????0

Total?transferred:??????1370000?bytes

HTML?transferred:???????290000?bytes

Requests?per?second:????4275.96?[#/sec]?(mean)

Time?per?request:???????23.387?[ms]?(mean)

Time?per?request:???????0.234?[ms]?(mean,?across?all?concurrent?requests)

Transfer?rate:??????????572.08?[Kbytes/sec]?received

Connection?Times?(ms)

??????????????min??mean[+/-sd]?median???max

Connect:????????0????8??14.7??????6?????223

Processing:?????2???15??17.6?????11?????232

Waiting:????????1???11??13.5??????8?????225

Total:??????????7???23??22.8?????18?????239

Percentage?of?the?requests?served?within?a?certain?time?(ms)

??50%?????18

??66%?????24

??75%?????26

??80%?????28

??90%?????33

??95%?????39

??98%?????45

??99%?????54

?100%????239?(longest?request)

根據(jù)指標(biāo)顯示之拨,我單機(jī)每秒就能處理 4000+ 的請求茉继,正常服務(wù)器都是多核配置,處理 1W+ 的請求根本沒有問題蚀乔。

而且查看日志發(fā)現(xiàn)整個(gè)服務(wù)過程中烁竭,請求都很正常,流量均勻吉挣,Redis 也很正常:

//stat.log

...

result:1,localSales:145

result:1,localSales:146

result:1,localSales:147

result:1,localSales:148

result:1,localSales:149

result:1,localSales:150

result:0,localSales:151

result:0,localSales:152

result:0,localSales:153

result:0,localSales:154

result:0,localSales:156

...

總結(jié)回顧

總體來說派撕,秒殺系統(tǒng)是非常復(fù)雜的婉弹。我們這里只是簡單介紹模擬了一下單機(jī)如何優(yōu)化到高性能,集群如何避免單點(diǎn)故障腥刹,保證訂單不超賣马胧、不少賣的一些策略,完整的訂單系統(tǒng)還有訂單進(jìn)度的查看衔峰,每臺(tái)服務(wù)器上都有一個(gè)任務(wù)佩脊,定時(shí)的從總庫存同步余票和庫存信息展示給用戶,還有用戶在訂單有效期內(nèi)不支付垫卤,釋放訂單威彰,補(bǔ)充到庫存等等。

我們實(shí)現(xiàn)了高并發(fā)搶票的核心邏輯穴肘,可以說系統(tǒng)設(shè)計(jì)的非常的巧妙歇盼,巧妙的避開了對 DB 數(shù)據(jù)庫 IO 的操作。?對 Redis 網(wǎng)絡(luò) IO 的高并發(fā)請求评抚,幾乎所有的計(jì)算都是在內(nèi)存中完成的豹缀,而且有效的保證了不超賣、不少賣慨代,還能夠容忍部分機(jī)器的宕機(jī)邢笙。

我覺得其中有兩點(diǎn)特別值得學(xué)習(xí)總結(jié):

①負(fù)載均衡,分而治之

通過負(fù)載均衡侍匙,將不同的流量劃分到不同的機(jī)器上氮惯,每臺(tái)機(jī)器處理好自己的請求,將自己的性能發(fā)揮到極致想暗。?這樣系統(tǒng)的整體也就能承受極高的并發(fā)了妇汗,就像工作的一個(gè)團(tuán)隊(duì),每個(gè)人都將自己的價(jià)值發(fā)揮到了極致说莫,團(tuán)隊(duì)成長自然是很大的杨箭。

②合理的使用并發(fā)和異步

自 Epoll 網(wǎng)絡(luò)架構(gòu)模型解決了 c10k 問題以來,異步越來越被服務(wù)端開發(fā)人員所接受储狭,能夠用異步來做的工作互婿,就用異步來做,在功能拆解上能達(dá)到意想不到的效果晶密。?這點(diǎn)在 Nginx、Node.JS模她、Redis 上都能體現(xiàn)稻艰,他們處理網(wǎng)絡(luò)請求使用的 Epoll 模型,用實(shí)踐告訴了我們單線程依然可以發(fā)揮強(qiáng)大的威力侈净。服務(wù)器已經(jīng)進(jìn)入了多核時(shí)代尊勿,Go 語言這種天生為并發(fā)而生的語言僧凤,完美的發(fā)揮了服務(wù)器多核優(yōu)勢,很多可以并發(fā)處理的任務(wù)都可以使用并發(fā)來解決元扔,比如 Go 處理 HTTP 請求時(shí)每個(gè)請求都會(huì)在一個(gè) Goroutine 中執(zhí)行躯保。總之澎语,怎樣合理的壓榨 CPU途事,讓其發(fā)揮出應(yīng)有的價(jià)值,是我們一直需要探索學(xué)習(xí)的方向擅羞。

--------? END? ---------

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尸变,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子减俏,更是在濱河造成了極大的恐慌召烂,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件娃承,死亡現(xiàn)場離奇詭異奏夫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)历筝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門酗昼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人漫谷,你說我怎么就攤上這事仔雷。” “怎么了舔示?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵碟婆,是天一觀的道長。 經(jīng)常有香客問我惕稻,道長竖共,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任俺祠,我火速辦了婚禮公给,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蜘渣。我一直安慰自己淌铐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布蔫缸。 她就那樣靜靜地躺著腿准,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拾碌。 梳的紋絲不亂的頭發(fā)上吐葱,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天街望,我揣著相機(jī)與錄音,去河邊找鬼弟跑。 笑死灾前,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的孟辑。 我是一名探鬼主播哎甲,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼扑浸!你這毒婦竟也來了烧给?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤喝噪,失蹤者是張志新(化名)和其女友劉穎础嫡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酝惧,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡榴鼎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晚唇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巫财。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哩陕,靈堂內(nèi)的尸體忽然破棺而出平项,到底是詐尸還是另有隱情,我是刑警寧澤悍及,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布闽瓢,位于F島的核電站,受9級特大地震影響心赶,放射性物質(zhì)發(fā)生泄漏扣讼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一缨叫、第九天 我趴在偏房一處隱蔽的房頂上張望椭符。 院中可真熱鬧,春花似錦耻姥、人聲如沸销钝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蒸健。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纵装,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工据某, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留橡娄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓癣籽,卻偏偏與公主長得像挽唉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子筷狼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容