高併發架構設計都有哪些關鍵點

高併發架構設計都有哪些關鍵點?

秒殺其實主要解決兩個問題,一個是併發讀,一個是併發寫前端

其實,秒殺的總體架構能夠歸納爲「穩、準、快」幾個關鍵字java

而後就是「準」,就是秒殺 10 臺 iPhone,那就只能成交 10 臺,多一臺少一臺都不行。一旦庫存不對,那平臺就要承擔損失,因此「準」就是要求保證數據的一致性。redis

最後再看「快」,「快」其實很好理解,它就是說系統的性能要足夠高,不然你怎麼支撐這麼大的流量呢?不光是服務端要作極致的性能優化,並且在整個請求鏈路上都要作協同的優化,每一個地方快一點,整個系統就完美了。算法

因此從技術角度上看「穩、準、快」,就對應了咱們架構上的高可用、一致性和高性能的要求,咱們的專欄也將主要圍繞這幾個方面來展開,具體以下。數據庫

  • 高性能。 秒殺涉及大量的併發讀和併發寫,所以支持高併發訪問這點很是關鍵。本專欄將從設計數據的動靜分離方案、熱點的發現與隔離、請求的削峯與分層過濾、服務端的極致優化這 4 個方面重點介紹。
  • 一致性。 秒殺中商品減庫存的實現方式一樣關鍵。可想而知,有限數量的商品在同一時刻被不少倍的請求同時來減庫存,減庫存又分爲「拍下減庫存」「付款減庫存」以及預扣等幾種,在大併發更新的過程當中都要保證數據的準確性,其難度可想而知。所以,我將用一篇文章來專門講解如何設計秒殺減庫存方案。
  • 高可用。 雖然我介紹了不少極致的優化思路,但現實中總不免出現一些咱們考慮不到的狀況,因此要保證系統的高可用和正確性,咱們還要設計一個 PlanB 來兜底,以便在最壞狀況發生時仍然可以從容應對。專欄的最後,我將帶你思考能夠從哪些環節來設計兜底方案。

設計高併發系統時應該注意的5個架構原則

高併發系統本質上就是一個知足大併發、高性能和高可用的分佈式系統

設計原則

數據操做要儘可能少

  1. 請求的數據能少就少。請求的數據包括上傳給系統的數據和系統返回給用戶的數據
  2. 返回的數據能少就少, 減小後端序列化的時間
  3. 數據庫操做能少就少 , 這個就不說了

請求數要儘可能少

這個就很直觀了 , 請求數少併發量就少後端

調用鏈路要儘可能短

高併發系統 1. 要下降系統依賴 , 防止由於依賴形成的各類問題 , 提升可用性 2. 下降流量入侵 , 大流量儘可能隔絕在外面瀏覽器

不要有單點

系統中的單點能夠說是系統架構上的一個大忌,由於單點意味着沒有備份,風險不可控 , 其次流量不能分發像redis這種會有熱點數據問題緩存

一些基本案例

其實構建一個高併發系統並無那麼複雜 , 有一下的幾個方法能夠扛住比較高的併發性能優化

  1. 把高併發系統獨立出來單獨打造一個系統,這樣能夠有針對性地作優化,例如這個獨立出來的系統就減小了店鋪裝修的功能,減小了頁面的複雜度
  2. 在系統部署上也獨立作一個機器集羣,這樣秒殺的大流量就不會影響到正常的商品購買集羣的機器負載
  3. 將熱點數據(如庫存數據)單獨放到一個緩存系統中,以提升「讀性能」
  4. 增長秒殺答題,防止有秒殺器搶單
  5. 一些數據放入cdn中進行緩存
  6. 使用本地緩存部分數據

對電商來講系統差很少是這種樣子 :服務器

高併發系統場景優化1 - 動靜分離

所謂動靜分離 , 就是將一些不常變化 , 能夠靜態化 , 無狀態 , 不須要邏輯處理的一些字段放在一個專門的系統或者地方 , 獲取的時候不須要走後端系統的方法

動靜分離原則

1. 把靜態數據緩存到離用戶最近的地方

