面試官:面對千萬級、億級流量怎麼處理?

這個《我想進大廠》系列的最後一篇,終結篇。可能有點標題黨了,可是我想要表達的意思和目的是一致的。java

這是一道很常見的面試題,可是大多數人並不知道怎麼回答,這種問題其實能夠有不少形式的提問方式,你必定見過並且感受無從下手:mysql

面對業務急劇增加你怎麼處理?面試

業務量增加10倍、100倍怎麼處理?redis

大家系統怎麼支撐高併發的?算法

怎麼設計一個高併發系統?sql

高併發系統都有什麼特色?數據庫

… …數組

諸如此類,問法不少,可是面試這種類型的問題,看着很難無處下手,可是咱們能夠有一個常規的思路去回答,就是圍繞支撐高併發的業務場景怎麼設計系統才合理?若是你能想到這一點,那接下來咱們就能夠圍繞硬件和軟件層面怎麼支撐高併發這個話題去闡述了。本質上,這個問題就是綜合考驗你對各個細節是否知道怎麼處理,是否有經驗處理過而已。緩存

面對超高的併發,首先硬件層面機器要能扛得住,其次架構設計作好微服務的拆分,代碼層面各類緩存、削峯、解耦等等問題要處理好,數據庫層面作好讀寫分離、分庫分表,穩定性方面要保證有監控,熔斷限流降級該有的必需要有,發生問題能及時發現處理。這樣從整個系統設計方面就會有一個初步的概念。安全

微服務架構演化

在互聯網早期的時候,單體架構就足以支撐起平常的業務需求,你們的全部業務服務都在一個項目裏,部署在一臺物理機器上。全部的業務包括你的交易系統、會員信息、庫存、商品等等都夾雜在一塊兒,當流量一旦起來以後,單體架構的問題就暴露出來了,機器掛了全部的業務所有沒法使用了。

因而,集羣架構的架構開始出現,單機沒法抗住的壓力,最簡單的辦法就是水平拓展橫向擴容了,這樣,經過負載均衡把壓力流量分攤到不一樣的機器上,暫時是解決了單點致使服務不可用的問題。

可是隨着業務的發展,在一個項目裏維護全部的業務場景使開發和代碼維護變得愈來愈困難,一個簡單的需求改動都須要發佈整個服務,代碼的合併衝突也會變得愈來愈頻繁,同時線上故障出現的可能性越大。微服務的架構模式就誕生了。

把每一個獨立的業務拆分開獨立部署,開發和維護的成本下降,集羣能承受的壓力也提升了,不再會出現一個小小的改動點須要牽一髮而動全身了。

以上的點從高併發的角度而言,彷佛均可以歸類爲經過服務拆分和集羣物理機器的擴展提升了總體的系統抗壓能力,那麼,隨之拆分而帶來的問題也就是高併發系統須要解決的問題。

RPC

微服務化的拆分帶來的好處和便利性是顯而易見的,可是與此同時各個微服務之間的通訊就須要考慮了。傳統HTTP的通訊方式對性能是極大的浪費,這時候就須要引入諸如Dubbo類的RPC框架,基於TCP長鏈接的方式提升整個集羣通訊的效率。

咱們假設原來來自客戶端的QPS是9000的話,那麼經過負載均衡策略分散到每臺機器就是3000,而HTTP改成RPC以後接口的耗時縮短了,單機和總體的QPS就提高了。而RPC框架自己通常都自帶負載均衡、熔斷降級的機制,能夠更好的維護整個系統的高可用性。

那麼說完RPC,做爲基本上國內廣泛的選擇Dubbo的一些基本原理就是接下來的問題。

Dubbo工做原理

  1. 服務啓動的時候,provider和consumer根據配置信息,鏈接到註冊中心register,分別向註冊中心註冊和訂閱服務

  2. register根據服務訂閱關係,返回provider信息到consumer,同時consumer會把provider信息緩存到本地。若是信息有變動,consumer會收到來自register的推送

  3. consumer生成代理對象,同時根據負載均衡策略,選擇一臺provider,同時定時向monitor記錄接口的調用次數和時間信息

  4. 拿到代理對象以後,consumer經過代理對象發起接口調用

  5. provider收到請求後對數據進行反序列化,而後經過代理調用具體的接口實現

