我和同事們對(duì)小米網(wǎng)的搶購(gòu)系統(tǒng)做了最后的檢查與演練。幾個(gè)小時(shí)后,小米網(wǎng)今年開年來(lái)最重要的一次大型活動(dòng)“米粉節(jié)”就要開始了。
這次米粉節(jié)活動(dòng),是小米電商的成人禮,是一次重要的考試。小米網(wǎng)從網(wǎng)站前端、后臺(tái)系統(tǒng)、倉(cāng)儲(chǔ)物流、售后等各個(gè)環(huán)節(jié),都將接受一次全面的壓力測(cè)試。
10點(diǎn)整,一波流量高峰即將到來(lái),幾百萬(wàn)用戶將準(zhǔn)點(diǎn)擠入小米網(wǎng)的服務(wù)器。而首先迎接壓力沖擊的,就是擋在最前面的搶購(gòu)系統(tǒng)。
而這個(gè)搶購(gòu)系統(tǒng)是重新開發(fā)、剛剛上線不久的,這是它第一次接受這樣嚴(yán)峻的考驗(yàn)。
系統(tǒng)能不能頂住壓力?能不能順暢正確地執(zhí)行業(yè)務(wù)邏輯?這些問(wèn)題不到搶購(gòu)高峰那一刻,誰(shuí)都不能百分百確定。
9點(diǎn)50分,流量已經(jīng)爬升得很高了;10點(diǎn)整,搶購(gòu)系統(tǒng)自動(dòng)開啟,購(gòu)物車中已經(jīng)順利加入了搶購(gòu)商品。
一兩分鐘后,熱門的搶購(gòu)商品已經(jīng)售罄自動(dòng)停止搶購(gòu)。搶購(gòu)系統(tǒng)抗住了壓力。
我長(zhǎng)舒一口氣,之前積累的壓力都消散了。我坐到角落的沙發(fā)里,默默回想搶購(gòu)系統(tǒng)所經(jīng)歷的那些驚心動(dòng)魄的故事。這可真是一場(chǎng)很少人有機(jī)會(huì)經(jīng)歷的探險(xiǎn)呢。
搶購(gòu)系統(tǒng)是怎樣誕生的
時(shí)間回到2011年底。小米公司在這一年8月16日首次發(fā)布了手機(jī),立刻引起了市場(chǎng)轟動(dòng)。隨后,在一天多的時(shí)間內(nèi)預(yù)約了30萬(wàn)臺(tái)。之后的幾個(gè)月,這30萬(wàn)臺(tái)小米手機(jī)通過(guò)排號(hào)的方式依次發(fā)貨,到當(dāng)年年底全部發(fā)完。
然后便是開放購(gòu)買。最初的開放購(gòu)買直接在小米的商城系統(tǒng)上進(jìn)行,但我們那時(shí)候完全低估了“搶購(gòu)”的威力。瞬間爆發(fā)的平常幾十倍流量迅速淹沒了小米網(wǎng)商城服務(wù)器,數(shù)據(jù)庫(kù)死鎖、網(wǎng)頁(yè)刷新超時(shí),用戶購(gòu)買體驗(yàn)非常差。
市場(chǎng)需求不等人,一周后又要進(jìn)行下一輪開放搶購(gòu)。一場(chǎng)風(fēng)暴就等在前方,而我們只有一周的時(shí)間了,整個(gè)開發(fā)部都承擔(dān)著巨大的壓力。
小米網(wǎng)可以采用的常規(guī)優(yōu)化手段并不太多,增加帶寬、服務(wù)器、尋找代碼中的瓶頸點(diǎn)優(yōu)化代碼。但是,小米公司只是一家剛剛成立一年多的小公司,沒有那么多的服務(wù)器和帶寬。而且,如果代碼中有瓶頸點(diǎn),即使能增加一兩倍的服務(wù)器和帶寬,也一樣會(huì)被瞬間爆發(fā)的幾十倍負(fù)載所沖垮。而要優(yōu)化商城的代碼,時(shí)間上已沒有可能。電商網(wǎng)站很復(fù)雜,說(shuō)不定某個(gè)不起眼的次要功能,在高負(fù)載情況下就會(huì)成為瓶頸點(diǎn)拖垮整個(gè)網(wǎng)站。
這時(shí)開發(fā)組面臨一個(gè)選擇,是繼續(xù)在現(xiàn)有商城上優(yōu)化,還是單獨(dú)搞一套搶購(gòu)系統(tǒng)?我們決定冒險(xiǎn)一試,我和幾個(gè)同事一起突擊開發(fā)一套獨(dú)立的搶購(gòu)系統(tǒng),希望能夠絕境逢生。
擺在我們面前的是一道似乎無(wú)解的難題,它要達(dá)到的目標(biāo)如下:
設(shè)計(jì)方案就是多個(gè)限制條件下求得的解。時(shí)間、可靠性、成本,這是我們面臨的限制條件。要在那么短的時(shí)間內(nèi)解決難題,必須選擇最簡(jiǎn)單可靠的技術(shù),必須是經(jīng)過(guò)足夠驗(yàn)證的技術(shù),解決方案必須是最簡(jiǎn)單的。
在高并發(fā)情況下,影響系統(tǒng)性能的一個(gè)關(guān)鍵因素是:數(shù)據(jù)的一致性要求。在前面所列的目標(biāo)中,有兩項(xiàng)是關(guān)于數(shù)據(jù)一致性的:商品剩余數(shù)量、用戶是否已經(jīng)搶購(gòu)成功。如果要保證嚴(yán)格的數(shù)據(jù)一致性,那么在集群中需要一個(gè)中心服務(wù)器來(lái)存儲(chǔ)和操作這個(gè)值。這會(huì)造成性能的單點(diǎn)瓶頸。
在分布式系統(tǒng)設(shè)計(jì)中,有一個(gè)CAP原理?!耙恢滦?、可用性、分區(qū)容忍性”三個(gè)要素最多只能同時(shí)實(shí)現(xiàn)兩點(diǎn),不可能三者兼顧。我們要面對(duì)極端的爆發(fā)流量負(fù)載,分區(qū)容忍性和可用性會(huì)非常重要,因此決定犧牲數(shù)據(jù)的強(qiáng)一致性要求。
做出這個(gè)重要的決定后,剩下的設(shè)計(jì)決定就自然而然地產(chǎn)生了:
最后的系統(tǒng)原理見后面的第一版搶購(gòu)系統(tǒng)原理圖(圖1)。
圖1 第一版搶購(gòu)系統(tǒng)原理圖
系統(tǒng)基本原理:在PHP服務(wù)器上,通過(guò)一個(gè)文件來(lái)表示商品是否售罄。如果文件存在即表示已經(jīng)售罄。PHP程序接收用戶搶購(gòu)請(qǐng)求后,查看用戶是否預(yù)約以及是否搶購(gòu)過(guò),然后檢查售罄標(biāo)志文件是否存在。對(duì)預(yù)約用戶,如果未售罄并且用戶未搶購(gòu)成功過(guò),即返回?fù)屬?gòu)成功的結(jié)果,并記錄一條日志。日志通過(guò)異步的方式傳輸?shù)街行目刂乒?jié)點(diǎn),完成記數(shù)等操作。
最后,搶購(gòu)成功用戶的列表異步導(dǎo)入商場(chǎng)系統(tǒng),搶購(gòu)成功的用戶在接下來(lái)的幾個(gè)小時(shí)內(nèi)下單即可。這樣,流量高峰完全被搶購(gòu)系統(tǒng)擋住,商城系統(tǒng)不需要面對(duì)高流量。
在這個(gè)分布式系統(tǒng)的設(shè)計(jì)中,對(duì)持久化數(shù)據(jù)的處理是影響性能的重要因素。我們沒有選擇傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù),而是選用了Redis服務(wù)器。選用Redis基于下面幾個(gè)理由。
在整個(gè)系統(tǒng)中,最頻繁的I/O操作,就是PHP對(duì)Redis的讀寫操作。如果處理不好,Redis服務(wù)器將成為系統(tǒng)的性能瓶頸。
系統(tǒng)中對(duì)Redis的操作包含三種類型的操作:查詢是否有預(yù)約、是否搶購(gòu)成功、寫入搶購(gòu)成功狀態(tài)。為了提升整體的處理能力,可采用讀寫分離方式。
所有的讀操作通過(guò)從庫(kù)完成,所有的寫操作只通過(guò)控制端一個(gè)進(jìn)程寫入主庫(kù)。
在PHP對(duì)Redis服務(wù)器的讀操作中,需要注意的是連接數(shù)的影響。如果PHP是通過(guò)短連接訪問(wèn)Redis服務(wù)器的,則在高峰時(shí)有可能堵塞Redis服務(wù)器,造成雪崩效應(yīng)。這一問(wèn)題可以通過(guò)增加Redis從庫(kù)的數(shù)量來(lái)解決。
而對(duì)于Redis的寫操作,在我們的系統(tǒng)中并沒有壓力。因?yàn)橄到y(tǒng)是通過(guò)異步方式,收集PHP產(chǎn)生的日志,由一個(gè)管理端的進(jìn)程來(lái)順序?qū)懭隦edis主庫(kù)。
另一個(gè)需要注意的點(diǎn)是Redis的持久化配置。用戶的預(yù)約信息全部存儲(chǔ)在Redis的進(jìn)程內(nèi)存中,它向磁盤保存一次,就會(huì)造成一次等待。嚴(yán)重的話會(huì)導(dǎo)致?lián)屬?gòu)高峰時(shí)系統(tǒng)前端無(wú)法響應(yīng)。因此要盡量避免持久化操作。我們的做法是,所有用于讀取的從庫(kù)完全關(guān)閉持久化,一個(gè)用于備份的從庫(kù)打開持久化配置。同時(shí)使用日志作為應(yīng)急恢復(fù)的保險(xiǎn)措施。
整個(gè)系統(tǒng)使用了大約30臺(tái)服務(wù)器,其中包括20臺(tái)PHP服務(wù)器,以及10臺(tái)Redis服務(wù)器。在接下來(lái)的搶購(gòu)中,它順利地抗住了壓力?;叵肫甬?dāng)時(shí)的場(chǎng)景,真是非常的驚心動(dòng)魄。
第二版搶購(gòu)系統(tǒng)
經(jīng)過(guò)了兩年多的發(fā)展,小米網(wǎng)已經(jīng)越來(lái)越成熟。公司準(zhǔn)備在2014年4月舉辦一次盛大的“米粉節(jié)”活動(dòng)。這次持續(xù)一整天的購(gòu)物狂歡節(jié)是小米網(wǎng)電商的一次成人禮。商城前端、庫(kù)存、物流、售后等環(huán)節(jié)都將經(jīng)歷一次考驗(yàn)。
對(duì)于搶購(gòu)系統(tǒng)來(lái)說(shuō),最大的不同就是一天要經(jīng)歷多輪搶購(gòu)沖擊,而且有多種不同商品參與搶購(gòu)。我們之前的搶購(gòu)系統(tǒng),是按照一周一次搶購(gòu)來(lái)設(shè)計(jì)及優(yōu)化的,根本無(wú)法支撐米粉節(jié)復(fù)雜的活動(dòng)。而且經(jīng)過(guò)一年多的修修補(bǔ)補(bǔ),第一版搶購(gòu)系統(tǒng)積累了很多的問(wèn)題,正好趁此機(jī)會(huì)對(duì)它進(jìn)行徹底重構(gòu)。
第二版系統(tǒng)主要關(guān)注系統(tǒng)的靈活性與可運(yùn)營(yíng)性(圖2)。對(duì)于高并發(fā)的負(fù)載能力,穩(wěn)定性、準(zhǔn)確性這些要求,已經(jīng)是基礎(chǔ)性的最低要求了。我希望將這個(gè)系統(tǒng)做得可靈活配置,支持各種商品各種條件組合,并且為將來(lái)的擴(kuò)展打下良好的基礎(chǔ)。
圖2 第二版系統(tǒng)總體結(jié)構(gòu)圖
在這一版中,搶購(gòu)系統(tǒng)與商城系統(tǒng)依然隔離,兩個(gè)系統(tǒng)之間通過(guò)約定的數(shù)據(jù)結(jié)構(gòu)交互,信息傳遞精簡(jiǎn)。通過(guò)搶購(gòu)系統(tǒng)確定一個(gè)用戶搶得購(gòu)買資格后,用戶自動(dòng)在商城系統(tǒng)中將商品加入購(gòu)物車。
在之前第一版搶購(gòu)系統(tǒng)中,我們后來(lái)使用Go語(yǔ)言開發(fā)了部分模塊,積累了一定的經(jīng)驗(yàn)。因此第二版系統(tǒng)的核心部分,我們決定使用Go語(yǔ)言進(jìn)行開發(fā)。
我們可以讓Go程序常駐內(nèi)存運(yùn)行,各種配置以及狀態(tài)信息都可以保存在內(nèi)存中,減少I/O操作開銷。對(duì)于商品數(shù)量信息,可以在進(jìn)程內(nèi)進(jìn)行操作。不同商品可以分別保存到不同的服務(wù)器的Go進(jìn)程中,以此來(lái)分散壓力,提升處理速度。
系統(tǒng)服務(wù)端主要分為兩層架構(gòu),即HTTP服務(wù)層和業(yè)務(wù)處理層。HTTP服務(wù)層用于維持用戶的訪問(wèn)請(qǐng)求,業(yè)務(wù)處理層則用于進(jìn)行具體的邏輯判斷。兩層之間的數(shù)據(jù)交互通過(guò)消息隊(duì)列來(lái)實(shí)現(xiàn)。
HTTP服務(wù)層主要功能如下:
業(yè)務(wù)處理層主要功能如下:
用戶的搶購(gòu)請(qǐng)求通過(guò)消息隊(duì)列,依次進(jìn)入業(yè)務(wù)處理層的Go進(jìn)程里,然后順序地處理請(qǐng)求,將搶購(gòu)結(jié)果返回給前面的HTTP服務(wù)層。
商品剩余數(shù)量等信息,根據(jù)商品編號(hào)分別保存在業(yè)務(wù)層特定的服務(wù)器進(jìn)程中。我們選擇保證商品數(shù)據(jù)的一致性,放棄了數(shù)據(jù)的分區(qū)容忍性。
這兩個(gè)模塊用于搶購(gòu)過(guò)程中的請(qǐng)求處理,系統(tǒng)中還有相應(yīng)的策略控制模塊,以及防刷和系統(tǒng)管理模塊等(圖3)。
圖3 第二版系統(tǒng)詳細(xì)結(jié)構(gòu)圖
在第二版搶購(gòu)系統(tǒng)的開發(fā)過(guò)程中,我們遇到了HTTP層Go程序內(nèi)存消耗過(guò)多的問(wèn)題。
由于HTTP層主要用于維持住用戶的訪問(wèn)請(qǐng)求,每個(gè)請(qǐng)求中的數(shù)據(jù)都會(huì)占用一定的內(nèi)存空間,當(dāng)大量的用戶進(jìn)行訪問(wèn)時(shí)就會(huì)導(dǎo)致內(nèi)存使用量不斷上漲。當(dāng)內(nèi)存占用量達(dá)到一定程度(50%)時(shí),Go中的GC機(jī)制會(huì)越來(lái)越慢,但仍然會(huì)有大量的用戶進(jìn)行訪問(wèn),導(dǎo)致出現(xiàn)“雪崩”效應(yīng),內(nèi)存不斷上漲,最終機(jī)器內(nèi)存的使用率會(huì)達(dá)到90%以上甚至99%,導(dǎo)致服務(wù)不可用。
在Go語(yǔ)言原生的HTTP包中會(huì)為每個(gè)請(qǐng)求分配8KB的內(nèi)存,用于讀緩存和寫緩存。而在我們的服務(wù)場(chǎng)景中只有GET請(qǐng)求,服務(wù)需要的信息都包含在HTTP Header中,并沒有Body,實(shí)際上不需要如此大的內(nèi)存進(jìn)行存儲(chǔ)。
為了避免讀寫緩存的頻繁申請(qǐng)和銷毀,HTTP包建立了一個(gè)緩存池,但其長(zhǎng)度只有4,因此在大量連接創(chuàng)建時(shí),會(huì)大量申請(qǐng)內(nèi)存,創(chuàng)建新對(duì)象。而當(dāng)大量連接釋放時(shí),又會(huì)導(dǎo)致很多對(duì)象內(nèi)存無(wú)法回收到緩存池,增加了GC的壓力。
HTTP協(xié)議是構(gòu)建在TCP協(xié)議之上的,Go的原生HTTP模塊中是沒有提供直接的接口關(guān)閉底層TCP連接的,而HTTP 1.1中對(duì)連接狀態(tài)默認(rèn)使用keep-alive方式。這樣,在客戶端多次請(qǐng)求服務(wù)端時(shí),可以復(fù)用一個(gè)TCP連接,避免頻繁建立和斷開連接,導(dǎo)致服務(wù)端一直等待讀取下一個(gè)請(qǐng)求而不釋放連接。但同樣在我們的服務(wù)場(chǎng)景中不存在TCP連接復(fù)用的需求。當(dāng)一個(gè)用戶完成一個(gè)請(qǐng)求后,希望能夠盡快關(guān)閉連接。keep-alive方式導(dǎo)致已完成處理的用戶連接不能盡快關(guān)閉,連接無(wú)法釋放,導(dǎo)致連接數(shù)不斷增加,對(duì)服務(wù)端的內(nèi)存和帶寬都有影響。
通過(guò)上面的分析,我們的解決辦法如下。
通過(guò)這樣的改進(jìn),我們的HTTP前端服務(wù)器最大穩(wěn)定連接數(shù)可以超過(guò)一百萬(wàn)。
第二版搶購(gòu)系統(tǒng)順利完成了米粉節(jié)的考驗(yàn)。
總結(jié)
技術(shù)方案需要依托具體的問(wèn)題而存在。脫離了應(yīng)用場(chǎng)景,無(wú)論多么酷炫的技術(shù)都失去了價(jià)值。搶購(gòu)系統(tǒng)面臨的現(xiàn)實(shí)問(wèn)題復(fù)雜多變,我們也依然在不斷地摸索改進(jìn)。
意見反饋
×
Copyright © 1998-2019 甘肅信息港 All rights reserved.