常見的就三種 , 用戶瀏覽器裏、CDN 上 或者 在服務端的 Cache 中

2. 緩存http連接信息減小解析過程

Web 代理服務器根據請求URL查找緩存,直接取出對應的 HTTP 響應頭和響應體而後直接返回,這個響應過程簡單得連 HTTP 協議都不用從新組裝,甚至連 HTTP 請求頭也不須要解析

如何動靜分離改造

靜態數據處理方法

  1. URL 惟一化 : 爲啥要 URL 惟一呢?前面說了咱們是要緩存整個 HTTP 鏈接,那麼以什麼做爲 Key 呢?就以 URL 做爲緩存的 Key,例如以 id=xxx 這個格式進行區分
  2. 分離瀏覽者相關的因素。瀏覽者相關的因素包括是否已登陸,以及登陸身份等,這些相關因素咱們能夠單獨拆分出來,經過動態請求來獲取
  3. 分離時間因素。服務端輸出的時間也經過動態請求獲取。
  4. 異步化地域因素。詳情頁面上與地域相關的因素作成異步方式獲取,固然你也能夠經過動態請求方式獲取,只是這裏經過異步獲取更合適。
  5. 去掉 Cookie。服務端輸出的頁面包含的 Cookie 能夠經過代碼軟件來刪除,如 Web 服務器 Varnish 能夠經過 unset req.http.cookie 命令去掉 Cookie。注意,這裏說的去掉 Cookie 並非用戶端收到的頁面就不含 Cookie 了,而是說,在緩存的靜態數據中不含有 Cookie

動態數據處理方法

這個沒有什麼好辦法 , 動態數據必定會將流量打到後端 , 因此儘量的減小這部分 , 若是不行就加機器

動靜分離的幾種架構方案

有 3 種方案可選:

  1. 單機本地cache層
  2. 統一 Cache 層
  3. 上 CDN。

1. 單機本地cache層

就是使用內存緩存好比java的ehcache等

優勢 缺點
無網絡開銷 佔用內存大
使用簡單 同步機制須要使用其餘方法保證

統一 Cache 層

典型的就是redis集羣

優勢 缺點
StartFragment單獨一個 Cache 層,能夠減小多個應用接入時使用 Cache 的成本。這樣接入的應用只要維護本身的 Java 系統就好,不須要單獨維護 Cache,而只關心如何使用便可 EndFragment StartFragmentCache 層內部交換網絡成爲瓶頸 EndFragment
StartFragment統一 Cache 的方案更易於維護,如後面增強監控、配置的自動化,只須要一套解決方案就行,統一塊兒來維護升級也比較方便。 EndFragment StartFragment緩存服務器的網卡也會是瓶頸; EndFragment
StartFragment能夠共享內存,最大化利用內存,不一樣系統之間的內存能夠動態切換,從而可以有效應對各類攻擊。 EndFragment StartFragment機器少風險較大,掛掉一臺就會影響很大一部分緩存數據。 EndFragment
要解決上面這些問題,能夠再對 Cache 作 Hash 分組,即一組 Cache 緩存的內容相同,這樣可以避免熱點數據過分集中致使新的瓶頸產生。 好比redis 熱點數據分組

上 CDN

在將整個系統作動靜分離後,咱們天然會想到更進一步的方案,就是將 Cache 進一步前移到 CDN 上,由於 CDN 離用戶最近,效果會更好

有如下幾個問題須要解決

  1. 失效問題。前面咱們也有提到過緩存時效的問題,不知道你有沒有理解,我再來解釋一下。談到靜態數據時,我說過一個關鍵詞叫「相對不變」,它的言外之意是「可能會變化」。好比一篇文章,如今不變,但若是你發現個錯別字,是否是就會變化了?若是你的緩存時效很長,那用戶端在很長一段時間內看到的都是錯的。因此,這個方案中也是,咱們須要保證 CDN 能夠在秒級時間內,讓分佈在全國各地的 Cache 同時失效,這對 CDN 的失效系統要求很高
  2. 命中率問題。Cache 最重要的一個衡量指標就是「高命中率」,否則 Cache 的存在就失去了意義。一樣,若是將數據所有放到全國的 CDN 上,必然致使 Cache 分散,而 Cache 分散又會致使訪問請求命中同一個 Cache 的可能性下降,那麼命中率就成爲一個問題。
  3. 發佈更新問題。若是一個業務系統每週都有平常業務須要發佈,那麼發佈系統必須足夠簡潔高效,並且你還要考慮有問題時快速回滾和排查問題的簡便性。

