1. 程式人生 > >一個秒殺系統設計詳解

一個秒殺系統設計詳解

一些資料:

大家還記得2013年的小米秒殺嗎?三款小米手機各11萬臺開賣,走的都是大秒系統,3分鐘後成為雙十一第一家也是最快破億的旗艦店。經過日誌統計,前端系統雙11峰值有效請求約60w以上的QPS ,而後端cache的叢集峰值近2000w/s、單機也近30w/s,但到真正的寫時流量要小很多了,當時最高下單減庫存tps是紅米創造,達到1500/s。

熱點隔離:

秒殺系統設計的第一個原則就是將這種熱點資料隔離出來,不要讓1%的請求影響到另外的99%,隔離出來後也更方便對這1%的請求做針對性優化。針對秒殺我們做了多個層次的隔離:

 

  • 業務隔離。把秒殺做成一種營銷活動,賣家要參加秒殺這種營銷活動需要單獨報名,從技術上來說,賣家報名後對我們來說就是已知熱點,當真正開始時我們可以提前做好預熱。

  • 系統隔離。系統隔離更多是執行時的隔離,可以通過分組部署的方式和另外99%分開。秒殺還申請了單獨的域名,目的也是讓請求落到不同的叢集中。

  • 資料隔離。秒殺所呼叫的資料大部分都是熱資料,比如會啟用單獨cache叢集或MySQL資料庫來放熱點資料,目前也是不想0.01%的資料影響另外99.99%。

當然實現隔離很有多辦法,如可以按照使用者來區分,給不同使用者分配不同cookie,在接入層路由到不同服務介面中;還有在接入層可以對URL的不同Path來設定限流策略等。服務層通過呼叫不同的服務介面;資料層可以給資料打上特殊的標來區分。目的都是把已經識別出來的熱點和普通請求區分開來。

動靜分離:

前面介紹在系統層面上的原則是要做隔離,接下去就是要把熱點資料進行動靜分離,這也是解決大流量系統的一個重要原則。如何給系統做動靜分離的靜態化改造我以前寫過一篇《高訪問量系統的靜態化架構設計》詳細介紹了淘寶商品系統的靜態化設計思路,感興趣的可以在《程式設計師》雜誌上找一下。我們的大秒系統是從商品詳情繫統發展而來,所以本身已經實現了動靜分離,如圖1。

除此之外還有如下特點:

 

  • 把整個頁面Cache在使用者瀏覽器

  • 如果強制重新整理整個頁面,也會請求到CDN

  • 實際有效請求只是“重新整理搶寶”按鈕

這樣把90%的靜態資料快取在使用者端或者CDN上,當真正秒殺時使用者只需要點選特殊的按鈕“重新整理搶寶”即可,而不需要重新整理整個頁面,這樣只向服務端請求很少的有效資料,而不需要重複請求大量靜態資料。秒殺的動態資料和普通的詳情頁面的動態資料相比更少,效能也比普通的詳情提升3倍以上。所以“重新整理搶寶”這種設計思路很好地解決了不重新整理頁面就能請求到服務端最新的動態資料。

基於時間分片削峰

熟悉淘寶秒殺的都知道,第一版的秒殺系統本身並沒有答題功能,後面才增加了秒殺答題,當然秒殺答題一個很重要的目的是為了防止秒殺器,2011年秒殺非常火的時候,秒殺器也比較猖獗,而沒有達到全民參與和營銷的目的,所以增加的答題來限制秒殺器。增加答題後,下單的時間基本控制在2s後,秒殺器的下單比例也下降到5%以下。新的答題頁面如圖2。

 

其實增加答題還有一個重要的功能,就是把峰值的下單請求給拉長了,從以前的1s之內延長到2~10s左右,請求峰值基於時間分片了,這個時間的分片對服務端處理併發非常重要,會減輕很大壓力,另外由於請求的先後,靠後的請求自然也沒有庫存了,也根本到不了最後的下單步驟,所以真正的併發寫就非常有限了。其實這種設計思路目前也非常普遍,如支付寶的“咻一咻”已及微信的搖一搖。

 

除了在前端通過答題在使用者端進行流量削峰外,在服務端一般通過鎖或者佇列來控制瞬間請求。

資料分層校驗

對大流量系統的資料做分層校驗也是最重要的設計原則,所謂分層校驗就是對大量的請求做成“漏斗”式設計,如圖3所示:在不同層次儘可能把無效的請求過濾,“漏斗”的最末端才是有效的請求,要達到這個效果必須對資料做分層的校驗,下面是一些原則:

 

  • 先做資料的動靜分離

  • 將90%的資料快取在客戶端瀏覽器

  • 將動態請求的讀資料Cache在Web端

  • 對讀資料不做強一致性校驗

  • 對寫資料進行基於時間的合理分片

  • 對寫請求做限流保護

  • 對寫資料進行強一致性校驗