Dubbo負載均衡策略

  1. 加權隨機:假設咱們有一組服務器 servers = [A, B, C],他們對應的權重爲 weights = [5, 3, 2],權重總和爲10。如今把這些權重值平鋪在一維座標值上,[0, 5) 區間屬於服務器 A,[5, 8) 區間屬於服務器 B,[8, 10) 區間屬於服務器 C。接下來經過隨機數生成器生成一個範圍在 [0, 10) 之間的隨機數,而後計算這個隨機數會落到哪一個區間上就能夠了。

  2. 最小活躍數:每一個服務提供者對應一個活躍數 active,初始狀況下,全部服務提供者活躍數均爲0。每收到一個請求,活躍數加1,完成請求後則將活躍數減1。在服務運行一段時間後,性能好的服務提供者處理請求的速度更快,所以活躍數降低的也越快,此時這樣的服務提供者可以優先獲取到新的服務請求。

  3. 一致性hash:經過hash算法,把provider的invoke和隨機節點生成hash,並將這個 hash 投射到 [0, 2^32 - 1] 的圓環上,查詢的時候根據key進行md5而後進行hash,獲得第一個節點的值大於等於當前hash的invoker。

圖片來自dubbo官方

  1. 加權輪詢:好比服務器 A、B、C 權重比爲 5:2:1,那麼在8次請求中,服務器 A 將收到其中的5次請求,服務器 B 會收到其中的2次請求,服務器 C 則收到其中的1次請求。

集羣容錯

  1. Failover Cluster失敗自動切換:dubbo的默認容錯方案,當調用失敗時自動切換到其餘可用的節點,具體的重試次數和間隔時間可用經過引用服務的時候配置,默認重試次數爲1也就是隻調用一次。

  2. Failback Cluster快速失敗:在調用失敗,記錄日誌和調用信息,而後返回空結果給consumer,而且經過定時任務每隔5秒對失敗的調用進行重試

  3. Failfast Cluster失敗自動恢復:只會調用一次,失敗後馬上拋出異常

  4. Failsafe Cluster失敗安全:調用出現異常,記錄日誌不拋出,返回空結果

  5. Forking Cluster並行調用多個服務提供者:經過線程池建立多個線程,併發調用多個provider,結果保存到阻塞隊列,只要有一個provider成功返回告終果,就會馬上返回結果

  6. Broadcast Cluster廣播模式:逐個調用每一個provider,若是其中一臺報錯,在循環調用結束後,拋出異常。

消息隊列

對於MQ的做用你們都應該很瞭解了,削峯填谷、解耦。依賴消息隊列,同步轉異步的方式,能夠下降微服務之間的耦合。

對於一些不須要同步執行的接口,能夠經過引入消息隊列的方式異步執行以提升接口響應時間。在交易完成以後須要扣庫存,而後可能須要給會員發放積分,本質上,發積分的動做應該屬於履約服務,對實時性的要求也不高,咱們只要保證最終一致性也就是能履約成功就好了。對於這種同類性質的請求就能夠走MQ異步,也就提升了系統抗壓能力了。

對於消息隊列而言,怎麼在使用的時候保證消息的可靠性、不丟失?

消息可靠性

消息丟失可能發生在生產者發送消息、MQ自己丟失消息、消費者丟失消息3個方面。

生產者丟失

生產者丟失消息的可能點在於程序發送失敗拋異常了沒有重試處理,或者發送的過程成功可是過程當中網絡閃斷MQ沒收到,消息就丟失了。

因爲同步發送的通常不會出現這樣使用方式,因此咱們就不考慮同步發送的問題,咱們基於異步發送的場景來講。