由於上面的這些問題 , 因此cdn的部署方法通常都是分網絡分區域的中心化部署

高併發系統場景優化2 - 處理熱點

要關注熱點

首先,熱點請求會大量佔用服務器處理資源,雖然這個熱點可能只佔請求總量的億分之一,然而卻可能搶佔 90% 的服務器資源,若是這個熱點請求仍是沒有價值的無效請求,那麼對系統資源來講徹底是浪費。

其次,即便這些熱點是有效的請求,咱們也要識別出來作針對性的優化,從而用更低的代價來支撐這些熱點請求

熱點操做和熱點數據

所謂「熱點操做」,例如大量的刷新頁面、大量的添加購物車、雙十一零點大量的下單等都屬於此類操做。對系統來講,這些操做能夠抽象爲「讀請求」和「寫請求」,這兩種熱點請求的處理方式截然不同,讀請求的優化空間要大一些,而寫請求的瓶頸通常都在存儲層,優化的思路就是根據 CAP 理論作平衡

熱點數據」比較好理解,那就是用戶的熱點請求對應的數據。而熱點數據又分爲「靜態熱點數據」和「動態熱點數據」

靜態熱點數據和動態熱點數據

所謂「靜態熱點數據」,就是可以提早預測的熱點數據。例如,咱們能夠經過賣家報名的方式提早篩選出來,經過報名系統對這些熱點商品進行打標。另外,咱們還能夠經過大數據分析來提早發現熱點商品,好比咱們分析歷史成交記錄、用戶的購物車記錄,來發現哪些商品可能更熱門、更好賣,這些都是能夠提早分析出來的熱點

所謂「動態熱點數據」,就是不能被提早預測到的,系統在運行過程當中臨時產生的熱點。例如,賣家在抖音上作了廣告,而後商品一下就火了,致使它在短期內被大量購買

咱們如何處理熱點數據

發現熱點數據

熱點數據靜態發現

靜態熱點數據能夠經過商業手段,例如強制讓賣家經過報名參加的方式提早把熱點商品篩選出來,實現方式是經過一個運營系統,把參加活動的商品數據進行打標,而後經過一個後臺系統對這些熱點商品進行預處理,如提早進行緩存 . 或者使用技術手段提早預測,例如對買家天天訪問的商品進行大數據計算,而後統計出 TOP N 的商品,咱們能夠認爲這些 TOP N 的商品就是熱點商品。

熱點數據動態發現

主要處理動態熱點數據的 , 都是使用技術手段實現的

  1. 構建一個異步的系統,它能夠收集交易鏈路上各個環節中的中間件產品的熱點 Key,如 Nginx、緩存、RPC 服務框架等這些中間件(一些中間件產品自己已經有熱點統計模塊)。
  2. 創建一個熱點上報和能夠按照需求訂閱的熱點服務的下發規範,主要目的是經過交易鏈路上各個系統(包括詳情、購物車、交易、優惠、庫存、物流等)訪問的時間差,把上游已經發現的熱點透傳給下游系統,提早作好保護。好比,對於大促高峯期,詳情繫統是最先知道的,在統一接入層上 Nginx 模塊統計的熱點 URL。
  3. 將上游系統收集的熱點數據發送到熱點服務檯,而後下游系統(如交易系統)就會知道哪些商品會被頻繁調用,而後作熱點保護。

這裏我給出了一個圖,其中用戶訪問商品時通過的路徑有不少,咱們主要是依賴前面的導購頁面(包括首頁、搜索頁面、商品詳情、購物車等)提早識別哪些商品的訪問量高,經過這些系統中的中間件來收集熱點數據,並記錄到日誌中。