秒殺系統正是按照這個原則設計的系統架構,如圖4所示。

把大量靜態不需要檢驗的資料放在離使用者最近的地方;在前端讀系統中檢驗一些基本資訊,如使用者是否具有秒殺資格、商品狀態是否正常、使用者答題是否正確、秒殺是否已經結束等;在寫資料系統中再校驗一些如是否是非法請求,營銷等價物是否充足(淘金幣等),寫的資料一致性如檢查庫存是否還有等;最後在資料庫層保證資料最終準確性,如庫存不能減為負數。

實時熱點發現:

其實秒殺系統本質是還是一個數據讀的熱點問題,而且是最簡單一種,因為在文提到通過業務隔離,我們已能提前識別出這些熱點資料,我們可以提前做一些保護,提前識別的熱點資料處理起來還相對簡單,比如分析歷史成交記錄發現哪些商品比較熱門,分析使用者的購物車記錄也可以發現那些商品可能會比較好賣,這些都是可以提前分析出來的熱點。比較困難的是那種我們提前發現不了突然成為熱點的商品成為熱點,這種就要通過實時熱點資料分析了,目前我們設計可以在3s內發現交易鏈路上的實時熱點資料,然後根據實時發現的熱點資料每個系統做實時保護。 具體實現如下:

 

  • 構建一個非同步的可以收集交易鏈路上各個中介軟體產品如Tengine、Tair快取、HSF等本身的統計的熱點key(Tengine和Tair快取等中介軟體產品本身已經有熱點統計模組)。

  • 建立一個熱點上報和可以按照需求訂閱的熱點服務的下發規範,主要目的是通過交易鏈路上各個系統(詳情、購物車、交易、優惠、庫存、物流)訪問的時間差,把上游已經發現的熱點能夠透傳給下游系統,提前做好保護。比如大促高峰期詳情繫統是最早知道的,在統計接入層上Tengine模組統計的熱點URL。

  • 將上游的系統收集到熱點資料傳送到熱點服務檯上,然後下游系統如交易系統就會知道哪些商品被頻繁呼叫,然後做熱點保護。如圖5所示。

重要的幾個:其中關鍵部分包括:

 

  • 這個熱點服務後臺抓取熱點資料日誌最好是非同步的,一方面便於做到通用性,另一方面不影響業務系統和中介軟體產品的主流程。

  • 熱點服務後臺、現有各個中介軟體和應用在做的沒有取代關係,每個中介軟體和應用還需要保護自己,熱點服務後臺提供一個收集熱點資料提供熱點訂閱服務的統一規範和工具,便於把各個系統熱點資料透明出來。

  • 熱點發現要做到實時(3s內)。

關鍵技術及優化點:

前面介紹了一些如何設計大流量讀系統中用到的原則,但是當這些手段都用了,還是有大流量湧入該如何處理呢?秒殺系統要解決幾個關鍵問題。

 

Java處理大並發動態請求優化

 

其實Java和通用的Web伺服器相比(Nginx或Apache)在處理大併發HTTP請求時要弱一點,所以一般我們都會對大流量的Web系統做靜態化改造,讓大部分請求和資料直接在Nginx伺服器或者Web代理伺服器(Varnish、Squid等)上直接返回(可以減少資料的序列化與反序列化),不要將請求落到Java層上,讓Java層只處理很少資料量的動態請求,當然針對這些請求也有一些優化手段可以使用:

 

  • 直接使用Servlet處理請求。避免使用傳統的MVC框架也許能繞過一大堆複雜且用處不大的處理邏輯,節省個1ms時間,當然這個取決於你對MVC框架的依賴程度。

  • 直接輸出流資料。使用resp.getOutputStream()而不是resp.getWriter()可以省掉一些不變字元資料編碼,也能提升效能;還有資料輸出時也推薦使用JSON而不是模板引擎(一般都是解釋執行)輸出頁面。

 

同一商品大併發讀問題

 

你會說這個問題很容易解決,無非放到Tair快取裡面就行,集中式Tair快取為了保證命中率,一般都會採用一致性Hash,所以同一個key會落到一臺機器上,雖然我們的Tair快取機器單臺也能支撐30w/s的請求,但是像大秒這種級別的熱點商品還遠不夠,那如何徹底解決這種單點瓶頸?答案是採用應用層的Localcache,即在秒殺系統的單機上快取商品相關的資料,如何cache資料?也分動態和靜態:

 

  • 像商品中的標題和描述這些本身不變的會在秒殺開始之前全量推送到秒殺機器上並一直快取直到秒殺結束。

  • 像庫存這種動態資料會採用被動失效的方式快取一定時間(一般是數秒),失效後再去Tair快取拉取最新的資料。

 

