作者:季楓;

來源:http://www.cnblogs.com/jifeng/p/5264268.html

阿里淘寶雙十一秒殺系統設計詳解

一些數據:

大家還記得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建評價數據的索引時內存已經放不下,交易維度關聯訂單信息也同樣有這些問題。這類熱點數據需要做數據散列,再增加一個維度,把數據重新組織。

相關文章