咱們經過部署在每臺機器上的 Agent 把日誌彙總到聚合和分析集羣中,而後把符合必定規則的熱點數據,經過訂閱分發系統再推送到相應的系統中。你能夠是把熱點數據填充到 Cache 中,或者直接推送到應用服務器的內存中,還能夠對這些數據進行攔截,總之下游系統能夠訂閱這些數據,而後根據本身的需求決定如何處理這些數據。

打造熱點發現系統時,我根據以往經驗總結了幾點注意事項。

  1. 這個熱點服務後臺抓取熱點數據日誌最好採用異步方式,由於「異步」一方面便於保證通用性,另外一方面又不影響業務系統和中間件產品的主流程。
  2. 熱點服務發現和中間件自身的熱點保護模塊並存,每一箇中間件和應用還須要保護本身。熱點服務檯提供熱點數據的收集和訂閱服務,便於把各個系統的熱點數據透明出來。
  3. 熱點發現要作到接近實時(3s 內完成熱點數據的發現),由於只有作到接近實時,動態發現纔有意義,才能實時地對下游系統提供保護。

處理熱點數據

處理熱點數據一般有幾種思路:一是優化,二是限制,三是隔離。

優化

優化熱點數據最有效的辦法就是緩存熱點數據,若是熱點數據作了動靜分離,那麼能夠長期緩存靜態數據。可是,緩存熱點數據更多的是「臨時」緩存,即無論是靜態數據仍是動態數據,都用一個隊列短暫地緩存數秒鐘,因爲隊列長度有限,能夠採用 LRU 淘汰算法替換。

限制

限制更多的是一種保護機制,限制的辦法也有不少,例如對被訪問商品的 ID 作一致性 Hash,而後根據 Hash 作分桶,每一個分桶設置一個處理隊列,這樣能夠把熱點商品限制在一個請求隊列裏,防止因某些熱點商品佔用太多的服務器資源,而使其餘請求始終得不到服務器的處理資源。

隔離

高併發系統設計的第一個原則就是將這種熱點數據隔離出來,不要讓 1% 的請求影響到另外的 99%,隔離出來後也更方便對這 1% 的請求作針對性的優化。

具體到「秒殺」業務,咱們能夠在如下幾個層次實現隔離。
  1. 業務隔離。把秒殺作成一種營銷活動,賣家要參加秒殺這種營銷活動須要單獨報名,從技術上來講,賣家報名後對咱們來講就有了已知熱點,所以能夠提早作好預熱。
  2. 系統隔離。系統隔離更多的是運行時的隔離,能夠經過分組部署的方式和另外 99% 分開。秒殺能夠申請單獨的域名,目的也是讓請求落到不一樣的集羣中。
  3. 數據隔離。秒殺所調用的數據大部分都是熱點數據,好比會啓用單獨的 Cache 集羣或者 MySQL 數據庫來放熱點數據,目的也是不想 0.01% 的數據有機會影響 99.99% 數據。

固然了,實現隔離有不少種辦法。好比,你能夠按照用戶來區分,給不一樣的用戶分配不一樣的 Cookie,在接入層,路由到不一樣的服務接口中;再好比,你還能夠在接入層針對 URL 中的不一樣 Path 來設置限流策略。服務層調用不一樣的服務接口,以及數據層經過給數據打標來區分等等這些措施,其目的都是把已經識別出來的熱點請求和普通的請求區分開

高併發系統場景優化3 - 流量削峯

爲何要削峯

咱們知道服務器的處理資源是恆定的,你用或者不用它的處理能力都是同樣的,因此出現峯值的話,很容易致使忙處處理不過來,閒的時候卻又沒有什麼要處理。可是因爲要保證服務質量,咱們的不少處理資源只能按照忙的時候來預估,而這會致使資源的一個浪費

流量削峯的一些操做思路:排隊、答題、分層過濾

排隊

要對流量進行削峯,最容易想到的解決方案就是用消息隊列來緩衝瞬時流量,把同步的直接調用轉換成異步的間接推送,中間經過一個隊列在一端承接瞬時的流量洪峯,在另外一端平滑地將消息推送出去

若是流量峯值持續一段時間達到了消息隊列的處理上限,例如本機的消息積壓達到了存儲空間的上限,消息隊列一樣也會被壓垮,這樣雖然保護了下游的系統,可是和直接把請求丟棄也沒多大的區別