你可能會有疑問,像庫存這種頻繁更新資料一旦資料不一致會不會導致超賣?其實這就要用到我們前面介紹的讀資料分層校驗原則了,讀的場景可以允許一定的髒資料,因為這裡的誤判只會導致少量一些原本已經沒有庫存的下單請求誤認為還有庫存而已,等到真正寫資料時再保證最終的一致性。這樣在資料的高可用性和一致性做平衡來解決這種高併發的資料讀取問題。

 

同一資料大併發更新問題

 

解決大併發讀問題採用Localcache和資料的分層校驗的方式,但是無論如何像減庫存這種大併發寫還是避免不了,這也是秒殺這個場景下最核心的技術難題。

 

同一資料在資料庫裡肯定是一行儲存(MySQL),所以會有大量的執行緒來競爭InnoDB行鎖,當併發度越高時等待的執行緒也會越多,TPS會下降RT會上升,資料庫的吞吐量會嚴重受到影響。說到這裡會出現一個問題,就是單個熱點商品會影響整個資料庫的效能,就會出現我們不願意看到的0.01%商品影響99.99%的商品,所以一個思路也是要遵循前面介紹第一個原則進行隔離,把熱點商品放到單獨的熱點庫中。但是無疑也會帶來維護的麻煩(要做熱點資料的動態遷移以及單獨的資料庫等)。

 

分離熱點商品到單獨的資料庫還是沒有解決併發鎖的問題,要解決併發鎖有兩層辦法。

 

  • 應用層做排隊。按照商品維度設定佇列順序執行,這樣能減少同一臺機器對資料庫同一行記錄操作的併發度,同時也能控制單個商品佔用資料庫連線的數量,防止熱點商品佔用太多資料庫連線。

  • 資料庫層做排隊。應用層只能做到單機排隊,但應用機器數本身很多,這種排隊方式控制併發仍然有限,所以如果能在資料庫層做全域性排隊是最理想的,淘寶的資料庫團隊開發了針對這種MySQL的InnoDB層上的patch,可以做到資料庫層上對單行記錄做到併發排隊,如圖6所示。

你可能會問排隊和鎖競爭不要等待嗎?有啥區別?如果熟悉MySQL會知道,InnoDB內部的死鎖檢測以及MySQL Server和InnoDB的切換會比較耗效能,淘寶的MySQL核心團隊還做了很多其他方面的優化,如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的patch,配合在SQL裡面加hint,在事務裡不需要等待應用層提交COMMIT而在資料執行完最後一條SQL後直接根據TARGET_AFFECT_ROW結果提交或回滾,可以減少網路的等待時間(平均約0.7ms)。據我所知,目前阿里MySQL團隊已將這些patch及提交給MySQL官方評審。

大促熱點問題思考:

以秒殺這個典型系統為代表的熱點問題根據多年經驗我總結了些通用原則:隔離、動態分離、分層校驗,必須從整個全鏈路來考慮和優化每個環節,除了優化系統提升效能,做好限流和保護也是必備的功課。

 

除去前面介紹的這些熱點問題外,淘系還有多種其他資料熱點問題:

 

  • 資料訪問熱點,比如Detail中對某些熱點商品的訪問度非常高,即使是Tair快取這種Cache本身也有瓶頸問題,一旦請求量達到單機極限也會存在熱點保護問題。有時看起來好像很容易解決,比如說做好限流就行,但你想想一旦某個熱點觸發了一臺機器的限流閥值,那麼這臺機器Cache的資料都將無效,進而間接導致Cache被擊穿,請求落地應用層資料庫出現雪崩現象。這類問題需要與具體Cache產品結合才能有比較好的解決方案,這裡提供一個通用的解決思路,就是在Cache的client端做本地Localcache,當發現熱點資料時直接Cache在client裡,而不要請求到Cache的Server。

  • 資料更新熱點,更新問題除了前面介紹的熱點隔離和排隊處理之外,還有些場景,如對商品的lastmodifytime欄位更新會非常頻繁,在某些場景下這些多條SQL是可以合併的,一定時間內只執行最後一條SQL就行了,可以減少對資料庫的update操作。另外熱點商品的自動遷移,理論上也可以在資料路由層來完成,利用前面介紹的熱點實時發現自動將熱點從普通庫裡遷移出來放到單獨的熱點庫中。

 

按照某種維度建的索引產生熱點資料,比如實時搜尋中按照商品維度關聯評價資料,有些熱點商品的評價非常多,導致搜尋系統按照商品ID建評價資料的索引時記憶體已經放不下,交易維度關聯訂單資訊也同樣有這些問題。這類熱點資料需要做資料雜湊,再增加一個維度,把資料重新組織。

 

 

 

 

轉載自 http://www.cnblogs.com/jifeng/p/5264268.html