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