消息隊列,相似的排隊方式還有不少,例如:

  1. 利用線程池加鎖等待也是一種經常使用的排隊方式;
  2. 先進先出、先進後出等經常使用的內存排隊算法的實現方式;
  3. 把請求序列化到文件中,而後再順序地讀文件(例如基於 MySQL binlog 的同步機制)來恢復請求等方式。

能夠看到,這些方式都有一個共同特徵,就是把「一步的操做」變成「兩步的操做」,其中增長的一步操做用來起到緩衝的做用。

答題

增長答題其實有不少目的

  1. 第一個目的是防止部分買家使用秒殺器在參加秒殺時做弊 , 過濾無用請求
  2. 延緩請求,起到對請求流量進行削峯的做用,從而讓系統可以更好地支持瞬時的流量高峯

分層過濾

前面介紹的排隊和答題要麼是少發請求,要麼對發出來的請求進行緩衝,而針對秒殺場景還有一種方法,就是對請求進行分層過濾,從而過濾掉一些無效的請求。分層過濾其實就是採用「漏斗」式設計來處理請求的

假如請求分別通過 CDN、前臺讀系統(如商品詳情繫統)、後臺系統(如交易系統)和數據庫這幾層,那麼:

  1. 大部分數據和流量在用戶瀏覽器或者 CDN 上獲取,這一層能夠攔截大部分數據的讀取;
  2. 通過第二層(即前臺系統)時數據(包括強一致性的數據)儘可能得走 Cache,過濾一些無效的請求;
  3. 再到第三層後臺系統,主要作數據的二次檢驗,對系統作好保護和限流,這樣數據量和請求就進一步減小;
  4. 最後在數據層完成數據的強一致性校驗。

分層過濾的核心思想是:在不一樣的層次儘量地過濾掉無效請求,讓「漏斗」最末端的纔是有效請求。而要達到這種效果,咱們就必須對數據作分層的校驗。

分層校驗的基本原則是:

  1. 將動態請求的讀數據緩存(Cache)在 Web 端,過濾掉無效的數據讀;
  2. 對讀數據不作強一致性校驗,減小由於一致性校驗產生瓶頸的問題;
  3. 對寫數據進行基於時間的合理分片,過濾掉過時的失效請求;
  4. 對寫請求作限流保護,將超出系統承載能力的請求過濾掉;
  5. 對寫數據進行強一致性校驗,只保留最後有效的數據。

分層校驗的目的是:在讀系統中,儘可能減小因爲一致性校驗帶來的系統瓶頸,可是儘可能將不影響性能的檢查條件提早,如用戶是否具備秒殺資格、商品狀態是否正常、用戶答題是否正確、秒殺是否已經結束、是否非法請求、營銷等價物是否充足等;在寫數據系統中,主要對寫的數據(如「庫存」)作一致性檢查,最後在數據庫層保證數據的最終準確性(如「庫存」不能減爲負數)。

高併發系統場景優化4 - 類庫存扣減場景

庫存場景很是典型 , 是高併發狀況下對數據進行讀寫操做的場景

先說一下場景

總結來講,減庫存操做通常有以下幾個方式:

  1. 下單減庫存,即當買家下單後,在商品的總庫存中減去買家購買數量。下單減庫存是最簡單的減庫存方式,也是控制最精確的一種,下單時直接經過數據庫的事務機制控制商品庫存,這樣必定不會出現超賣的狀況。可是你要知道,有些人下完單可能並不會付款。
  2. 付款減庫存,即買家下單後,並不當即減庫存,而是等到有用戶付款後才真正減庫存,不然庫存一直保留給其餘買家。但由於付款時才減庫存,若是併發比較高,有可能出現買家下單後付不了款的狀況,由於可能商品已經被其餘人買走了。
  3. 預扣庫存,這種方式相對複雜一些,買家下單後,庫存爲其保留必定的時間(如 10 分鐘),超過這個時間,庫存將會自動釋放,釋放後其餘買家就能夠繼續購買。在買家付款前,系統會校驗該訂單的庫存是否還有保留:若是沒有保留,則再次嘗試預扣;若是庫存不足(也就是預扣失敗)則不容許繼續付款;若是預扣成功,則完成付款並實際地減去庫存。

