導讀:最初的秒殺系統的原型是淘寶詳情上的定時上架功能,由於有些賣家爲了吸引眼球,把價格壓得很是低。但這給的詳情繫統帶來了很是大壓力,爲了將這樣的突發流量隔離,才設計了秒殺系統。文章主要介紹大秒系統以及這樣的典型讀數據的熱點問題的解決思路和實踐經驗。前端
你們還記得2013年的小米秒殺嗎?三款小米手機各11萬臺開賣,走的都是大秒系統。3分鐘後成爲雙十一第一家也是最快破億的旗艦店。數據庫
通過日誌統計,前端系統雙11峯值有效請求約60w以上的QPS 。然後端cache的集羣峯值近2000w/s、單機也近30w/s,但到真正的寫時流量要小很是多了,當時最高下單減庫存tps是紅米創造。達到1500/s。後端
秒殺系統設計的第一個原則就是將這樣的熱點數據隔離出來,不要讓1%的請求影響到另外的99%。隔離出來後也更方便對這1%的請求作針對性優化。瀏覽器
針對秒殺咱們作了多個層次的隔離:緩存
業務隔離。把秒殺作成一種營銷活動,賣家要參加秒殺這樣的營銷活動需要單獨報名,從技術上來講,賣家報名後對咱們來講就是已知熱點。當真正開始時咱們可以提早作好預熱。性能優化
系統隔離。系統隔離不少其它是執行時的隔離,可以經過分組部署的方式和另外99%分開。秒殺還申請了單獨的域名,目的也是讓請求落到不一樣的集羣中。微信
數據隔離。cookie
秒殺所調用的數據大部分都是熱數據,比方會啓用單獨cache集羣或MySQL數據庫來放熱點數據,眼下也是不想0.01%的數據影響另外99.99%。網絡
固然實現隔離很是有多辦法,如可以依照用戶來區分。給不一樣用戶分配不一樣cookie。在接入層路由到不一樣服務接口中。還有在接入層可以對URL的不一樣Path來設置限流策略等。服務層經過調用不一樣的服務接口;數據層可以給數據打上特殊的標來區分。架構
目的都是把已經識別出來的熱點和普通請求區分開來。
前面介紹在系統層面上的原則是要作隔離,接下去就是要把熱點數據進行動靜分離,這也是解決大流量系統的一個重要原則。
怎樣給系統作動靜分離的靜態化改造我曾經寫過一篇《高訪問量系統的靜態化架構設計》具體介紹了淘寶商品系統的靜態化設計思路。感興趣的可以在《程序猿》雜誌上找一下。咱們的大秒系統是從商品詳情繫統發展而來,因此自己已經實現了動靜分離。如圖1。
除此以外還有例如如下特色:
把整個頁面Cache在用戶瀏覽器
假設強制刷新整個頁面,也會請求到CDN
實際有效請求僅僅是「刷新搶寶」button
這樣把90%的靜態數據緩存在用戶端或者CDN上,當真正秒殺時用戶僅僅需要點擊特殊的button「刷新搶寶」就能夠,而不需要刷新整個頁面,這樣僅僅向服務端請求很是少的有效數據,而不需要反覆請求大量靜態數據。秒殺的動態數據和普通的詳情頁面的動態數據相比更少,性能也比普通的詳情提高3倍以上。
因此「刷新搶寶」這樣的設計思路很是好地攻克了不刷新頁面就能請求到服務端最新的動態數據。
基於時間分片削峯
熟悉淘寶秒殺的都知道。初版的秒殺系統自己並無答題功能,後面才添加了秒殺答題,固然秒殺答題一個很是重要的目的是爲了防止秒殺器,2011年秒殺很是火的時候,秒殺器也比較猖獗,而沒有達到全民參與和營銷的目的。因此添加的答題來限制秒殺器。添加答題後。下單的時間基本控制在2s後,秒殺器的下單比例也降低到5%下面。新的答題頁面如圖2。
事實上添加答題另外一個重要的功能。就是把峯值的下單請求給拉長了。從曾經的1s以內延長到2~10s左右,請求峯值基於時間分片了,這個時間的分片對服務端處理併發很重要,會減輕很大壓力,另外由於請求的前後,靠後的請求天然也沒有庫存了,也根本到不了最後的下單步驟,因此真正的併發寫就頗有限了。事實上這樣的設計思路眼下也很廣泛,如支付寶的「咻一咻」已及微信的搖一搖。
除了在前端經過答題在用戶端進行流量削峯外。在服務端通常經過鎖或者隊列來控制瞬間請求。
對大流量系統的數據作分層校驗也是最重要的設計原則。所謂分層校驗就是對大量的請求作成「漏斗」式設計。如圖3所看到的:在不一樣層次儘量把無效的請求過濾,「漏斗」的最末端纔是有效的請求,要達到這個效果必須對數據作分層的校驗,如下是一些原則:
先作數據的動靜分離
將90%的數據緩存在client瀏覽器
將動態請求的讀數據Cache在Web端
對讀數據不作強一致性校驗
對寫數據進行基於時間的合理分片
對寫請求作限流保護
對寫數據進行強一致性校驗
秒殺系統正是依照這個原則設計的系統架構。如圖4所看到的。
把大量靜態不需要檢驗的數據放在離用戶近期的地方。在前端讀系統中檢驗一些基本信息,如用戶是否具備秒殺資格、商品狀態是否正常、用戶答題是否正確、秒殺是否已經結束等。在寫數據系統中再校驗一些如是不是非法請求,營銷等價物是否充足(淘金幣等),寫的數據一致性如檢查庫存是否還有等;最後在數據庫層保證數據終於準確性。如庫存不能減爲負數。
事實上秒殺系統本質是仍是一個數據讀的熱點問題,而且是最簡單一種,因爲在文提到經過業務隔離,咱們已能提早識別出這些熱點數據,咱們可以提早作一些保護,提早識別的熱點數據處理起來還相對簡單,比方分析歷史成交記錄發現哪些商品比較熱門,分析用戶的購物車記錄也可以發現那些商品可能會比較好賣。這些都是可以提早分析出來的熱點。比較困難的是那種咱們提早發現不了忽然成爲熱點的商品成爲熱點。這樣的就要經過實時熱點數據分析了。眼下咱們設計可以在3s內發現交易鏈路上的實時熱點數據。而後依據實時發現的熱點數據每個系統作實時保護。
詳細實現例如如下:
構建一個異步的可以收集交易鏈路上各個中間件產品如Tengine、Tair緩存、HSF等自己的統計的熱點key(Tengine和Tair緩存等中間件產品自己已經有熱點統計模塊)。
創建一個熱點上報和能夠依照需求訂閱的熱點服務的下發規範。主要目的是經過交易鏈路上各個系統(詳情、購物車、交易、優惠、庫存、物流)訪問的時間差,把上游已經發現的熱點能夠透傳給下游系統,提早作好保護。
比方大促高峯期詳情繫統是最先知道的,在統計接入層上Tengine模塊統計的熱點URL。
將上游的系統收集到熱點數據發送到熱點服務檯上,而後下游系統如交易系統就會知道哪些商品被頻繁調用。而後作熱點保護。如圖5所看到的。
重要的幾個:當中關鍵部分包含:
這個熱點服務後臺抓取熱點數據日誌最好是異步的,一方面便於作到通用性。還有一方面不影響業務系統和中間件產品的主流程。
熱點服務後臺、現有各個中間件和應用在作的沒有代替關係,每個中間件和應用還需要保護本身,熱點服務後臺提供一個收集熱點數據提供熱點訂閱服務的統一規範和工具,便於把各個系統熱點數據透明出來。
熱點發現要作到實時(3s內)。
前面介紹了一些怎樣設計大流量讀系統中用到的原則。但是當這些手段都用了,仍是有大流量涌入該怎樣處理呢?秒殺系統要解決幾個關鍵問題。
Java處理大並發動態請求優化
事實上Java和通用的Webserver相比(Nginx或Apache)在處理大併發HTTP請求時要弱一點,因此通常咱們都會對大流量的Web系統作靜態化改造,讓大部分請求和數據直接在Nginxserver或者Web代理server(Varnish、Squid等)上直接返回(可以下降數據的序列化與反序列化)。不要將請求落到Java層上。讓Java層僅僅處理很是少數據量的動態請求,固然針對這些請求也有一些優化手段可以使用:
直接使用Servlet處理請求。避免使用傳統的MVC框架或許能繞過一大堆複雜且用處不大的處理邏輯,節省個1ms時間。固然這個取決於你對MVC框架的依賴程度。
直接輸出流數據。使用resp.getOutputStream()而不是resp.getWriter()可以省掉一些不變字符數據編碼。也能提高性能;還有數據輸出時也推薦使用JSON而不是模板引擎(通常都是解釋運行)輸出頁面。
同一商品大併發讀問題
你會說這個問題很是easy解決,無非放到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自己也有瓶頸問題,一旦請求量達到單機極限也會存在熱點保護問題。有時看起來好像很是easy解決。比方說作好限流便可。但你想一想一旦某個熱點觸發了一臺機器的限流閥值,那麼這臺機器Cache的數據都將無效,進而間接致使Cache被擊穿。請求落地應用層數據庫出現雪崩現象。
這類問題需要與詳細Cache產品結合才幹有比較好的解決方式。這裏提供一個通用的解決思路,就是在Cache的client端作本地Localcache,當發現熱點數據時直接Cache在client裏。而不要請求到Cache的Server。
數據更新熱點,更新問題除了前面介紹的熱點隔離和排隊處理以外,還有些場景,如對商品的lastmodifytime字段更新會很頻繁,在某些場景下這些多條SQL是可以合併的。必定時間內僅僅運行最後一條SQL便可了,可以下降對數據庫的update操做。另外熱點商品的本身主動遷移,理論上也可以在數據路由層來完畢,利用前面介紹的熱點實時發現本身主動將熱點從普通庫裏遷移出來放到單獨的熱點庫中。
依照某種維度建的索引產生熱點數據,比方實時搜索中依照商品維度關聯評價數據。有些熱點商品的評價許多,致使搜索系統依照商品ID建評價數據的索引時內存已經放不下,交易維度關聯訂單信息也相同有這些問題。這類熱點數據需要作數據散列,再添加一個維度,把數據又一次組織。
做者簡單介紹:許令波,2009年增長淘寶,眼下負責商品詳情業務和穩定性相關工做,長期關注性能優化領域。參與了淘寶高訪問量Web系統基本的優化項目,著有《深刻分析Java Web技術內幕》一書。我的站點http://xulingbo.net