小米网抢购系统开发实践和我的个人观察
本文個人觀察部分,為自己的一點看法。
正文內容,轉載于
《程序員》2014年11月刊:電商峰值系統架構設計
http://www.csdn.net/article/2014-11-04/2822459
個人觀察
1.小米搶購系統,是在小米電商比較成功之后,才開發掛在電商平臺上的。 因此,搶購系統剛剛上線,就有很大的流量。 而普通的網站,剛剛上線,流量是逐步增加的。2.一個周就重新實現了搶購系統,也太牛了吧,似乎有夸張的成份。 3.在現有網站里改造,確實比較難,不能有任何的失誤。 升級搶購系統,算是一次比較大的重構。 為了新功能,把已有功能搞出問題了,可是大事。 4.把同步換成異步,是“第一版搶購系統”的核心思想。 類似于電商購物流程中的,把“下單”和“支付”分成2個階段。 5.空間換時間。 Redis,數據都存儲在內存中,少數Redis有持久化配置,以防萬一。 6.搶購系統的業務似乎并不復雜。 有幾個系統,記錄點日志,異步讀取日志并處理,一周就能干完的。 7.用戶只需要點1個按鈕,按鈕背后卻是有很多的故事。 作為用戶也真不錯,不用想那么多煩心的事。 生活中,還有很多這樣的事。 8.看了系統架構圖和思路,也就那樣。 大公司,知名公司,技術解決方案也都是在業界成熟的基礎技術之上實現的,更多還是側重公司的業務、技術思想、工程實踐、模擬測試。 視野開闊了,很多技術問題,根本沒有那么難。 除了學習能力,信息不對稱,也是個關鍵的問題。 現在互聯網很普及了,信息不對稱這個問題,比早些年好多了,我們真是趕上了一個好年頭。 9.強一致性。 看了不少高并發大流量的網站設計,基本都是在“一致性”方面有所讓步。只滿足最終的一致性,中間步驟出現不一致性,不影響大局。 10.node.js異步編程。 2014年用過一段時間的node.js,性能很不錯,異步編程很不習慣。完全人工的去寫異步代碼,太痛苦了。 好在node.js有一些框架,可以手寫“同步過程代碼”,內部轉換成異步的。 聽說淘寶內部,也很早就開始實踐node.js了。 個人覺得,業務教簡單,性能要求高的系統,可以考慮用node.js實現。 如果只是想做個web網站,真心不建議用node.js。 11.用文件實現分布式鎖。 在PHP服務器上,通過一個文件來表示商品是否售罄。 用文件實現分布式鎖,是可以實現的。不過有個問題。 PHP服務器,肯定不止一臺吧。那么這個文件存放在哪里呢?不太可能每個服務器上都存一份吧。如果這樣, 文件至少會有同步的過程。 如果文件是放在,單獨的服務器上,1臺或者2臺,這樣如何呢?想了想,和前一種情況并沒有本質的區別。 只用1臺,比較簡單,但萬一掛了 ,分布式鎖服務就沒了。
“通過一個文件來表示商品是否售罄”又想了想,感覺自己理解錯了。這個場景并不是“用文件實現分布式鎖”。 商品售罄的時候,在每一個PHP服務器上,放一個文件嗎? 在放文件的過程中,又來了搶購這個商品的情況,怎么辦呢?
12.分析業務,做出架構,編碼實現,會比較高效。 看這篇總結,思路還是挺清晰的。 不過,我倒是覺得,他們在開發過程中,肯定發現了一些新問題的。 我們現在看到的只是最終的版本。
另外,層次劃分也很重要,HTTP層,業務層,不同的層,干不同的事情。 13.這個搶購系統的研發過程,提現了“技術人員”“程序員”這個職業,可以是“智力密集型”的。 每天碼重復代碼,也真是夠坑的。 生產力上不去,未來怎么進一步發展呢。 14.畫圖很重要。 看架構圖、流程圖,非常重要。 梳理自己的,檢驗自己的設計,方便與人交流。 15.PHP、node.js、go。 這么多語言,要學習好多內容額。 在這種項目,有人帶,跟著學習和實踐,成長會比較快。 我在想一個問題,那些寫PHP系統的哥們,可以看到nodejs代碼嗎? 16.小米搶購系統,只有少部分程序員可以接觸到。 大部分的公司,都是創業小公司、業務量不大的公司。 很多程序員沒有機會去實踐,自己去寫和模擬,動力不足,不容易堅持下去。 幫公司做項目,自己增加經驗,還有錢拿,多好。
17.互聯網系統架構的資料,越來越多。 想要提升,更多還是在于實踐
搶購系統是怎樣誕生的
時間回到2011年底。小米公司在這一年8月16日首次發布了手機,立刻引起了市場轟動。隨后,在一天多的時間內預約了30萬臺。之后的幾個月,這30萬臺小米手機通過排號的方式依次發貨,到當年年底全部發完。
然后便是開放購買。最初的開放購買直接在小米的商城系統上進行,但我們那時候完全低估了“搶購”的威力。瞬間爆發的平常幾十倍流量迅速淹沒了小米網商城服務器,數據庫死鎖、網頁刷新超時,用戶購買體驗非常差。
市場需求不等人,一周后又要進行下一輪開放搶購。一場風暴就等在前方,而我們只有一周的時間了,整個開發部都承擔著巨大的壓力。
小米網可以采用的常規優化手段并不太多,增加帶寬、服務器、尋找代碼中的瓶頸點優化代碼。但是,小米公司只是一家剛剛成立一年多的小公司,沒有那么多的服務器和帶寬。而且,如果代碼中有瓶頸點,即使能增加一兩倍的服務器和帶寬,也一樣會被瞬間爆發的幾十倍負載所沖垮。而要優化商城的代碼,時間上已沒有可能。電商網站很復雜,說不定某個不起眼的次要功能,在高負載情況下就會成為瓶頸點拖垮整個網站。
這時開發組面臨一個選擇,是繼續在現有商城上優化,還是單獨搞一套搶購系統?我們決定冒險一試,我和幾個同事一起突擊開發一套獨立的搶購系統,希望能夠絕境逢生。
擺在我們面前的是一道似乎無解的難題,它要達到的目標如下:
- 只有一周時間,一周內完成設計、開發、測試、上線;
- 失敗的代價無法承受,系統必須順暢運行;
- 搶購結果必須可靠;
- 面對海量用戶的并發搶購,商品不能賣超;
- ?一個用戶只能搶一臺手機;
- 用戶體驗盡量好些。
設計方案就是多個限制條件下求得的解。時間、可靠性、成本,這是我們面臨的限制條件。要在那么短的時間內解決難題,必須選擇最簡單可靠的技術,必須是經過足夠驗證的技術,解決方案必須是最簡單的。
在高并發情況下,影響系統性能的一個關鍵因素是:數據的一致性要求。在前面所列的目標中,有兩項是關于數據一致性的:商品剩余數量、用戶是否已經搶購成功。如果要保證嚴格的數據一致性,那么在集群中需要一個中心服務器來存儲和操作這個值。這會造成性能的單點瓶頸。
在分布式系統設計中,有一個CAP原理?!耙恢滦?、可用性、分區容忍性”三個要素最多只能同時實現兩點,不可能三者兼顧。我們要面對極端的爆發流量負載,分區容忍性和可用性會非常重要,因此決定犧牲數據的強一致性要求。
做出這個重要的決定后,剩下的設計決定就自然而然地產生了:
最后的系統原理見后面的第一版搶購系統原理圖(圖1)。
圖1 ?第一版搶購系統原理圖
系統基本原理:
在PHP服務器上,通過一個文件來表示商品是否售罄。如果文件存在即表示已經售罄。PHP程序接收用戶搶購請求后,查看用戶是否預約以及是否搶購過,然后檢查售罄標志文件是否存在。對預約用戶,如果未售罄并且用戶未搶購成功過,即返回搶購成功的結果,并記錄一條日志。日志通過異步的方式傳輸到中心控制節點,完成記數等操作。
最后,搶購成功用戶的列表異步導入商場系統,搶購成功的用戶在接下來的幾個小時內下單即可。這樣,流量高峰完全被搶購系統擋住,商城系統不需要面對高流量。
在這個分布式系統的設計中,對持久化數據的處理是影響性能的重要因素。
我們沒有選擇傳統關系型數據庫,而是選用了Redis服務器。
選用Redis基于下面幾個理由。
在整個系統中,最頻繁的I/O操作,就是PHP對Redis的讀寫操作。如果處理不好,Redis服務器將成為系統的性能瓶頸。
系統中對Redis的操作包含三種類型的操作:查詢是否有預約、是否搶購成功、寫入搶購成功狀態。為了提升整體的處理能力,可采用讀寫分離方式。
所有的讀操作通過從庫完成,所有的寫操作只通過控制端一個進程寫入主庫。
在PHP對Redis服務器的讀操作中,需要注意的是連接數的影響。如果PHP是通過短連接訪問Redis服務器的,則在高峰時有可能堵塞Redis服務器,造成雪崩效應。這一問題可以通過增加Redis從庫的數量來解決。
而對于Redis的寫操作,在我們的系統中并沒有壓力。因為系統是通過異步方式,收集PHP產生的日志,由一個管理端的進程來順序寫入Redis主庫。
另一個需要注意的點是Redis的持久化配置。用戶的預約信息全部存儲在Redis的進程內存中,它向磁盤保存一次,就會造成一次等待。嚴重的話會導致搶購高峰時系統前端無法響應。因此要盡量避免持久化操作。我們的做法是,所有用于讀取的從庫完全關閉持久化,一個用于備份的從庫打開持久化配置。同時使用日志作為應急恢復的保險措施。
整個系統使用了大約30臺服務器,其中包括20臺PHP服務器,以及10臺Redis服務器。
在接下來的搶購中,它順利地抗住了壓力?;叵肫甬敃r的場景,真是非常的驚心動魄。
第二版搶購系統
經過了兩年多的發展,小米網已經越來越成熟。公司準備在2014年4月舉辦一次盛大的“米粉節”活動。這次持續一整天的購物狂歡節是小米網電商的一次成人禮。商城前端、庫存、物流、售后等環節都將經歷一次考驗。
對于搶購系統來說,最大的不同就是一天要經歷多輪搶購沖擊,而且有多種不同商品參與搶購。我們之前的搶購系統,是按照一周一次搶購來設計及優化的,根本無法支撐米粉節復雜的活動。而且經過一年多的修修補補,第一版搶購系統積累了很多的問題,正好趁此機會對它進行徹底重構。
第二版系統主要關注系統的靈活性與可運營性(圖2)。對于高并發的負載能力,穩定性、準確性這些要求,已經是基礎性的最低要求了。我希望將這個系統做得可靈活配置,支持各種商品各種條件組合,并且為將來的擴展打下良好的基礎。
圖2 ?第二版系統總體結構圖
在這一版中,搶購系統與商城系統依然隔離,兩個系統之間通過約定的數據結構交互,信息傳遞精簡。通過搶購系統確定一個用戶搶得購買資格后,用戶自動在商城系統中將商品加入購物車。
在之前第一版搶購系統中,我們后來使用Go語言開發了部分模塊,積累了一定的經驗。因此第二版系統的核心部分,我們決定使用Go語言進行開發。
我們可以讓Go程序常駐內存運行,各種配置以及狀態信息都可以保存在內存中,減少I/O操作開銷。對于商品數量信息,可以在進程內進行操作。不同商品可以分別保存到不同的服務器的Go進程中,以此來分散壓力,提升處理速度。
系統服務端主要分為兩層架構,即HTTP服務層和業務處理層。HTTP服務層用于維持用戶的訪問請求,業務處理層則用于進行具體的邏輯判斷。兩層之間的數據交互通過消息隊列來實現。
HTTP服務層主要功能如下:
業務處理層主要功能如下:
用戶的搶購請求通過消息隊列,依次進入業務處理層的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方式導致已完成處理的用戶連接不能盡快關閉,連接無法釋放,導致連接數不斷增加,對服務端的內存和帶寬都有影響。
通過上面的分析,我們的解決辦法如下。
通過這樣的改進,我們的HTTP前端服務器最大穩定連接數可以超過一百萬。
第二版搶購系統順利完成了米粉節的考驗。
總結
技術方案需要依托具體的問題而存在。脫離了應用場景,無論多么酷炫的技術都失去了價值。搶購系統面臨的現實問題復雜多變,我們也依然在不斷地摸索改進。
作者韓祝鵬,小米公司程序員。早期負責MIUI系統發布與運營,后帶領小米網系統組設計與開發小米網搶購系統。
小米網搶購系統開發實踐和我的個人觀察
來源:http://www.mamicode.com/info-detail-1293706.html
總結
以上是生活随笔為你收集整理的小米网抢购系统开发实践和我的个人观察的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信申请工商银行信用卡多久知道通过没有
- 下一篇: ipo是什么