一些資料
大家還記得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。

圖1 大秒系統動靜分離
除此之外還有如下特點:
把整個頁面cache在使用者浏覽器
如果強制重新整理整個頁面,也會請求到cdn
實際有效請求隻是“重新整理搶寶”按鈕
這樣把90%的靜态資料緩存在使用者端或者cdn上,當真正秒殺時使用者隻需要點選特殊的按鈕“重新整理搶寶”即可,而不需要重新整理整個頁面,這樣隻向服務端請求很少的有效資料,而不需要重複請求大量靜态資料。秒殺的動态資料和普通的詳情頁面的動态資料相比更少,性能也比普通的詳情提升3倍以上。是以“重新整理搶寶”這種設計思路很好地解決了不重新整理頁面就能請求到服務端最新的動态資料。
基于時間分片削峰
熟悉淘寶秒殺的都知道,第一版的秒殺系統本身并沒有答題功能,後面才增加了秒殺答題,當然秒殺答題一個很重要的目的是為了防止秒殺器,2011年秒殺非常火的時候,秒殺器也比較猖獗,而沒有達到全民參與和營銷的目的,是以增加的答題來限制秒殺器。增加答題後,下單的時間基本控制在2s後,秒殺器的下單比例也下降到5%以下。新的答題頁面如圖2。
圖2 秒答題頁面
其實增加答題還有一個重要的功能,就是把峰值的下單請求給拉長了,從以前的1s之内延長到2~10s左右,請求峰值基于時間分片了,這個時間的分片對服務端處理并發非常重要,會減輕很大壓力,另外由于請求的先後,靠後的請求自然也沒有庫存了,也根本到不了最後的下單步驟,是以真正的并發寫就非常有限了。其實這種設計思路目前也非常普遍,如支付寶的“咻一咻”已及微信的搖一搖。
除了在前端通過答題在使用者端進行流量削峰外,在服務端一般通過鎖或者隊列來控制瞬間請求。
資料分層校驗
圖3 分層校驗
對大流量系統的資料做分層校驗也是最重要的設計原則,所謂分層校驗就是對大量的請求做成“漏鬥”式設計,如圖3所示:在不同層次盡可能把無效的請求過濾,“漏鬥”的最末端才是有效的請求,要達到這個效果必須對資料做分層的校驗,下面是一些原則:
先做資料的動靜分離
将90%的資料緩存在用戶端浏覽器
将動态請求的讀資料cache在web端
對讀資料不做強一緻性校驗
對寫資料進行基于時間的合理分片
對寫請求做限流保護
對寫資料進行強一緻性校驗
秒殺系統正是按照這個原則設計的系統架構,如圖4所示。
圖4 秒殺系統分層架構
把大量靜态不需要檢驗的資料放在離使用者最近的地方;在前端讀系統中檢驗一些基本資訊,如使用者是否具有秒殺資格、商品狀态是否正常、使用者答題是否正确、秒殺是否已經結束等;在寫資料系統中再校驗一些如是否是非法請求,營銷等價物是否充足(淘金币等),寫的資料一緻性如檢查庫存是否還有等;最後在資料庫層保證資料最終準确性,如庫存不能減為負數。
實時熱點發現
其實秒殺系統本質是還是一個資料讀的熱點問題,而且是最簡單一種,因為在文提到通過業務隔離,我們已能提前識别出這些熱點資料,我們可以提前做一些保護,提前識别的熱點資料處理起來還相對簡單,比如分析曆史成交記錄發現哪些商品比較熱門,分析使用者的購物車記錄也可以發現那些商品可能會比較好賣,這些都是可以提前分析出來的熱點。比較困難的是那種我們提前發現不了突然成為熱點的商品成為熱點,這種就要通過實時熱點資料分析了,目前我們設計可以在3s内發現交易鍊路上的實時熱點資料,然後根據實時發現的熱點資料每個系統做實時保護。 具體實作如下:
建構一個異步的可以收集交易鍊路上各個中間件産品如tengine、tair緩存、hsf等本身的統計的熱點key(tengine和tair緩存等中間件産品本身已經有熱點統計子產品)。
建立一個熱點上報和可以按照需求訂閱的熱點服務的下發規範,主要目的是通過交易鍊路上各個系統(詳情、購物車、交易、優惠、庫存、物流)通路的時間差,把上遊已經發現的熱點能夠透傳給下遊系統,提前做好保護。比如大促高峰期詳情系統是最早知道的,在統計接入層上tengine子產品統計的熱點url。
将上遊的系統收集到熱點資料發送到熱點服務台上,然後下遊系統如交易系統就會知道哪些商品被頻繁調用,然後做熱點保護。如圖5所示。
圖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所示。
圖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://wely.iteye.com/blog/2282093]