異步發送分爲兩個方式:異步有回調和異步無回調,無回調的方式,生產者發送完後無論結果可能就會形成消息丟失,而經過異步發送+回調通知+本地消息表的形式咱們就能夠作出一個解決方案。如下單的場景舉例。

  1. 下單後先保存本地數據和MQ消息表,這時候消息的狀態是發送中,若是本地事務失敗,那麼下單失敗,事務回滾。
  2. 下單成功,直接返回客戶端成功,異步發送MQ消息
  3. MQ回調通知消息發送結果,對應更新數據庫MQ發送狀態
  4. JOB輪詢超過必定時間(時間根據業務配置)還未發送成功的消息去重試
  5. 在監控平臺配置或者JOB程序處理超過必定次數一直髮送不成功的消息,告警,人工介入。


通常而言,對於大部分場景來講異步回調的形式就能夠了,只有那種須要徹底保證不能丟失消息的場景咱們作一套完整的解決方案。

MQ丟失

若是生產者保證消息發送到MQ,而MQ收到消息後還在內存中,這時候宕機了又沒來得及同步給從節點,就有可能致使消息丟失。

好比RocketMQ:

RocketMQ分爲同步刷盤和異步刷盤兩種方式,默認的是異步刷盤,就有可能致使消息還未刷到硬盤上就丟失了,能夠經過設置爲同步刷盤的方式來保證消息可靠性,這樣即便MQ掛了,恢復的時候也能夠從磁盤中去恢復消息。

好比Kafka也能夠經過配置作到:

acks=all 只有參與複製的全部節點所有收到消息,才返回生產者成功。這樣的話除非全部的節點都掛了,消息纔會丟失。
replication.factor=N,設置大於1的數,這會要求每一個partion至少有2個副本
min.insync.replicas=N,設置大於1的數,這會要求leader至少感知到一個follower還保持着鏈接
retries=N,設置一個很是大的值,讓生產者發送失敗一直重試

雖然咱們能夠經過配置的方式來達到MQ自己高可用的目的,可是都對性能有損耗,怎樣配置須要根據業務作出權衡。

消費者丟失

消費者丟失消息的場景:消費者剛收到消息,此時服務器宕機,MQ認爲消費者已經消費,不會重複發送消息,消息丟失。

RocketMQ默認是須要消費者回復ack確認,而kafka須要手動開啓配置關閉自動offset。

消費方不返回ack確認,重發的機制根據MQ類型的不一樣發送時間間隔、次數都不盡相同,若是重試超過次數以後會進入死信隊列,須要手工來處理了。(Kafka沒有這些)

消息的最終一致性

事務消息能夠達到分佈式事務的最終一致性,事務消息就是MQ提供的相似XA的分佈式事務能力。

半事務消息就是MQ收到了生產者的消息,可是沒有收到二次確認,不能投遞的消息。

實現原理以下:

  1. 生產者先發送一條半事務消息到MQ
  2. MQ收到消息後返回ack確認
  3. 生產者開始執行本地事務
  4. 若是事務執行成功發送commit到MQ,失敗發送rollback
  5. 若是MQ長時間未收到生產者的二次確認commit或者rollback,MQ對生產者發起消息回查
  6. 生產者查詢事務執行最終狀態
  7. 根據查詢事務狀態再次提交二次確認

最終,若是MQ收到二次確認commit,就能夠把消息投遞給消費者,反之若是是rollback,消息會保存下來而且在3天后被刪除。

數據庫

對於整個系統而言,最終全部的流量的查詢和寫入都落在數據庫上,數據庫是支撐系統高併發能力的核心。怎麼下降數據庫的壓力,提高數據庫的性能是支撐高併發的基石。主要的方式就是經過讀寫分離和分庫分表來解決這個問題。

對於整個系統而言,流量應該是一個漏斗的形式。好比咱們的日活用戶DAU有20萬,實際可能天天來到提單頁的用戶只有3萬QPS,最終轉化到下單支付成功的QPS只有1萬。那麼對於系統來講讀是大於寫的,這時候能夠經過讀寫分離的方式來下降數據庫的壓力。

