秒殺其實主要解決兩個問題,一個是併發讀,一個是併發寫前端
其實,秒殺的總體架構能夠歸納爲「穩、準、快」幾個關鍵字java
而後就是「準」,就是秒殺 10 臺 iPhone,那就只能成交 10 臺,多一臺少一臺都不行。一旦庫存不對,那平臺就要承擔損失,因此「準」就是要求保證數據的一致性。redis
最後再看「快」,「快」其實很好理解,它就是說系統的性能要足夠高,不然你怎麼支撐這麼大的流量呢?不光是服務端要作極致的性能優化,並且在整個請求鏈路上都要作協同的優化,每一個地方快一點,整個系統就完美了。算法
因此從技術角度上看「穩、準、快」,就對應了咱們架構上的高可用、一致性和高性能的要求,咱們的專欄也將主要圍繞這幾個方面來展開,具體以下。數據庫
高併發系統本質上就是一個知足大併發、高性能和高可用的分佈式系統
這個就很直觀了 , 請求數少併發量就少後端
高併發系統 1. 要下降系統依賴 , 防止由於依賴形成的各類問題 , 提升可用性 2. 下降流量入侵 , 大流量儘可能隔絕在外面瀏覽器
系統中的單點能夠說是系統架構上的一個大忌,由於單點意味着沒有備份,風險不可控 , 其次流量不能分發像redis這種會有熱點數據問題緩存
其實構建一個高併發系統並無那麼複雜 , 有一下的幾個方法能夠扛住比較高的併發性能優化
對電商來講系統差很少是這種樣子 :服務器
所謂動靜分離 , 就是將一些不常變化 , 能夠靜態化 , 無狀態 , 不須要邏輯處理的一些字段放在一個專門的系統或者地方 , 獲取的時候不須要走後端系統的方法
常見的就三種 , 用戶瀏覽器裏、CDN 上 或者 在服務端的 Cache 中
Web 代理服務器根據請求URL查找緩存,直接取出對應的 HTTP 響應頭和響應體而後直接返回,這個響應過程簡單得連 HTTP 協議都不用從新組裝,甚至連 HTTP 請求頭也不須要解析
這個沒有什麼好辦法 , 動態數據必定會將流量打到後端 , 因此儘量的減小這部分 , 若是不行就加機器
有 3 種方案可選:
就是使用內存緩存好比java的ehcache等
優勢 | 缺點 |
---|---|
無網絡開銷 | 佔用內存大 |
使用簡單 | 同步機制須要使用其餘方法保證 |
典型的就是redis集羣
優勢 | 缺點 |
---|---|
StartFragment單獨一個 Cache 層,能夠減小多個應用接入時使用 Cache 的成本。這樣接入的應用只要維護本身的 Java 系統就好,不須要單獨維護 Cache,而只關心如何使用便可 EndFragment | StartFragmentCache 層內部交換網絡成爲瓶頸 EndFragment |
StartFragment統一 Cache 的方案更易於維護,如後面增強監控、配置的自動化,只須要一套解決方案就行,統一塊兒來維護升級也比較方便。 EndFragment | StartFragment緩存服務器的網卡也會是瓶頸; EndFragment |
StartFragment能夠共享內存,最大化利用內存,不一樣系統之間的內存能夠動態切換,從而可以有效應對各類攻擊。 EndFragment | StartFragment機器少風險較大,掛掉一臺就會影響很大一部分緩存數據。 EndFragment |
要解決上面這些問題,能夠再對 Cache 作 Hash 分組,即一組 Cache 緩存的內容相同,這樣可以避免熱點數據過分集中致使新的瓶頸產生。 好比redis 熱點數據分組
在將整個系統作動靜分離後,咱們天然會想到更進一步的方案,就是將 Cache 進一步前移到 CDN 上,由於 CDN 離用戶最近,效果會更好
有如下幾個問題須要解決
由於上面的這些問題 , 因此cdn的部署方法通常都是分網絡分區域的中心化部署
首先,熱點請求會大量佔用服務器處理資源,雖然這個熱點可能只佔請求總量的億分之一,然而卻可能搶佔 90% 的服務器資源,若是這個熱點請求仍是沒有價值的無效請求,那麼對系統資源來講徹底是浪費。
其次,即便這些熱點是有效的請求,咱們也要識別出來作針對性的優化,從而用更低的代價來支撐這些熱點請求
所謂「熱點操做」,例如大量的刷新頁面、大量的添加購物車、雙十一零點大量的下單等都屬於此類操做。對系統來講,這些操做能夠抽象爲「讀請求」和「寫請求」,這兩種熱點請求的處理方式截然不同,讀請求的優化空間要大一些,而寫請求的瓶頸通常都在存儲層,優化的思路就是根據 CAP 理論作平衡
熱點數據」比較好理解,那就是用戶的熱點請求對應的數據。而熱點數據又分爲「靜態熱點數據」和「動態熱點數據」
所謂「靜態熱點數據」,就是可以提早預測的熱點數據。例如,咱們能夠經過賣家報名的方式提早篩選出來,經過報名系統對這些熱點商品進行打標。另外,咱們還能夠經過大數據分析來提早發現熱點商品,好比咱們分析歷史成交記錄、用戶的購物車記錄,來發現哪些商品可能更熱門、更好賣,這些都是能夠提早分析出來的熱點
所謂「動態熱點數據」,就是不能被提早預測到的,系統在運行過程當中臨時產生的熱點。例如,賣家在抖音上作了廣告,而後商品一下就火了,致使它在短期內被大量購買
靜態熱點數據能夠經過商業手段,例如強制讓賣家經過報名參加的方式提早把熱點商品篩選出來,實現方式是經過一個運營系統,把參加活動的商品數據進行打標,而後經過一個後臺系統對這些熱點商品進行預處理,如提早進行緩存 . 或者使用技術手段提早預測,例如對買家天天訪問的商品進行大數據計算,而後統計出 TOP N 的商品,咱們能夠認爲這些 TOP N 的商品就是熱點商品。
主要處理動態熱點數據的 , 都是使用技術手段實現的
這裏我給出了一個圖,其中用戶訪問商品時通過的路徑有不少,咱們主要是依賴前面的導購頁面(包括首頁、搜索頁面、商品詳情、購物車等)提早識別哪些商品的訪問量高,經過這些系統中的中間件來收集熱點數據,並記錄到日誌中。
咱們經過部署在每臺機器上的 Agent 把日誌彙總到聚合和分析集羣中,而後把符合必定規則的熱點數據,經過訂閱分發系統再推送到相應的系統中。你能夠是把熱點數據填充到 Cache 中,或者直接推送到應用服務器的內存中,還能夠對這些數據進行攔截,總之下游系統能夠訂閱這些數據,而後根據本身的需求決定如何處理這些數據。
打造熱點發現系統時,我根據以往經驗總結了幾點注意事項。
處理熱點數據一般有幾種思路:一是優化,二是限制,三是隔離。
優化熱點數據最有效的辦法就是緩存熱點數據,若是熱點數據作了動靜分離,那麼能夠長期緩存靜態數據。可是,緩存熱點數據更多的是「臨時」緩存,即無論是靜態數據仍是動態數據,都用一個隊列短暫地緩存數秒鐘,因爲隊列長度有限,能夠採用 LRU 淘汰算法替換。
限制更多的是一種保護機制,限制的辦法也有不少,例如對被訪問商品的 ID 作一致性 Hash,而後根據 Hash 作分桶,每一個分桶設置一個處理隊列,這樣能夠把熱點商品限制在一個請求隊列裏,防止因某些熱點商品佔用太多的服務器資源,而使其餘請求始終得不到服務器的處理資源。
高併發系統設計的第一個原則就是將這種熱點數據隔離出來,不要讓 1% 的請求影響到另外的 99%,隔離出來後也更方便對這 1% 的請求作針對性的優化。
具體到「秒殺」業務,咱們能夠在如下幾個層次實現隔離。
固然了,實現隔離有不少種辦法。好比,你能夠按照用戶來區分,給不一樣的用戶分配不一樣的 Cookie,在接入層,路由到不一樣的服務接口中;再好比,你還能夠在接入層針對 URL 中的不一樣 Path 來設置限流策略。服務層調用不一樣的服務接口,以及數據層經過給數據打標來區分等等這些措施,其目的都是把已經識別出來的熱點請求和普通的請求區分開
咱們知道服務器的處理資源是恆定的,你用或者不用它的處理能力都是同樣的,因此出現峯值的話,很容易致使忙處處理不過來,閒的時候卻又沒有什麼要處理。可是因爲要保證服務質量,咱們的不少處理資源只能按照忙的時候來預估,而這會致使資源的一個浪費
要對流量進行削峯,最容易想到的解決方案就是用消息隊列來緩衝瞬時流量,把同步的直接調用轉換成異步的間接推送,中間經過一個隊列在一端承接瞬時的流量洪峯,在另外一端平滑地將消息推送出去
若是流量峯值持續一段時間達到了消息隊列的處理上限,例如本機的消息積壓達到了存儲空間的上限,消息隊列一樣也會被壓垮,這樣雖然保護了下游的系統,可是和直接把請求丟棄也沒多大的區別
消息隊列,相似的排隊方式還有不少,例如:
能夠看到,這些方式都有一個共同特徵,就是把「一步的操做」變成「兩步的操做」,其中增長的一步操做用來起到緩衝的做用。
增長答題其實有不少目的
前面介紹的排隊和答題要麼是少發請求,要麼對發出來的請求進行緩衝,而針對秒殺場景還有一種方法,就是對請求進行分層過濾,從而過濾掉一些無效的請求。分層過濾其實就是採用「漏斗」式設計來處理請求的
假如請求分別通過 CDN、前臺讀系統(如商品詳情繫統)、後臺系統(如交易系統)和數據庫這幾層,那麼:
分層過濾的核心思想是:在不一樣的層次儘量地過濾掉無效請求,讓「漏斗」最末端的纔是有效請求。而要達到這種效果,咱們就必須對數據作分層的校驗。
分層校驗的基本原則是:
分層校驗的目的是:在讀系統中,儘可能減小因爲一致性校驗帶來的系統瓶頸,可是儘可能將不影響性能的檢查條件提早,如用戶是否具備秒殺資格、商品狀態是否正常、用戶答題是否正確、秒殺是否已經結束、是否非法請求、營銷等價物是否充足等;在寫數據系統中,主要對寫的數據(如「庫存」)作一致性檢查,最後在數據庫層保證數據的最終準確性(如「庫存」不能減爲負數)。
庫存場景很是典型 , 是高併發狀況下對數據進行讀寫操做的場景
先說一下場景
總結來講,減庫存操做通常有以下幾個方式:
「下單減庫存」在數據一致性上,主要就是保證大併發請求時庫存數據不能爲負數,也就是要保證數據庫中的庫存字段值不能爲負數,通常咱們有多種解決方案
在交易環節中,「庫存」是個關鍵數據,也是個熱點數據,由於交易的各個環節中均可能涉及對庫存的查詢。可是,我在前面介紹分層過濾時提到過,秒殺中並不須要對庫存有精確的一致性讀,把庫存數據放到緩存(Cache)中,能夠大大提高讀性能。
解決大併發讀問題,能夠採用 LocalCache(即在秒殺系統的單機上緩存商品相關的數據)和對數據進行分層過濾的方式,可是像減庫存這種大併發寫不管如何仍是避免不了,這也是秒殺場景下最爲核心的一個技術難題。
所以,這裏我想專門來講一下秒殺場景下減庫存的極致優化思路,包括如何在緩存中減庫存以及如何在數據庫中減庫存。
好比使用redis , 其實咱們可使用lua腳原本保證一致性
若是你的秒殺商品的減庫存邏輯很是單一,好比沒有複雜的 SKU 庫存和總庫存這種聯動關係,或者多組sku同時扣減的這種不涉及復瑣事務的場景,我以爲徹底能夠.
若是涉及到多組扣減 , 若是有比較複雜的減庫存邏輯,或者須要使用事務,仍是建議必須在數據庫中完成減庫存-> 或者緩存支持事務
因爲 MySQL 存儲數據的特色,同一數據在數據庫裏確定是一行存儲(MySQL),所以會有大量線程來競爭 InnoDB 行鎖,而併發度越高時等待線程會越多,TPS(Transaction Per Second,即每秒處理的消息數)會降低,響應時間(RT)會上升,數據庫的吞吐量就會嚴重受影響
這就可能引起一個問題,就是單個熱點商品會影響整個數據庫的性能, 致使 0.01% 的商品影響 99.99% 的商品的售賣,這是咱們不肯意看到的狀況。一個解決思路是遵循前面介紹的原則進行隔離,把熱點商品放到單獨的熱點庫中。可是這無疑會帶來維護上的麻煩,好比要作熱點數據的動態遷移以及單獨的數據庫等
而分離熱點商品到單獨的數據庫仍是沒有解決併發鎖的問題,咱們應該怎麼辦呢?要解決併發鎖的問題,有兩種辦法:
你可能有疑問了,排隊和鎖競爭不都是要等待嗎,有啥區別?若是熟悉 MySQL 的話,你會知道 InnoDB 內部的死鎖檢測,以及 MySQL Server 和 InnoDB 的切換會比較消耗性能,淘寶的 MySQL 核心團隊還作了不少其餘方面的優化,如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的補丁程序,配合在 SQL 裏面加提示(hint),在事務裏不須要等待應用層提交(COMMIT),而在數據執行完最後一條 SQL 後,直接根據 TARGET_AFFECT_ROW 的結果進行提交或回滾,能夠減小網絡等待時間(平均約 0.7ms)。據我所知,目前阿里 MySQL 團隊已經將包含這些補丁程序的 MySQL 開源。另外,數據更新問題除了前面介紹的熱點隔離和排隊處理以外,還有些場景(如對商品的 lastmodifytime 字段的)更新會很是頻繁,在某些場景下這些多條 SQL 是能夠合併的,必定時間內只要執行最後一條 SQL 就好了,以便減小對數據庫的更新操做。
高併發系統爲了保證系統的高可用,咱們必須設計一個 Plan B 方案來兜底
說到系統的高可用建設,它實際上是一個系統工程,須要考慮到系統建設的各個階段,也就是說它其實貫穿了系統建設的整個生命週期,以下圖所示:
具體來講,系統的高可用建設涉及架構階段、編碼階段、測試階段、發佈階段、運行階段,以及故障發生時。接下來,咱們分別看一下。
爲何系統的高可用建設要放到整個生命週期中全面考慮?由於咱們在每一個環節中均可能犯錯,而有些環節犯的錯,你在後面是沒法彌補的。例如在架構階段,你沒有消除單點問題,那麼系統上線後,遇到突發流量把單點給掛了,你就只能乾瞪眼,有時候想加機器都加不進去。因此高可用建設是一個系統工程,必須在每一個環節都作好。
那麼針對秒殺系統,咱們重點介紹在遇到大流量時,應該從哪些方面來保障系統的穩定運行,因此更多的是看如何針對運行階段進行處理,這就引出了接下來的內容:降級、限流和拒絕服務。
所謂「降級」,就是當系統的容量達到必定程度時,限制或者關閉系統的某些非核心功能,從而把有限的資源保留給更核心的業務。它是一個有目的、有計劃的執行過程,因此對降級咱們通常須要有一套預案來配合執行。若是咱們把它系統化,就能夠經過預案系統和開關係統來實現降級。
降級方案能夠這樣設計:當秒殺流量達到 5w/s 時,把成交記錄的獲取從展現 20 條降級到只展現 5 條。「從 20 改到 5」這個操做由一個開關來實現,也就是設置一個可以從開關係統動態獲取的系統參數。
這裏,我給出開關係統的示意圖。它分爲兩部分,一部分是開關控制檯,它保存了開關的具體配置信息,以及具體執行開關所對應的機器列表;另外一部分是執行下發開關數據的 Agent,主要任務就是保證開關被正確執行,即便系統重啓後也會生效。
執行降級無疑是在系統性能和用戶體驗之間選擇了前者,降級後確定會影響一部分用戶的體驗,例如在雙 11 零點時,若是優惠券系統扛不住,可能會臨時降級商品詳情的優惠信息展現,把有限的系統資源用在保障交易系統正確展現優惠信息上,即保障用戶真正下單時的價格是正確的。因此降級的核心目標是犧牲次要的功能和用戶體驗來保證核心業務流程的穩定,是一個不得已而爲之的舉措。
若是說降級是犧牲了一部分次要的功能和用戶的體驗效果,那麼限流就是更極端的一種保護措施了。限流就是當系統容量達到瓶頸時,咱們須要經過限制一部分流量來保護系統,並作到既能夠人工執行開關,也支持自動化保護的措施。
這裏,我一樣給出了限流系統的示意圖。整體來講,限流既能夠是在客戶端限流,也能夠是在服務端限流。此外,限流的實現方式既要支持 URL 以及方法級別的限流,也要支持基於 QPS 和線程的限流。
首先,我之內部的系統調用爲例,來分別說下客戶端限流和服務端限流的優缺點。
在限流的實現手段上來說,基於 QPS 和線程數的限流應用最多,最大 QPS 很容易經過壓測提早獲取,例如咱們的系統最高支持 1w QPS 時,能夠設置 8000 來進行限流保護。線程數限流在客戶端比較有效,例如在遠程調用時咱們設置鏈接池的線程數,超出這個併發線程請求,就將線程進行排隊或者直接超時丟棄。
限流無疑會影響用戶的正常請求,因此必然會致使一部分用戶請求失敗,所以在系統處理這種異常時必定要設置超時時間,防止因被限流的請求不能 fast fail(快速失敗)而拖垮系統。
若是限流還不能解決問題,最後一招就是直接拒絕服務了。
當系統負載達到必定閾值時,例如 CPU 使用率達到 90% 或者系統 load 值達到 2*CPU 核數時,系統直接拒絕全部請求,這種方式是最暴力但也最有效的系統保護方式。例如秒殺系統,咱們在以下幾個環節設計過載保護:
在最前端的 Nginx 上設置過載保護,當機器負載達到某個值時直接拒絕
HTTP 請求並返回 503 錯誤碼,在 Java 層一樣也能夠設計過載保護。
拒絕服務能夠說是一種不得已的兜底方案,用以防止最壞狀況發生,防止因把服務器壓跨而長時間完全沒法提供服務。像這種系統過載保護雖然在過載時沒法提供服務,可是系統仍然能夠運做,當負載降低時又很容易恢復,因此每一個系統和每一個環節都應該設置這個兜底方案,對系統作最壞狀況下的保護。
咱們討論的主要是系統服務端性能,通常用 QPS(Query Per Second,每秒請求數)來衡量,還有一個影響和 QPS 也息息相關,那就是響應時間(Response Time,RT),它能夠理解爲服務器處理響應的耗時
正常狀況下響應時間(RT)越短,一秒鐘處理的請求數(QPS)天然也就會越多,這在單線程處理的狀況下看起來是線性的關係,即咱們只要把每一個請求的響應時間降到最低,那麼性能就會最高。
可是你可能想到響應時間總有一個極限,不可能無限降低,因此又出現了另一個維度,即經過多線程,來處理請求。這樣理論上就變成了「總 QPS =(1000ms / 響應時間)× 線程數量」,這樣性能就和兩個因素相關了,一個是一次響應的服務端耗時,一個是處理請求的線程數。
對於大部分的 Web 系統而言,響應時間通常都是由 CPU 執行時間和線程等待時間(好比 RPC、IO 等待、Sleep、Wait 等)組成,即服務器在處理一個請求時,一部分是 CPU 自己在作運算,還有一部分是在各類等待。
理解了服務器處理請求的邏輯,估計你會說爲何咱們不去減小這種等待時間。很遺憾,根據咱們實際的測試發現,減小線程等待時間對提高性能的影響沒有咱們想象得那麼大,它並非線性的提高關係,這點在不少代理服務器(Proxy)上能夠作驗證。
若是代理服務器自己沒有 CPU 消耗,咱們在每次給代理服務器代理的請求加個延時,即增長響應時間,可是這對代理服務器自己的吞吐量並無多大的影響,由於代理服務器自己的資源並無被消耗,能夠經過增長代理服務器的處理線程數,來彌補響應時間對代理服務器的 QPS 的影響。
其實,真正對性能有影響的是 CPU 的執行時間。這也很好理解,由於 CPU 的執行真正消耗了服務器的資源。通過實際的測試,若是減小 CPU 一半的執行時間,就能夠增長一倍的 QPS。
也就是說,咱們應該致力於減小 CPU 的執行時間。
單看「總 QPS」的計算公式,你會以爲線程數越多 QPS 也就會越高,但這會一直正確嗎?顯然不是,線程數不是越多越好,由於線程自己也消耗資源,也受到其餘因素的制約。例如,線程越多系統的線程切換成本就會越高,並且每一個線程也都會耗費必定內存。
那麼,設置什麼樣的線程數最合理呢?其實不少多線程的場景都有一個默認配置,即「線程數 = 2 * CPU 核數 + 1」。除去這個配置,還有一個根據最佳實踐得出來的公式:
線程數 = [(線程等待時間 + 線程 CPU 時間) / 線程 CPU 時間] × CPU 數量 => 這個公式的核心思想就行將等待的時間讓給其餘線程去處理
固然,最好的辦法是經過性能測試來發現最佳的線程數。
對 Java 系統來講,能夠優化的地方不少,這裏我重點說一下比較有效的幾種手段,供你參考,它們是:減小編碼、減小序列化。接下來,咱們分別來看一下。
Java 的編碼運行比較慢,這是 Java 的一大硬傷。在不少場景下,只要涉及字符串的操做(如輸入輸出操做、I/O 操做)都比較耗 CPU 資源,無論它是磁盤 I/O 仍是網絡 I/O,由於都須要將字符轉換成字節,而這個轉換必須編碼。
每一個字符的編碼都須要查表,而這種查表的操做很是耗資源,因此減小字符到字節或者相反的轉換、減小字符編碼會很是有成效。減小編碼就能夠大大提高性能。
那麼如何才能減小編碼呢?例如,網頁輸出是能夠直接進行流輸出的,即用 resp.getOutputStream() 函數寫數據,把一些靜態的數據提早轉化成字節,等到真正往外寫的時候再直接用 OutputStream() 函數寫,就能夠減小靜態數據的編碼轉換。好比 把靜態的字符串提早編碼成字節並緩存,而後直接輸出字節內容到頁面,從而大大減小編碼的性能消耗的,網頁輸出的性能比沒有提早進行字符到字節轉換時提高了 30% 左右。
序列化也是 Java 性能的一大天敵,減小 Java 中的序列化操做也能大大提高性能。又由於序列化每每是和編碼同時發生的,因此減小序列化也就減小了編碼。
序列化大部分是在 RPC 中發生的,所以避免或者減小 RPC 就能夠減小序列化,固然當前的序列化協議也已經作了不少優化來提高性能。有一種新的方案,就是能夠將多個關聯性比較強的應用進行「合併部署」,而減小不一樣應用之間的 RPC 也能夠減小序列化的消耗。
所謂「合併部署」,就是把兩個本來在不一樣機器上的不一樣應用合併部署到一臺機器上,固然不只僅是部署在一臺機器上,還要在同一個 Tomcat 容器中,且不能走本機的 Socket,這樣才能避免序列化的產生。