小米網搶購系統開發實踐和我的個人觀察
本文個人觀察部分,為自己的一點看法。
正文內容,轉載於
《程式設計師》2014年11月刊:電商峰值系統架構設計
http://www.csdn.net/article/2014-11-04/2822459
個人觀察
搶購系統是怎樣誕生的
時間回到2011年底。小米公司在這一年8月16日首次釋出了手機,立刻引起了市場轟動。隨後,在一天多的時間內預約了30萬臺。之後的幾個月,這30萬臺小米手機通過排號的方式依次發貨,到當年年底全部發完。
然後便是開放購買。最初的開放購買直接在小米的商城系統上進行,但我們那時候完全低估了“搶購”的威力。瞬間爆發的平常幾十倍流量迅速淹沒了小米網商城伺服器,資料庫死鎖、網頁重新整理超時,使用者購買體驗非常差。
市場需求不等人,一週後又要進行下一輪開放搶購。一場風暴就等在前方,而我們只有一週的時間了,整個開發部都承擔著巨大的壓力。
小米網可以採用的常規優化手段並不太多,增加頻寬、伺服器、尋找程式碼中的瓶頸點優化程式碼。但是,小米公司只是一家剛剛成立一年多的小公司,沒有那麼多的伺服器和頻寬。而且,如果程式碼中有瓶頸點,即使能增加一兩倍的伺服器和頻寬,也一樣會被瞬間爆發的幾十倍負載所沖垮。而要優化商城的程式碼,時間上已沒有可能。電商網站很複雜,說不定某個不起眼的次要功能,在高負載情況下就會成為瓶頸點拖垮整個網站。
這時開發組面臨一個選擇,是繼續在現有商城上優化,還是單獨搞一套搶購系統?我們決定冒險一試,我和幾個同事一起突擊開發一套獨立的搶購系統,希望能夠絕境逢生。
擺在我們面前的是一道似乎無解的難題,它要達到的目標如下:
- 只有一週時間,一週內完成設計、開發、測試、上線;
- 失敗的代價無法承受,系統必須順暢執行;
- 搶購結果必須可靠;
- 面對海量使用者的併發搶購,商品不能賣超;
- 一個使用者只能搶一臺手機;
- 使用者體驗儘量好些。
設計方案就是多個限制條件下求得的解。時間、可靠性、成本,這是我們面臨的限制條件。要在那麼短的時間內解決難題,必須選擇最簡單可靠的技術,必須是經過足夠驗證的技術,解決方案必須是最簡單的。
在高併發情況下,影響系統效能的一個關鍵因素是:資料的一致性要求。在前面所列的目標中,有兩項是關於資料一致性的:商品剩餘數量、使用者是否已經搶購成功。如果要保證嚴格的資料一致性,那麼在叢集中需要一箇中心伺服器來儲存和操作這個值。這會造成效能的單點瓶頸。
在分散式系統設計中,有一個CAP原理。“一致性、可用性、分割槽容忍性”三個要素最多隻能同時實現兩點,不可能三者兼顧。我們要面對極端的爆發流量負載,分割槽容忍性和可用性會非常重要,因此決定犧牲資料的強一致性要求。
做出這個重要的決定後,剩下的設計決定就自然而然地產生了:
- 技術上要選擇最可靠的,因為團隊用PHP的居多,所以系統使用PHP開發;
- 搶資格過程要最簡化,使用者只需點一個搶購按鈕,返回結果表示搶購成功或者已經售罄;
- 對搶購請求的處理儘量簡化,將I/O操作控制到最少,減少每個請求的時間;
- 儘量去除效能單點,將壓力分散,整體效能可以線性擴充套件;
- 放棄資料強一致性要求,通過非同步的方式處理資料。
最後的系統原理見後面的第一版搶購系統原理圖(圖1)。
圖1 第一版搶購系統原理圖
系統基本原理:
在PHP伺服器上,通過一個檔案來表示商品是否售罄。如果檔案存在即表示已經售罄。PHP程式接收使用者搶購請求後,檢視使用者是否預約以及是否搶購過,然後檢查售罄標誌檔案是否存在。對預約使用者,如果未售罄並且使用者未搶購成功過,即返回搶購成功的結果,並記錄一條日誌。日誌通過非同步的方式傳輸到中心控制節點,完成記數等操作。
最後,搶購成功使用者的列表非同步匯入商場系統,搶購成功的使用者在接下來的幾個小時內下單即可。這樣,流量高峰完全被搶購系統擋住,商城系統不需要面對高流量。
在這個分散式系統的設計中,對持久化資料的處理是影響效能的重要因素。
我們沒有選擇傳統關係型資料庫,而是選用了Redis伺服器。
選用Redis基於下面幾個理由。
- 首先需要儲存的資料是典型的Key/Value對形式,每個UID對應一個字串資料。傳統資料庫的複雜功能用不上,用KV庫正合適。
- Redis的資料是in-memory的,可以極大提高查詢效率。
- Redis具有足夠用的主從複製機制,以及靈活設定的持久化操作配置。這兩點正好是我們需要的。
在整個系統中,最頻繁的I/O操作,就是PHP對Redis的讀寫操作。如果處理不好,Redis伺服器將成為系統的效能瓶頸。
系統中對Redis的操作包含三種型別的操作:查詢是否有預約、是否搶購成功、寫入搶購成功狀態。為了提升整體的處理能力,可採用讀寫分離方式。
所有的讀操作通過從庫完成,所有的寫操作只通過控制端一個程式寫入主庫。
在PHP對Redis伺服器的讀操作中,需要注意的是連線數的影響。如果PHP是通過短連線訪問Redis伺服器的,則在高峰時有可能堵塞Redis伺服器,造成雪崩效應。這一問題可以通過增加Redis從庫的數量來解決。
而對於Redis的寫操作,在我們的系統中並沒有壓力。因為系統是通過非同步方式,收集PHP產生的日誌,由一個管理端的程式來順序寫入Redis主庫。
另一個需要注意的點是Redis的持久化配置。使用者的預約資訊全部儲存在Redis的程式記憶體中,它向磁碟儲存一次,就會造成一次等待。嚴重的話會導致搶購高峰時系統前端無法響應。因此要儘量避免持久化操作。我們的做法是,所有用於讀取的從庫完全關閉持久化,一個用於備份的從庫開啟持久化配置。同時使用日誌作為應急恢復的保險措施。
整個系統使用了大約30臺伺服器,其中包括20臺PHP伺服器,以及10臺Redis伺服器。
在接下來的搶購中,它順利地抗住了壓力。回想起當時的場景,真是非常的驚心動魄。
第二版搶購系統
經過了兩年多的發展,小米網已經越來越成熟。公司準備在2014年4月舉辦一次盛大的“米粉節”活動。這次持續一整天的購物狂歡節是小米網電商的一次成人禮。商城前端、庫存、物流、售後等環節都將經歷一次考驗。
對於搶購系統來說,最大的不同就是一天要經歷多輪搶購衝擊,而且有多種不同商品參與搶購。我們之前的搶購系統,是按照一週一次搶購來設計及優化的,根本無法支撐米粉節複雜的活動。而且經過一年多的修修補補,第一版搶購系統積累了很多的問題,正好趁此機會對它進行徹底重構。
第二版系統主要關注系統的靈活性與可運營性(圖2)。對於高併發的負載能力,穩定性、準確性這些要求,已經是基礎性的最低要求了。我希望將這個系統做得可靈活配置,支援各種商品各種條件組合,並且為將來的擴充套件打下良好的基礎。
圖2 第二版系統總體結構圖
在這一版中,搶購系統與商城系統依然隔離,兩個系統之間通過約定的資料結構互動,資訊傳遞精簡。通過搶購系統確定一個使用者搶得購買資格後,使用者自動在商城系統中將商品加入購物車。
在之前第一版搶購系統中,我們後來使用Go語言開發了部分模組,積累了一定的經驗。因此第二版系統的核心部分,我們決定使用Go語言進行開發。
我們可以讓Go程式常駐記憶體執行,各種配置以及狀態資訊都可以儲存在記憶體中,減少I/O操作開銷。對於商品數量資訊,可以在程式內進行操作。不同商品可以分別儲存到不同的伺服器的Go程式中,以此來分散壓力,提升處理速度。
系統服務端主要分為兩層架構,即HTTP服務層和業務處理層。HTTP服務層用於維持使用者的訪問請求,業務處理層則用於進行具體的邏輯判斷。兩層之間的資料互動通過訊息佇列來實現。
HTTP服務層主要功能如下:
- 進行基本的URL正確性校驗;
- 對惡意訪問的使用者進行過濾,攔截黃牛;
- 提供使用者驗證碼;
- 將正常訪問使用者資料放入相應商品佇列中;
- 等待業務處理層返回的處理結果。
業務處理層主要功能如下:
- 接收商品佇列中的資料;
- 對使用者請求進行處理;
- 將請求結果放入相應的返回佇列中。
使用者的搶購請求通過訊息佇列,依次進入業務處理層的Go程式裡,然後順序地處理請求,將搶購結果返回給前面的HTTP服務層。
商品剩餘數量等資訊,根據商品編號分別儲存在業務層特定的伺服器程式中。我們選擇保證商品資料的一致性,放棄了資料的分割槽容忍性。
這兩個模組用於搶購過程中的請求處理,系統中還有相應的策略控制模組,以及防刷和系統管理模組等(圖3)。
圖3 第二版系統詳細結構圖
在第二版搶購系統的開發過程中,我們遇到了HTTP層Go程式記憶體消耗過多的問題。
由於HTTP層主要用於維持住使用者的訪問請求,每個請求中的資料都會佔用一定的記憶體空間,當大量的使用者進行訪問時就會導致記憶體使用量不斷上漲。當記憶體佔用量達到一定程度(50%)時,Go中的GC機制會越來越慢,但仍然會有大量的使用者進行訪問,導致出現“雪崩”效應,記憶體不斷上漲,最終機器記憶體的使用率會達到90%以上甚至99%,導致服務不可用。
在Go語言原生的HTTP包中會為每個請求分配8KB的記憶體,用於讀快取和寫快取。而在我們的服務場景中只有GET請求,服務需要的資訊都包含在HTTP Header中,並沒有Body,實際上不需要如此大的記憶體進行儲存。
為了避免讀寫快取的頻繁申請和銷燬,HTTP包建立了一個快取池,但其長度只有4,因此在大量連線建立時,會大量申請記憶體,建立新物件。而當大量連線釋放時,又會導致很多物件記憶體無法回收到快取池,增加了GC的壓力。
HTTP協議是構建在TCP協議之上的,Go的原生HTTP模組中是沒有提供直接的介面關閉底層TCP連線的,而HTTP 1.1中對連線狀態預設使用keep-alive方式。這樣,在客戶端多次請求服務端時,可以複用一個TCP連線,避免頻繁建立和斷開連線,導致服務端一直等待讀取下一個請求而不釋放連線。但同樣在我們的服務場景中不存在TCP連線複用的需求。當一個使用者完成一個請求後,希望能夠儘快關閉連線。keep-alive方式導致已完成處理的使用者連線不能儘快關閉,連線無法釋放,導致連線數不斷增加,對服務端的記憶體和頻寬都有影響。
通過上面的分析,我們的解決辦法如下。
- 在無法優化Go語言中GC機制時,要避免“雪崩效應”就要儘量避免服務佔用的記憶體超過限制(50%),在處於這個限制內時,GC可以有效進行。可通過增加伺服器的方式來分散記憶體壓力,並盡力優化服務佔用的記憶體大小。同時Go 1.3也對其GC做了一定優化。
- 我們為搶購這個特定服務場景定製了新的HTTP包,將TCP連線讀快取大小改為1KB。
- 在定製的HTTP包中,將快取池的大小改為100萬,避免讀寫快取的頻繁申請和銷燬。
- 當每個請求處理完成後,通過設定Response的Header中Connection為close來主動關閉連線。
通過這樣的改進,我們的HTTP前端伺服器最大穩定連線數可以超過一百萬。
第二版搶購系統順利完成了米粉節的考驗。
總結
技術方案需要依託具體的問題而存在。脫離了應用場景,無論多麼酷炫的技術都失去了價值。搶購系統面臨的現實問題複雜多變,我們也依然在不斷地摸索改進。
作者韓祝鵬,小米公司程式設計師。早期負責MIUI系統釋出與運營,後帶領小米網系統組設計與開發小米網搶購系統。
相關文章
- 京東搶購服務高併發實踐
- 電商網站秒殺與搶購的系統架構網站架構
- 關於PHP高併發搶購系統設計PHP
- 網校系統原始碼:網校系統開發和成品購買的區別原始碼
- 個人開發的內容管理系統
- Web系統大規模併發——電商秒殺與搶購Web
- JavaScript設計模式與開發實踐 – 觀察者模式JavaScript設計模式
- 購物直播系統開發,APP開發(功能)APP
- 建立個人知識體系的實踐
- Laravel 生產實踐:為個人網站配置谷歌 reCAPTCHA 身份驗證系統Laravel網站谷歌APT
- PHP開發中多種方案實現高併發下的搶購、秒殺功能PHP
- 好必購商城系統app開發APP
- Thinkphp開發簡單購物系統PHP
- 一個人的網站開發網站
- Redis 實現高併發下的搶購 / 秒殺功能Redis
- 求購《Java實用系統開發指南》第二版Java
- PHP 開發淘寶代購系統原始碼 + 代購程式原始碼 + 代購集運系統PHP原始碼
- 物聯網開發最佳實踐
- 《JavaScript設計模式與開發實踐》模式篇(5)—— 觀察者模式JavaScript設計模式
- 湘宜購商城系統開發/湘宜購商城小程式開發技術方案
- 求購個JAVA開發的OA系統,要求能二次開發Java
- RabbitMQ個人實踐MQ
- Laravel 高併發搶購模擬Laravel
- 搶購活動的粗略設計和實現
- 分散式儲存系統的最佳實踐:系統發展路徑分散式
- 電商搶購秒殺系統的設計及應用場景分析
- php+redis實現搶購功能PHPRedis
- 全棧開發--vue.js+php開發個人部落格系統全棧Vue.jsPHP
- 網格交易系統開發
- 直播系統原始碼搶佔網際網路市場很有“發言權”原始碼
- 網上搶購茅臺催生黃牛黨:必須嚴打各類搶購軟體
- 系統開發中的實體BeanBean
- Promise的個人理解及實踐Promise
- 鏈動2+1系統復購模式開發模式
- 網曝小米研發自主作業系統 MIOS首次曝光作業系統iOS
- 小米路由器搭建個人網站教程 小米路由怎麼搭建網站?路由器網站
- 優酷鴻蒙開發實踐|多屏互動開發實踐鴻蒙
- 歡樂拼購商城系統開發,Viiva購歡樂拼購拼團模式介紹模式