面試官:面對千萬級、億級流量怎么處理?
這是一道很常見的面試題,但是大多數(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)網(wǎng)早期的時候,單體架構就足以支撐起日常的業(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。現(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)調用結束后,拋出異常。
消息隊列

消息可靠性
下單后先保存本地數(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ā)送不成功的消息,告警,人工介入。

acks=all 只有參與復制的所有節(jié)點全部收到消息,才返回生產者成功。這樣的話除非所有的節(jié)點都掛了,消息才會丟失。
replication.factor=N,設置大于1的數(shù),這會要求每個partion至少有2個副本
min.insync.replicas=N,設置大于1的數(shù),這會要求leader至少感知到一個follower還保持著連接
retries=N,設置一個非常大的值,讓生產者發(fā)送失敗一直重試

消息的最終一致性
生產者先發(fā)送一條半事務消息到MQ MQ收到消息后返回ack確認 生產者開始執(zhí)行本地事務 如果事務執(zhí)行成功發(fā)送commit到MQ,失敗發(fā)送rollback 如果MQ長時間未收到生產者的二次確認commit或者rollback,MQ對生產者發(fā)起消息回查 生產者查詢事務執(zhí)行最終狀態(tài) 根據(jù)查詢事務狀態(tài)再次提交二次確認

數(shù)據(jù)庫

水平分表
分表后的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

緩存

熱key問題

提前把熱key打散到不同的服務器,降低壓力 加入二級緩存,提前加載熱key數(shù)據(jù)到內存中,如果redis宕機,走內存查詢
緩存擊穿
加鎖更新,比如請求查詢A,發(fā)現(xiàn)緩存中沒有,對A這個key加鎖,同時去數(shù)據(jù)庫查詢數(shù)據(jù),寫入緩存,再返回給用戶,這樣后面的請求就可以從緩存中拿到數(shù)據(jù)了。 將過期時間組合寫在value中,通過異步的方式不斷的刷新過期時間,防止此類現(xiàn)象。

緩存穿透


緩存雪崩

針對不同key設置不同的過期時間,避免同時過期 限流,如果redis宕機,可以限流,避免同時刻大量請求打崩DB 二級緩存,同熱key的方案。
穩(wěn)定性

總結
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