讀寫分離也就至關於數據庫集羣的方式下降了單節點的壓力。而面對數據的急劇增加,原來的單庫單表的存儲方式已經沒法支撐整個業務的發展,這時候就須要對數據庫進行分庫分表了。針對微服務而言垂直的分庫自己已是作過的,剩下大部分都是分表的方案了。

水平分表

首先根據業務場景來決定使用什麼字段做爲分表字段(sharding_key),好比咱們如今日訂單1000萬,咱們大部分的場景來源於C端,咱們能夠用user_id做爲sharding_key,數據查詢支持到最近3個月的訂單,超過3個月的作歸檔處理,那麼3個月的數據量就是9億,能夠分1024張表,那麼每張表的數據大概就在100萬左右。

好比用戶id爲100,那咱們都通過hash(100),而後對1024取模,就能夠落到對應的表上了。

分表後的ID惟一性

由於咱們主鍵默認都是自增的,那麼分表以後的主鍵在不一樣表就確定會有衝突了。有幾個辦法考慮:

  1. 設定步長,好比1-1024張表咱們分別設定1-1024的基礎步長,這樣主鍵落到不一樣的表就不會衝突了。
  2. 分佈式ID,本身實現一套分佈式ID生成算法或者使用開源的好比雪花算法這種
  3. 分表後不使用主鍵做爲查詢依據,而是每張表單獨新增一個字段做爲惟一主鍵使用,好比訂單表訂單號是惟一的,無論最終落在哪張表都基於訂單號做爲查詢依據,更新也同樣。

主從同步原理

  1. master提交完事務後,寫入binlog
  2. slave鏈接到master,獲取binlog
  3. master建立dump線程,推送binglog到slave
  4. slave啓動一個IO線程讀取同步過來的master的binlog,記錄到relay log中繼日誌中
  5. slave再開啓一個sql線程讀取relay log事件並在slave執行,完成同步
  6. slave記錄本身的binglog

因爲mysql默認的複製方式是異步的,主庫把日誌發送給從庫後不關心從庫是否已經處理,這樣會產生一個問題就是假設主庫掛了,從庫處理失敗了,這時候從庫升爲主庫後,日誌就丟失了。由此產生兩個概念。

全同步複製

主庫寫入binlog後強制同步日誌到從庫,全部的從庫都執行完成後才返回給客戶端,可是很顯然這個方式的話性能會受到嚴重影響。

半同步複製

和全同步不一樣的是,半同步複製的邏輯是這樣,從庫寫入日誌成功後返回ACK確認給主庫,主庫收到至少一個從庫的確認就認爲寫操做完成。

緩存

緩存做爲高性能的表明,在某些特殊業務可能承擔90%以上的熱點流量。對於一些活動好比秒殺這種併發QPS可能幾十萬的場景,引入緩存事先預熱能夠大幅下降對數據庫的壓力,10萬的QPS對於單機的數據庫來講可能就掛了,可是對於如redis這樣的緩存來講就徹底不是問題。

以秒殺系統舉例,活動預熱商品信息能夠提早緩存提供查詢服務,活動庫存數據能夠提早緩存,下單流程能夠徹底走緩存扣減,秒殺結束後再異步寫入數據庫,數據庫承擔的壓力就小的太多了。固然,引入緩存以後就還要考慮緩存擊穿、雪崩、熱點一系列的問題了。

熱key問題

所謂熱key問題就是,忽然有幾十萬的請求去訪問redis上的某個特定key,那麼這樣會形成流量過於集中,達到物理網卡上限,從而致使這臺redis的服務器宕機引起雪崩。

針對熱key的解決方案:

  1. 提早把熱key打散到不一樣的服務器,下降壓力
  2. 加入二級緩存,提早加載熱key數據到內存中,若是redis宕機,走內存查詢

緩存擊穿

