這是一道很常見的面試題,但是大多數(shù)人并不知道怎么回答,這種問題其實可以有很多形式的提問方式,你一定見過而且感覺無從下手:
面對業(yè)務急劇增長你怎么處理?
業(yè)務量增長10倍、100倍怎么處理?
你們系統(tǒng)怎么支撐高并發(fā)的?
怎么設計一個高并發(fā)系統(tǒng)?
高并發(fā)系統(tǒng)都有什么特點?
... ...
諸如此類,問法很多,但是面試這種類型的問題,看著很難無處下手,但是我們可以有一個常規(guī)的思路去回答,就是圍繞支撐高并發(fā)的業(yè)務場景怎么設計系統(tǒng)才合理?如果你能想到這一點,那接下來我們就可以圍繞硬件和軟件層面怎么支撐高并發(fā)這個話題去闡述了。本質上,這個問題就是綜合考驗你對各個細節(jié)是否知道怎么處理,是否有經驗處理過而已。
面對超高的并發(fā),首先硬件層面機器要能扛得住,其次架構設計做好微服務的拆分,代碼層面各種緩存、削峰、解耦等等問題要處理好,數(shù)據(jù)庫層面做好讀寫分離、分庫分表,穩(wěn)定性方面要保證有監(jiān)控,熔斷限流降級該有的必須要有,發(fā)生問題能及時發(fā)現(xiàn)處理。這樣從整個系統(tǒng)設計方面就會有一個初步的概念。
微服務架構演化
在互聯(lián)網早期的時候,單體架構就足以支撐起日常的業(yè)務需求,大家的所有業(yè)務服務都在一個項目里,部署在一臺物理機器上。所有的業(yè)務包括你的交易系統(tǒng)、會員信息、庫存、商品等等都夾雜在一起,當流量一旦起來之后,單體架構的問題就暴露出來了,機器掛了所有的業(yè)務全部無法使用了。
于是,集群架構的架構開始出現(xiàn),單機無法抗住的壓力,最簡單的辦法就是水平拓展橫向擴容了,這樣,通過負載均衡把壓力流量分攤到不同的機器上,暫時是解決了單點導致服務不可用的問題。
但是隨著業(yè)務的發(fā)展,在一個項目里維護所有的業(yè)務場景使開發(fā)和代碼維護變得越來越困難,一個簡單的需求改動都需要發(fā)布整個服務,代碼的合并沖突也會變得越來越頻繁,同時線上故障出現(xiàn)的可能性越大。微服務的架構模式就誕生了。
把每個獨立的業(yè)務拆分開獨立部署,開發(fā)和維護的成本降低,集群能承受的壓力也提高了,再也不會出現(xiàn)一個小小的改動點需要牽一發(fā)而動全身了。
以上的點從高并發(fā)的角度而言,似乎都可以歸類為通過服務拆分和集群物理機器的擴展提高了整體的系統(tǒng)抗壓能力,那么,隨之拆分而帶來的問題也就是高并發(fā)系統(tǒng)需要解決的問題。
RPC
微服務化的拆分帶來的好處和便利性是顯而易見的,但是與此同時各個微服務之間的通信就需要考慮了。傳統(tǒng)HTTP的通信方式對性能是極大的浪費,這時候就需要引入諸如Dubbo類的RPC框架,基于TCP長連接的方式提高整個集群通信的效率。
我們假設原來來自客戶端的QPS是9000的話,那么通過負載均衡策略分散到每臺機器就是3000,而HTTP改為RPC之后接口的耗時縮短了,單機和整體的QPS就提升了。而RPC框架本身一般都自帶負載均衡、熔斷降級的機制,可以更好的維護整個系統(tǒng)的高可用性。
那么說完RPC,作為基本上國內普遍的選擇Dubbo的一些基本原理就是接下來的問題。
Dubbo工作原理
-
服務啟動的時候,provider和consumer根據(jù)配置信息,連接到注冊中心register,分別向注冊中心注冊和訂閱服務
-
register根據(jù)服務訂閱關系,返回provider信息到consumer,同時consumer會把provider信息緩存到本地。如果信息有變更,consumer會收到來自register的推送
-
consumer生成代理對象,同時根據(jù)負載均衡策略,選擇一臺provider,同時定時向monitor記錄接口的調用次數(shù)和時間信息
-
拿到代理對象之后,consumer通過代理對象發(fā)起接口調用
-
provider收到請求后對數(shù)據(jù)進行反序列化,然后通過代理調用具體的接口實現(xiàn)
Dubbo負載均衡策略
-
加權隨機:假設我們有一組服務器 servers = [A, B, C],他們對應的權重為 weights = [5, 3, 2],權重總和為10?,F(xiàn)在把這些權重值平鋪在一維坐標值上,[0, 5) 區(qū)間屬于服務器 A,[5, 8) 區(qū)間屬于服務器 B,[8, 10) 區(qū)間屬于服務器 C。接下來通過隨機數(shù)生成器生成一個范圍在 [0, 10) 之間的隨機數(shù),然后計算這個隨機數(shù)會落到哪個區(qū)間上就可以了。
-
最小活躍數(shù):每個服務提供者對應一個活躍數(shù) active,初始情況下,所有服務提供者活躍數(shù)均為0。每收到一個請求,活躍數(shù)加1,完成請求后則將活躍數(shù)減1。在服務運行一段時間后,性能好的服務提供者處理請求的速度更快,因此活躍數(shù)下降的也越快,此時這樣的服務提供者能夠優(yōu)先獲取到新的服務請求。
-
一致性hash:通過hash算法,把provider的invoke和隨機節(jié)點生成hash,并將這個 hash 投射到 [0, 2^32 - 1] 的圓環(huán)上,查詢的時候根據(jù)key進行md5然后進行hash,得到第一個節(jié)點的值大于等于當前hash的invoker。
-
加權輪詢:比如服務器 A、B、C 權重比為 5:2:1,那么在8次請求中,服務器 A 將收到其中的5次請求,服務器 B 會收到其中的2次請求,服務器 C 則收到其中的1次請求。
集群容錯
-
Failover Cluster失敗自動切換:dubbo的默認容錯方案,當調用失敗時自動切換到其他可用的節(jié)點,具體的重試次數(shù)和間隔時間可用通過引用服務的時候配置,默認重試次數(shù)為1也就是只調用一次。
-
Failback Cluster快速失?。涸谡{用失敗,記錄日志和調用信息,然后返回空結果給consumer,并且通過定時任務每隔5秒對失敗的調用進行重試
-
Failfast Cluster失敗自動恢復:只會調用一次,失敗后立刻拋出異常
-
Failsafe Cluster失敗安全:調用出現(xiàn)異常,記錄日志不拋出,返回空結果
-
Forking Cluster并行調用多個服務提供者:通過線程池創(chuàng)建多個線程,并發(fā)調用多個provider,結果保存到阻塞隊列,只要有一個provider成功返回了結果,就會立刻返回結果
-
Broadcast Cluster廣播模式:逐個調用每個provider,如果其中一臺報錯,在循環(huán)調用結束后,拋出異常。
消息隊列
對于MQ的作用大家都應該很了解了,削峰填谷、解耦。依賴消息隊列,同步轉異步的方式,可以降低微服務之間的耦合。
對于一些不需要同步執(zhí)行的接口,可以通過引入消息隊列的方式異步執(zhí)行以提高接口響應時間。在交易完成之后需要扣庫存,然后可能需要給會員發(fā)放積分,本質上,發(fā)積分的動作應該屬于履約服務,對實時性的要求也不高,我們只要保證最終一致性也就是能履約成功就行了。對于這種同類性質的請求就可以走MQ異步,也就提高了系統(tǒng)抗壓能力了。
對于消息隊列而言,怎么在使用的時候保證消息的可靠性、不丟失?
消息可靠性
消息丟失可能發(fā)生在生產者發(fā)送消息、MQ本身丟失消息、消費者丟失消息3個方面。
生產者丟失
生產者丟失消息的可能點在于程序發(fā)送失敗拋異常了沒有重試處理,或者發(fā)送的過程成功但是過程中網絡閃斷MQ沒收到,消息就丟失了。
由于同步發(fā)送的一般不會出現(xiàn)這樣使用方式,所以我們就不考慮同步發(fā)送的問題,我們基于異步發(fā)送的場景來說。
異步發(fā)送分為兩個方式:異步有回調和異步無回調,無回調的方式,生產者發(fā)送完后不管結果可能就會造成消息丟失,而通過異步發(fā)送+回調通知+本地消息表的形式我們就可以做出一個解決方案。以下單的場景舉例。
-
下單后先保存本地數(shù)據(jù)和MQ消息表,這時候消息的狀態(tài)是發(fā)送中,如果本地事務失敗,那么下單失敗,事務回滾。 -
下單成功,直接返回客戶端成功,異步發(fā)送MQ消息 -
MQ回調通知消息發(fā)送結果,對應更新數(shù)據(jù)庫MQ發(fā)送狀態(tài) -
JOB輪詢超過一定時間(時間根據(jù)業(yè)務配置)還未發(fā)送成功的消息去重試 -
在監(jiān)控平臺配置或者JOB程序處理超過一定次數(shù)一直發(fā)送不成功的消息,告警,人工介入。
一般而言,對于大部分場景來說異步回調的形式就可以了,只有那種需要完全保證不能丟失消息的場景我們做一套完整的解決方案。
MQ丟失
如果生產者保證消息發(fā)送到MQ,而MQ收到消息后還在內存中,這時候宕機了又沒來得及同步給從節(jié)點,就有可能導致消息丟失。
比如RocketMQ:
RocketMQ分為同步刷盤和異步刷盤兩種方式,默認的是異步刷盤,就有可能導致消息還未刷到硬盤上就丟失了,可以通過設置為同步刷盤的方式來保證消息可靠性,這樣即使MQ掛了,恢復的時候也可以從磁盤中去恢復消息。
比如Kafka也可以通過配置做到:
acks=all 只有參與復制的所有節(jié)點全部收到消息,才返回生產者成功。這樣的話除非所有的節(jié)點都掛了,消息才會丟失。
replication.factor=N,設置大于1的數(shù),這會要求每個partion至少有2個副本
min.insync.replicas=N,設置大于1的數(shù),這會要求leader至少感知到一個follower還保持著連接
retries=N,設置一個非常大的值,讓生產者發(fā)送失敗一直重試
雖然我們可以通過配置的方式來達到MQ本身高可用的目的,但是都對性能有損耗,怎樣配置需要根據(jù)業(yè)務做出權衡。
消費者丟失
消費者丟失消息的場景:消費者剛收到消息,此時服務器宕機,MQ認為消費者已經消費,不會重復發(fā)送消息,消息丟失。
RocketMQ默認是需要消費者回復ack確認,而kafka需要手動開啟配置關閉自動offset。
消費方不返回ack確認,重發(fā)的機制根據(jù)MQ類型的不同發(fā)送時間間隔、次數(shù)都不盡相同,如果重試超過次數(shù)之后會進入死信隊列,需要手工來處理了。(Kafka沒有這些)
消息的最終一致性
事務消息可以達到分布式事務的最終一致性,事務消息就是MQ提供的類似XA的分布式事務能力。
半事務消息就是MQ收到了生產者的消息,但是沒有收到二次確認,不能投遞的消息。
實現(xiàn)原理如下:
-
生產者先發(fā)送一條半事務消息到MQ -
MQ收到消息后返回ack確認 -
生產者開始執(zhí)行本地事務 -
如果事務執(zhí)行成功發(fā)送commit到MQ,失敗發(fā)送rollback -
如果MQ長時間未收到生產者的二次確認commit或者rollback,MQ對生產者發(fā)起消息回查 -
生產者查詢事務執(zhí)行最終狀態(tài) -
根據(jù)查詢事務狀態(tài)再次提交二次確認
最終,如果MQ收到二次確認commit,就可以把消息投遞給消費者,反之如果是rollback,消息會保存下來并且在3天后被刪除。
數(shù)據(jù)庫
對于整個系統(tǒng)而言,最終所有的流量的查詢和寫入都落在數(shù)據(jù)庫上,數(shù)據(jù)庫是支撐系統(tǒng)高并發(fā)能力的核心。怎么降低數(shù)據(jù)庫的壓力,提升數(shù)據(jù)庫的性能是支撐高并發(fā)的基石。主要的方式就是通過讀寫分離和分庫分表來解決這個問題。
對于整個系統(tǒng)而言,流量應該是一個漏斗的形式。比如我們的日活用戶DAU有20萬,實際可能每天來到提單頁的用戶只有3萬QPS,最終轉化到下單支付成功的QPS只有1萬。那么對于系統(tǒng)來說讀是大于寫的,這時候可以通過讀寫分離的方式來降低數(shù)據(jù)庫的壓力。
讀寫分離也就相當于數(shù)據(jù)庫集群的方式降低了單節(jié)點的壓力。而面對數(shù)據(jù)的急劇增長,原來的單庫單表的存儲方式已經無法支撐整個業(yè)務的發(fā)展,這時候就需要對數(shù)據(jù)庫進行分庫分表了。針對微服務而言垂直的分庫本身已經是做過的,剩下大部分都是分表的方案了。
水平分表
首先根據(jù)業(yè)務場景來決定使用什么字段作為分表字段(sharding_key),比如我們現(xiàn)在日訂單1000萬,我們大部分的場景來源于C端,我們可以用user_id作為sharding_key,數(shù)據(jù)查詢支持到最近3個月的訂單,超過3個月的做歸檔處理,那么3個月的數(shù)據(jù)量就是9億,可以分1024張表,那么每張表的數(shù)據(jù)大概就在100萬左右。
比如用戶id為100,那我們都經過hash(100),然后對1024取模,就可以落到對應的表上了。
分表后的ID唯一性
因為我們主鍵默認都是自增的,那么分表之后的主鍵在不同表就肯定會有沖突了。有幾個辦法考慮:
-
設定步長,比如1-1024張表我們分別設定1-1024的基礎步長,這樣主鍵落到不同的表就不會沖突了。 -
分布式ID,自己實現(xiàn)一套分布式ID生成算法或者使用開源的比如雪花算法這種 -
分表后不使用主鍵作為查詢依據(jù),而是每張表單獨新增一個字段作為唯一主鍵使用,比如訂單表訂單號是唯一的,不管最終落在哪張表都基于訂單號作為查詢依據(jù),更新也一樣。
主從同步原理
-
master提交完事務后,寫入binlog -
slave連接到master,獲取binlog -
master創(chuàng)建dump線程,推送binglog到slave -
slave啟動一個IO線程讀取同步過來的master的binlog,記錄到relay log中繼日志中 -
slave再開啟一個sql線程讀取relay log事件并在slave執(zhí)行,完成同步 -
slave記錄自己的binglog
由于mysql默認的復制方式是異步的,主庫把日志發(fā)送給從庫后不關心從庫是否已經處理,這樣會產生一個問題就是假設主庫掛了,從庫處理失敗了,這時候從庫升為主庫后,日志就丟失了。由此產生兩個概念。
全同步復制
主庫寫入binlog后強制同步日志到從庫,所有的從庫都執(zhí)行完成后才返回給客戶端,但是很顯然這個方式的話性能會受到嚴重影響。
半同步復制
和全同步不同的是,半同步復制的邏輯是這樣,從庫寫入日志成功后返回ACK確認給主庫,主庫收到至少一個從庫的確認就認為寫操作完成。
緩存
緩存作為高性能的代表,在某些特殊業(yè)務可能承擔90%以上的熱點流量。對于一些活動比如秒殺這種并發(fā)QPS可能幾十萬的場景,引入緩存事先預熱可以大幅降低對數(shù)據(jù)庫的壓力,10萬的QPS對于單機的數(shù)據(jù)庫來說可能就掛了,但是對于如redis這樣的緩存來說就完全不是問題。
以秒殺系統(tǒng)舉例,活動預熱商品信息可以提前緩存提供查詢服務,活動庫存數(shù)據(jù)可以提前緩存,下單流程可以完全走緩存扣減,秒殺結束后再異步寫入數(shù)據(jù)庫,數(shù)據(jù)庫承擔的壓力就小的太多了。當然,引入緩存之后就還要考慮緩存擊穿、雪崩、熱點一系列的問題了。
熱key問題
所謂熱key問題就是,突然有幾十萬的請求去訪問redis上的某個特定key,那么這樣會造成流量過于集中,達到物理網卡上限,從而導致這臺redis的服務器宕機引發(fā)雪崩。
針對熱key的解決方案:
-
提前把熱key打散到不同的服務器,降低壓力 -
加入二級緩存,提前加載熱key數(shù)據(jù)到內存中,如果redis宕機,走內存查詢
緩存擊穿
緩存擊穿的概念就是單個key并發(fā)訪問過高,過期時導致所有請求直接打到db上,這個和熱key的問題比較類似,只是說的點在于過期導致請求全部打到DB上而已。
解決方案:
-
加鎖更新,比如請求查詢A,發(fā)現(xiàn)緩存中沒有,對A這個key加鎖,同時去數(shù)據(jù)庫查詢數(shù)據(jù),寫入緩存,再返回給用戶,這樣后面的請求就可以從緩存中拿到數(shù)據(jù)了。 -
將過期時間組合寫在value中,通過異步的方式不斷的刷新過期時間,防止此類現(xiàn)象。
緩存穿透
緩存穿透是指查詢不存在緩存中的數(shù)據(jù),每次請求都會打到DB,就像緩存不存在一樣。
針對這個問題,加一層布隆過濾器。布隆過濾器的原理是在你存入數(shù)據(jù)的時候,會通過散列函數(shù)將它映射為一個位數(shù)組中的K個點,同時把他們置為1。
這樣當用戶再次來查詢A,而A在布隆過濾器值為0,直接返回,就不會產生擊穿請求打到DB了。
顯然,使用布隆過濾器之后會有一個問題就是誤判,因為它本身是一個數(shù)組,可能會有多個值落到同一個位置,那么理論上來說只要我們的數(shù)組長度夠長,誤判的概率就會越低,這種問題就根據(jù)實際情況來就好了。
緩存雪崩
當某一時刻發(fā)生大規(guī)模的緩存失效的情況,比如你的緩存服務宕機了,會有大量的請求進來直接打到DB上,這樣可能導致整個系統(tǒng)的崩潰,稱為雪崩。雪崩和擊穿、熱key的問題不太一樣的是,他是指大規(guī)模的緩存都過期失效了。
針對雪崩幾個解決方案:
-
針對不同key設置不同的過期時間,避免同時過期 -
限流,如果redis宕機,可以限流,避免同時刻大量請求打崩DB -
二級緩存,同熱key的方案。
穩(wěn)定性
熔斷
比如營銷服務掛了或者接口大量超時的異常情況,不能影響下單的主鏈路,涉及到積分的扣減一些操作可以在事后做補救。
限流
對突發(fā)如大促秒殺類的高并發(fā),如果一些接口不做限流處理,可能直接就把服務打掛了,針對每個接口的壓測性能的評估做出合適的限流尤為重要。
降級
熔斷之后實際上可以說就是降級的一種,以熔斷的舉例來說營銷接口熔斷之后降級方案就是短時間內不再調用營銷的服務,等到營銷恢復之后再調用。
預案
一般來說,就算是有統(tǒng)一配置中心,在業(yè)務的高峰期也是不允許做出任何的變更的,但是通過配置合理的預案可以在緊急的時候做一些修改。
核對
針對各種分布式系統(tǒng)產生的分布式事務一致性或者受到攻擊導致的數(shù)據(jù)異常,非常需要核對平臺來做最后的兜底的數(shù)據(jù)驗證。比如下游支付系統(tǒng)和訂單系統(tǒng)的金額做核對是否正確,如果收到中間人攻擊落庫的數(shù)據(jù)是否保證正確性。
總結
其實可以看到,怎么設計高并發(fā)系統(tǒng)這個問題本身他是不難的,無非是基于你知道的知識點,從物理硬件層面到軟件的架構、代碼層面的優(yōu)化,使用什么中間件來不斷提高系統(tǒng)的抗壓能力。但是這個問題本身會帶來更多的問題,微服務本身的拆分帶來了分布式事務的問題,http、RPC框架的使用帶來了通信效率、路由、容錯的問題,MQ的引入帶來了消息丟失、積壓、事務消息、順序消息的問題,緩存的引入又會帶來一致性、雪崩、擊穿的問題,數(shù)據(jù)庫的讀寫分離、分庫分表又會帶來主從同步延遲、分布式ID、事務一致性的問題,而為了解決這些問題我們又要不斷的加入各種措施熔斷、限流、降級、離線核對、預案處理等等來防止和追溯這些問題。
—————END—————
喜歡本文的朋友,歡迎關注公眾號?程序員小灰,收看更多精彩內容
點個[在看],是對小灰最大的支持!
免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!