「下單減庫存」在數據一致性上,主要就是保證大併發請求時庫存數據不能爲負數,也就是要保證數據庫中的庫存字段值不能爲負數,通常咱們有多種解決方案

  1. 一種是在應用程序中經過事務來判斷,即保證減後庫存不能爲負數,不然就回滾;
  2. 直接設置數據庫的字段數據爲無符號整數,這樣減後庫存字段值小於零時會直接執行 SQL 語句來報錯
  3. 再有一種就是使用 CASE WHEN 判斷語句,例如這樣的 SQL 語句:UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END

秒殺減庫存的極致優化

在交易環節中,「庫存」是個關鍵數據,也是個熱點數據,由於交易的各個環節中均可能涉及對庫存的查詢。可是,我在前面介紹分層過濾時提到過,秒殺中並不須要對庫存有精確的一致性讀,把庫存數據放到緩存(Cache)中,能夠大大提高讀性能。

解決大併發讀問題,能夠採用 LocalCache(即在秒殺系統的單機上緩存商品相關的數據)和對數據進行分層過濾的方式,可是像減庫存這種大併發寫不管如何仍是避免不了,這也是秒殺場景下最爲核心的一個技術難題。

所以,這裏我想專門來講一下秒殺場景下減庫存的極致優化思路,包括如何在緩存中減庫存以及如何在數據庫中減庫存。

好比使用redis , 其實咱們可使用lua腳原本保證一致性

可是使用redis 有必定的侷限性

若是你的秒殺商品的減庫存邏輯很是單一,好比沒有複雜的 SKU 庫存和總庫存這種聯動關係,或者多組sku同時扣減的這種不涉及復瑣事務的場景,我以爲徹底能夠.

若是涉及到多組扣減 , 若是有比較複雜的減庫存邏輯,或者須要使用事務,仍是建議必須在數據庫中完成減庫存-> 或者緩存支持事務

因爲 MySQL 存儲數據的特色,同一數據在數據庫裏確定是一行存儲(MySQL),所以會有大量線程來競爭 InnoDB 行鎖,而併發度越高時等待線程會越多,TPS(Transaction Per Second,即每秒處理的消息數)會降低,響應時間(RT)會上升,數據庫的吞吐量就會嚴重受影響

這就可能引起一個問題,就是單個熱點商品會影響整個數據庫的性能, 致使 0.01% 的商品影響 99.99% 的商品的售賣,這是咱們不肯意看到的狀況。一個解決思路是遵循前面介紹的原則進行隔離,把熱點商品放到單獨的熱點庫中。可是這無疑會帶來維護上的麻煩,好比要作熱點數據的動態遷移以及單獨的數據庫等

而分離熱點商品到單獨的數據庫仍是沒有解決併發鎖的問題,咱們應該怎麼辦呢?要解決併發鎖的問題,有兩種辦法:

  1. 應用層作排隊。按照商品維度設置隊列順序執行,這樣能減小同一臺機器對數據庫同一行記錄進行操做的併發度,同時也能控制單個商品佔用數據庫鏈接的數量,防止熱點商品佔用太多的數據庫鏈接。
  2. 數據庫層作排隊。應用層只能作到單機的排隊,可是應用機器數自己不少,這種排隊方式控制併發的能力仍然有限,因此若是能在數據庫層作全局排隊是最理想的。阿里的數據庫團隊開發了針對這種 MySQL 的 InnoDB 層上的補丁程序(patch),能夠在數據庫層上對單行記錄作到併發排隊。

你可能有疑問了,排隊和鎖競爭不都是要等待嗎,有啥區別?若是熟悉 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 就好了,以便減小對數據庫的更新操做。

高併發系統場景優化4 - 兜底方案

高併發系統爲了保證系統的高可用,咱們必須設計一個 Plan B 方案來兜底

高可用建設應該從哪裏着手