緩存擊穿的概念就是單個key併發訪問太高,過時時致使全部請求直接打到db上,這個和熱key的問題比較相似,只是說的點在於過時致使請求所有打到DB上而已。

解決方案:

  1. 加鎖更新,好比請求查詢A,發現緩存中沒有,對A這個key加鎖,同時去數據庫查詢數據,寫入緩存,再返回給用戶,這樣後面的請求就能夠從緩存中拿到數據了。
  2. 將過時時間組合寫在value中,經過異步的方式不斷的刷新過時時間,防止此類現象。

緩存穿透

緩存穿透是指查詢不存在緩存中的數據,每次請求都會打到DB,就像緩存不存在同樣。

針對這個問題,加一層布隆過濾器。布隆過濾器的原理是在你存入數據的時候,會經過散列函數將它映射爲一個位數組中的K個點,同時把他們置爲1。

這樣當用戶再次來查詢A,而A在布隆過濾器值爲0,直接返回,就不會產生擊穿請求打到DB了。

顯然,使用布隆過濾器以後會有一個問題就是誤判,由於它自己是一個數組,可能會有多個值落到同一個位置,那麼理論上來講只要咱們的數組長度夠長,誤判的機率就會越低,這種問題就根據實際狀況來就行了。

緩存雪崩

當某一時刻發生大規模的緩存失效的狀況,好比你的緩存服務宕機了,會有大量的請求進來直接打到DB上,這樣可能致使整個系統的崩潰,稱爲雪崩。雪崩和擊穿、熱key的問題不太同樣的是,他是指大規模的緩存都過時失效了。

針對雪崩幾個解決方案:

  1. 針對不一樣key設置不一樣的過時時間,避免同時過時
  2. 限流,若是redis宕機,能夠限流,避免同時刻大量請求打崩DB
  3. 二級緩存,同熱key的方案。

穩定性

熔斷

好比營銷服務掛了或者接口大量超時的異常狀況,不能影響下單的主鏈路,涉及到積分的扣減一些操做能夠在過後作補救。

限流

對突發如大促秒殺類的高併發,若是一些接口不作限流處理,可能直接就把服務打掛了,針對每一個接口的壓測性能的評估作出合適的限流尤其重要。

降級

熔斷以後實際上能夠說就是降級的一種,以熔斷的舉例來講營銷接口熔斷以後降級方案就是短期內再也不調用營銷的服務,等到營銷恢復以後再調用。

預案

通常來講,就算是有統一配置中心,在業務的高峯期也是不容許作出任何的變動的,可是經過配置合理的預案能夠在緊急的時候作一些修改。

覈對

針對各類分佈式系統產生的分佈式事務一致性或者受到***致使的數據異常,很是須要覈對平臺來作最後的兜底的數據驗證。好比下游支付系統和訂單系統的金額作覈對是否正確,若是收到中間人***落庫的數據是否保證正確性。

總結

其實能夠看到,怎麼設計高併發系統這個問題自己他是不難的,無非是基於你知道的知識點,從物理硬件層面到軟件的架構、代碼層面的優化,使用什麼中間件來不斷提升系統的抗壓能力。可是這個問題自己會帶來更多的問題,微服務自己的拆分帶來了分佈式事務的問題,http、RPC框架的使用帶來了通訊效率、路由、容錯的問題,MQ的引入帶來了消息丟失、積壓、事務消息、順序消息的問題,緩存的引入又會帶來一致性、雪崩、擊穿的問題,數據庫的讀寫分離、分庫分表又會帶來主從同步延遲、分佈式ID、事務一致性的問題,而爲了解決這些問題咱們又要不斷的加入各類措施熔斷、限流、降級、離線覈對、預案處理等等來防止和追溯這些問題。

這篇文章結合了以前的文章的一些內容,實際上最開始的時候就是想寫這一篇,發現篇幅實在太大了並且內容很差歸納,因此就拆分了幾篇開始寫,這一篇算是對前面內容的一個概括和總結吧,不是我爲了水。

相關文章
相關標籤/搜索