說到系統的高可用建設,它實際上是一個系統工程,須要考慮到系統建設的各個階段,也就是說它其實貫穿了系統建設的整個生命週期,以下圖所示:

具體來講,系統的高可用建設涉及架構階段、編碼階段、測試階段、發佈階段、運行階段,以及故障發生時。接下來,咱們分別看一下。

  1. 架構階段:架構階段主要考慮系統的可擴展性和容錯性,要避免系統出現單點問題。例如多機房單元化部署,即便某個城市的某個機房出現總體故障,仍然不會影響總體網站的運轉。
  2. 編碼階段:編碼最重要的是保證代碼的健壯性,例如涉及遠程調用問題時,要設置合理的超時退出機制,防止被其餘系統拖垮,也要對調用的返回結果集有預期,防止返回的結果超出程序處理範圍,最多見的作法就是對錯誤異常進行捕獲,對沒法預料的錯誤要有默認處理結果。
  3. 測試階段:測試主要是保證測試用例的覆蓋度,保證最壞狀況發生時,咱們也有相應的處理流程。
  4. 發佈階段:發佈時也有一些地方須要注意,由於發佈時最容易出現錯誤,所以要有緊急的回滾機制。
  5. 運行階段:運行時是系統的常態,系統大部分時間都會處於運行態,運行態最重要的是對系統的監控要準確及時,發現問題可以準確報警而且報警數據要準確詳細,以便於排查問題。
  6. 故障發生:故障發生時首先最重要的就是及時止損,例如因爲程序問題致使商品價格錯誤,那就要及時下架商品或者關閉購買連接,防止形成重大資產損失。而後就是要可以及時恢復服務,並定位緣由解決問題。

爲何系統的高可用建設要放到整個生命週期中全面考慮?由於咱們在每一個環節中均可能犯錯,而有些環節犯的錯,你在後面是沒法彌補的。例如在架構階段,你沒有消除單點問題,那麼系統上線後,遇到突發流量把單點給掛了,你就只能乾瞪眼,有時候想加機器都加不進去。因此高可用建設是一個系統工程,必須在每一個環節都作好。

那麼針對秒殺系統,咱們重點介紹在遇到大流量時,應該從哪些方面來保障系統的穩定運行,因此更多的是看如何針對運行階段進行處理,這就引出了接下來的內容:降級、限流和拒絕服務。

降級

所謂「降級」,就是當系統的容量達到必定程度時,限制或者關閉系統的某些非核心功能,從而把有限的資源保留給更核心的業務。它是一個有目的、有計劃的執行過程,因此對降級咱們通常須要有一套預案來配合執行。若是咱們把它系統化,就能夠經過預案系統和開關係統來實現降級。

降級方案能夠這樣設計:當秒殺流量達到 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 層一樣也能夠設計過載保護。

拒絕服務能夠說是一種不得已的兜底方案,用以防止最壞狀況發生,防止因把服務器壓跨而長時間完全沒法提供服務。像這種系統過載保護雖然在過載時沒法提供服務,可是系統仍然能夠運做,當負載降低時又很容易恢復,因此每一個系統和每一個環節都應該設置這個兜底方案,對系統作最壞狀況下的保護。

最後,以java系統爲例 , 如何提升系統性能

咱們討論的主要是系統服務端性能,通常用 QPS(Query Per Second,每秒請求數)來衡量,還有一個影響和 QPS 也息息相關,那就是響應時間(Response Time,RT),它能夠理解爲服務器處理響應的耗時
正常狀況下響應時間(RT)越短,一秒鐘處理的請求數(QPS)天然也就會越多,這在單線程處理的狀況下看起來是線性的關係,即咱們只要把每一個請求的響應時間降到最低,那麼性能就會最高。
可是你可能想到響應時間總有一個極限,不可能無限降低,因此又出現了另一個維度,即經過多線程,來處理請求。這樣理論上就變成了「總 QPS =(1000ms / 響應時間)× 線程數量」,這樣性能就和兩個因素相關了,一個是一次響應的服務端耗時,一個是處理請求的線程數。

響應時間和QPS的關係

對於大部分的 Web 系統而言,響應時間通常都是由 CPU 執行時間和線程等待時間(好比 RPC、IO 等待、Sleep、Wait 等)組成,即服務器在處理一個請求時,一部分是 CPU 自己在作運算,還有一部分是在各類等待。

理解了服務器處理請求的邏輯,估計你會說爲何咱們不去減小這種等待時間。很遺憾,根據咱們實際的測試發現,減小線程等待時間對提高性能的影響沒有咱們想象得那麼大,它並非線性的提高關係,這點在不少代理服務器(Proxy)上能夠作驗證。

若是代理服務器自己沒有 CPU 消耗,咱們在每次給代理服務器代理的請求加個延時,即增長響應時間,可是這對代理服務器自己的吞吐量並無多大的影響,由於代理服務器自己的資源並無被消耗,能夠經過增長代理服務器的處理線程數,來彌補響應時間對代理服務器的 QPS 的影響。

其實,真正對性能有影響的是 CPU 的執行時間。這也很好理解,由於 CPU 的執行真正消耗了服務器的資源。通過實際的測試,若是減小 CPU 一半的執行時間,就能夠增長一倍的 QPS。

也就是說,咱們應該致力於減小 CPU 的執行時間。

線程數對 QPS 的影響

單看「總 QPS」的計算公式,你會以爲線程數越多 QPS 也就會越高,但這會一直正確嗎?顯然不是,線程數不是越多越好,由於線程自己也消耗資源,也受到其餘因素的制約。例如,線程越多系統的線程切換成本就會越高,並且每一個線程也都會耗費必定內存。

那麼,設置什麼樣的線程數最合理呢?其實不少多線程的場景都有一個默認配置,即「線程數 = 2 * CPU 核數 + 1」。除去這個配置,還有一個根據最佳實踐得出來的公式:

線程數 = [(線程等待時間 + 線程 CPU 時間) / 線程 CPU 時間] × CPU 數量 => 這個公式的核心思想就行將等待的時間讓給其餘線程去處理

固然,最好的辦法是經過性能測試來發現最佳的線程數。

如何優化系統

對 Java 系統來講,能夠優化的地方不少,這裏我重點說一下比較有效的幾種手段,供你參考,它們是:減小編碼、減小序列化。接下來,咱們分別來看一下。

1. 減小編碼

Java 的編碼運行比較慢,這是 Java 的一大硬傷。在不少場景下,只要涉及字符串的操做(如輸入輸出操做、I/O 操做)都比較耗 CPU 資源,無論它是磁盤 I/O 仍是網絡 I/O,由於都須要將字符轉換成字節,而這個轉換必須編碼。

每一個字符的編碼都須要查表,而這種查表的操做很是耗資源,因此減小字符到字節或者相反的轉換、減小字符編碼會很是有成效。減小編碼就能夠大大提高性能。

那麼如何才能減小編碼呢?例如,網頁輸出是能夠直接進行流輸出的,即用 resp.getOutputStream() 函數寫數據,把一些靜態的數據提早轉化成字節,等到真正往外寫的時候再直接用 OutputStream() 函數寫,就能夠減小靜態數據的編碼轉換。好比 把靜態的字符串提早編碼成字節並緩存,而後直接輸出字節內容到頁面,從而大大減小編碼的性能消耗的,網頁輸出的性能比沒有提早進行字符到字節轉換時提高了 30% 左右。

2. 減小序列化

序列化也是 Java 性能的一大天敵,減小 Java 中的序列化操做也能大大提高性能。又由於序列化每每是和編碼同時發生的,因此減小序列化也就減小了編碼。

序列化大部分是在 RPC 中發生的,所以避免或者減小 RPC 就能夠減小序列化,固然當前的序列化協議也已經作了不少優化來提高性能。有一種新的方案,就是能夠將多個關聯性比較強的應用進行「合併部署」,而減小不一樣應用之間的 RPC 也能夠減小序列化的消耗。

所謂「合併部署」,就是把兩個本來在不一樣機器上的不一樣應用合併部署到一臺機器上,固然不只僅是部署在一臺機器上,還要在同一個 Tomcat 容器中,且不能走本機的 Socket,這樣才能避免序列化的產生。

相關文章
相關標籤/搜索