微服務在面試時被問到的內容相對較少,常見的面試題如下:
- SpringCloud有哪些常用組件?分別是什么作用?
- 服務注冊發現的基本流程是怎樣的?
- Eureka和Nacos有哪些區別?
- Nacos的分級存儲模型是什么意思?
- Ribbon和SpringCloudLoadBalancer有什么差異
- 什么是服務雪崩,常見的解決方案有哪些?
- Hystix和Sentinel有什么區別和聯系?
- 限流的常見算法有哪些?
- 什么是CAP理論和BASE思想?
- 項目中碰到過分布式事務問題嗎?怎么解決的?
- AT模式如何解決臟讀和臟寫問題的?
- TCC模式與AT模式對比,有哪些優缺點
可以發現,這些問題都是圍繞著SpringCloud的相關組件的,其中有些問題我們在課堂上已經介紹過,這里不再贅述。我們重點講解一些之前沒有講過的,與底層實現有關的部分。
講解的思路還是基于SpringCloud的組件分類來講的,主要包括:
- 分布式事務
- 注冊中心
- 遠程調用
- 服務保護
等幾個方面
1.分布式事務
分布式事務,就是指不是在單個服務或單個數據庫架構下,產生的事務,例如:
- 跨數據源的分布式事務
- 跨服務的分布式事務
- 綜合情況
我們之前解決分布式事務問題是直接使用Seata框架的AT模式,但是解決分布式事務問題的方案遠不止這一種。
1.1.CAP定理
解決分布式事務問題,需要一些分布式系統的基礎知識作為理論指導,首先就是CAP定理。
1998年,加州大學的計算機科學家 Eric Brewer 提出,分布式系統有三個指標:
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分區容錯性)
它們的第一個字母分別是 C
、A
、P
。Eric Brewer認為任何分布式系統架構方案都不可能同時滿足這3個目標,這個結論就叫做 CAP 定理。
為什么呢?
1.1.1.一致性
Consistency
(一致性):用戶訪問分布式系統中的任意節點,得到的數據必須一致。
比如現在包含兩個節點,其中的初始數據是一致的:
暫時無法在飛書文檔外展示此內容
當我們修改其中一個節點的數據時,兩者的數據產生了差異:
暫時無法在飛書文檔外展示此內容
要想保住一致性,就必須實現node01 到 node02的數據 同步:
暫時無法在飛書文檔外展示此內容
1.1.2.可用性
Availability (可用性):用戶訪問分布式系統時,讀或寫操作總能成功。
只能讀不能寫,或者只能寫不能讀,或者兩者都不能執行,就說明系統弱可用或不可用。
1.1.3.分區容錯
Partition
,就是分區,就是當分布式系統節點之間出現網絡故障導致節點之間無法通信的情況:
暫時無法在飛書文檔外展示此內容
如上圖,node01和node02之間網關暢通,但是與node03之間網絡斷開。于是node03成為一個獨立的網絡分區;node01和node02在一個網絡分區。
Tolerance
,就是容錯,即便是系統出現網絡分區,整個系統也要持續對外提供服務。
1.1.4.矛盾
在分布式系統中,網絡不能100%保證暢通,也就是說網絡分區的情況一定會存在。而我們的系統必須要持續運行,對外提供服務。所以分區容錯性(P
)是硬性指標,所有分布式系統都要滿足。而在設計分布式系統時要取舍的就是一致性(C
)和可用性(A
)了。
假如現在出現了網絡分區,如圖:
暫時無法在飛書文檔外展示此內容
由于網絡故障,當我們把數據寫入node01時,可以與node02完成數據同步,但是無法同步給node03。現在有兩種選擇:
- 允許用戶任意讀寫,保證可用性。但由于node03無法完成同步,就會出現數據不一致的情況。滿足AP
- 不允許用戶寫,可以讀,直到網絡恢復,分區消失。這樣就確保了一致性,但犧牲了可用性。滿足CP
可見,在分布式系統中,A
和C
之間只能滿足一個。
1.2.BASE理論
既然分布式系統要遵循CAP定理,那么問題來了,我到底是該犧牲一致性還是可用性呢?如果犧牲了一致性,出現數據不一致該怎么處理?
人們在總結系統設計經驗時,最終得到了一些心得:
- Basically Available (基本可用):分布式系統在出現故障時,允許損失部分可用性,即保證核心可用。
- Soft State(軟狀態):在一定時間內,允許出現中間狀態,比如臨時的不一致狀態。
- Eventually Consistent(最終一致性):雖然無法保證強一致性,但是在軟狀態結束后,最終達到數據一致。
以上就是BASE理論。
簡單來說,BASE理論就是一種取舍的方案,不再追求完美,而是最終達成目標。因此解決分布式事務的思想也是這樣,有兩個方向:
- AP思想:各個子事務分別執行和提交,無需鎖定數據。允許出現結果不一致,然后采用彌補措施恢復,實現最終一致即可。例如
AT
模式就是如此 - CP思想:各個子事務執行后不要提交,而是等待彼此結果,然后同時提交或回滾。在這個過程中鎖定資源,不允許其它人訪問,數據處于不可用狀態,但能保證一致性。例如
XA
模式
1.3.AT模式的臟寫問題
我們先回顧一下AT模式的流程,AT模式也分為兩個階段:
第一階段是記錄數據快照,執行并提交事務:
第二階段根據階段一的結果來判斷:
- 如果每一個分支事務都成功,則事務已經結束(因為階段一已經提交),因此刪除階段一的快照即可
- 如果有任意分支事務失敗,則需要根據快照恢復到更新前數據。然后刪除快照
這種模式在大多數情況下(99%)并不會有什么問題,不過在極端情況下,特別是多線程并發訪問AT模式的分布式事務時,有可能出現臟寫問題,如圖:
解決思路就是引入了全局鎖的概念。在釋放DB鎖之前,先拿到全局鎖。避免同一時刻有另外一個事務來操作當前數據。
具體可以參考官方文檔:
https://seata.io/zh-cn/docs/dev/mode/at-mode.html
1.4.TCC模式
TCC模式與AT模式非常相似,每階段都是獨立事務,不同的是TCC通過人工編碼來實現數據恢復。需要實現三個方法:
try
:資源的檢測和預留;confirm
:完成資源操作業務;要求try
成功confirm
一定要能成功。cancel
:預留資源釋放,可以理解為try的反向操作。
1.4.1.流程分析
舉例,一個扣減用戶余額的業務。假設賬戶A原來余額是100,需要余額扣減30元。
階段一( Try ):檢查余額是否充足,如果充足則凍結金額增加30元,可用余額扣除30
初始余額:
余額充足,可以凍結:
此時,總金額 = 凍結金額 + 可用金額,數量依然是100不變。事務直接提交無需等待其它事務。
階段二(Confirm):假如要提交(Confirm),之前可用金額已經扣減,并轉移到凍結金額。因此可用金額不變,直接凍結金額扣減30即可:
此時,總金額 = 凍結金額 + 可用金額 = 0 + 70 = 70元
階段二(Canncel):如果要回滾(Cancel),則釋放之前凍結的金額,也就是凍結金額扣減30,可用余額增加30
1.4.2.事務懸掛和空回滾
假如一個分布式事務中包含兩個分支事務,try階段,一個分支成功執行,另一個分支事務阻塞:
如果阻塞時間太長,可能導致全局事務超時而觸發二階段的cancel
操作。兩個分支事務都會執行cancel操作:
要知道,其中一個分支是未執行try
操作的,直接執行了cancel
操作,反而會導致數據錯誤。因此,這種情況下,盡管cancel
方法要執行,但其中不能做任何回滾操作,這就是空回滾。
對于整個空回滾的分支事務,將來try方法阻塞結束依然會執行。但是整個全局事務其實已經結束了,因此永遠不會再有confirm或cancel,也就是說這個事務執行了一半,處于懸掛狀態,這就是業務懸掛問題。
以上問題都需要我們在編寫try、cancel方法時處理。
1.4.3.總結
TCC模式的每個階段是做什么的?
- Try:資源檢查和預留
- Confirm:業務執行和提交
- Cancel:預留資源的釋放
TCC的優點是什么?
- 一階段完成直接提交事務,釋放數據庫資源,性能好
- 相比AT模型,無需生成快照,無需使用全局鎖,性能最強
- 不依賴數據庫事務,而是依賴補償操作,可以用于非事務型數據庫
TCC的缺點是什么?
- 有代碼侵入,需要人為編寫try、Confirm和Cancel接口,太麻煩
- 軟狀態,事務是最終一致
- 需要考慮Confirm和Cancel的失敗情況,做好冪等處理、事務懸掛和空回滾處理
2.注冊中心
本章主要學習Nacos中的一些特性和原理,以及與Eureka的功能對比。
2.1.環境隔離
企業實際開發中,往往會搭建多個運行環境,例如:
- 開發環境
- 測試環境
- 預發布環境
- 生產環境
這些不同環境之間的服務和數據之間需要隔離。
還有的企業中,會開發多個項目,共享nacos集群。此時,這些項目之間也需要把服務和數據隔離。
因此,Nacos提供了基于namespace
的環境隔離功能。具體的隔離層次如圖所示:
說明:
- Nacos中可以配置多個
namespace
,相互之間完全隔離。默認的namespace
名為public
namespace
下還可以繼續分組,也就是group ,相互隔離。 默認的group是DEFAULT_GROUP
group
之下就是服務和配置了
2.1.1.創建namespace
nacos提供了一個默認的namespace
,叫做public
:
默認所有的服務和配置都屬于這個namespace
,當然我們也可以自己創建新的namespace
:
然后填寫表單:
添加完成后,可以在頁面看到我們新建的namespace
,并且Nacos為我們自動生成了一個命名空間id:
我們切換到配置列表頁,你會發現dev
這個命名空間下沒有任何配置:
因為之前我們添加的所有配置都在public
下:
2.1.2.微服務配置namespace
默認情況下,所有的微服務注冊發現、配置管理都是走public
這個命名空間。如果要指定命名空間則需要修改application.yml
文件。
比如,我們修改item-service
服務的bootstrap.yml文件,添加服務發現配置,指定其namespace
:
spring:application:name: item-service # 服務名稱profiles:active: devcloud:nacos:server-addr: 192.168.150.101 # nacos地址discovery: # 服務發現配置namespace: 8c468c63-b650-48da-a632-311c75e6d235 # 設置namespace,必須用id# 。。。略
啟動item-service
,查看服務列表,會發現item-service
出現在dev
下:
而其它服務則出現在public
下:
此時訪問http://localhost:8082/doc.html
,基于swagger
做測試:
會發現查詢結果中缺少商品的最新價格信息。
我們查看服務運行日志:
會發現cart-service
服務在遠程調用item-service
時,并沒有找到可用的實例。這證明不同namespace之間確實是相互隔離的,不可訪問。
當我們把namespace
切換回public
,或者統一都是以dev
時訪問恢復正常。
2.2.分級模型
在一些大型應用中,同一個服務可以部署很多實例。而這些實例可能分布在全國各地的不同機房。由于存在地域差異,網絡傳輸的速度會有很大不同,因此在做服務治理時需要區分不同機房的實例。
例如item-service,我們可以部署3個實例:
- 127.0.0.1:8081
- 127.0.0.1:8082
- 127.0.0.1:8083
假如這些實例分布在不同機房,例如:
- 127.0.0.1:8081,在上海機房
- 127.0.0.1:8082,在上海機房
- 127.0.0.1:8083,在杭州機房
Nacos中提供了集群(cluster
)的概念,來對應不同機房。也就是說,一個服務(service
)下可以有很多集群(cluster
),而一個集群(cluster
)中下又可以包含很多實例(instance
)。
如圖:
因此,結合我們上一節學習的namespace
命名空間的知識,任何一個微服務的實例在注冊到Nacos時,都會生成以下幾個信息,用來確認當前實例的身份,從外到內依次是:
- namespace:命名空間
- group:分組
- service:服務名
- cluster:集群
- instance:實例,包含ip和端口
這就是nacos中的服務分級模型。
在Nacos內部會有一個服務實例的注冊表,是基于Map實現的,其結構與分級模型的對應關系如下:
查看nacos控制臺,會發現默認情況下所有服務的集群都是default:
如果我們要修改服務所在集群,只需要修改bootstrap.yml
即可:
spring:cloud:nacos:discovery:cluster-name: BJ # 集群名稱,自定義
我們修改item-service
的bootstrap.yml
,然后重新創建一個實例:
再次查看nacos:
發現8084這個新的實例確實屬于BJ
這個集群了。
2.3.Eureka
Eureka是Netflix公司開源的一個服務注冊中心組件,早期版本的SpringCloud都是使用Eureka作為注冊中心。由于Eureka和Nacos的starter中提供的功能都是基于SpringCloudCommon規范,因此兩者使用起來差別不大。
課前資料中提供了一個Eureka的demo:
我們可以用idea打開查看一下:
結構說明:
eureka-server
:Eureka的服務端,也就是注冊中心。沒錯,Eureka服務端要自己創建項目order-service
:訂單服務,是一個服務調用者,查詢訂單的時候要查詢用戶user-service
:用戶服務,是一個服務提供者,對外暴露查詢用戶的接口
啟動以后,訪問localhost:10086
即可查看到Eureka的控制臺,相對于Nacos來說簡陋了很多:
微服務引入Eureka的方式也極其簡單,分兩步:
- 引入
eureka-client
依賴 - 配置
eureka
地址
接下來就是編寫OpenFeign的客戶端了,怎么樣?是不是跟Nacos用起來基本一致。
2.4.Eureka和Nacos對比
Eureka和Nacos都能起到注冊中心的作用,用法基本類似。但還是有一些區別的,例如:
- Nacos支持配置管理,而Eureka則不支持。
而且服務注冊發現上也有區別,我們來做一個實驗:
我們停止user-service
服務,然后觀察Eureka控制臺,你會發現很長一段時間過去后,Eureka服務依然沒有察覺user-service
的異常狀態。
這與Eureka的健康檢測機制有關。在Eureka中,健康檢測的原理如下:
- 微服務啟動時注冊信息到Eureka,這點與Nacos一致。
- 微服務每隔30秒向Eureka發送心跳請求,報告自己的健康狀態。Nacos中默認是5秒一次。
- Eureka如果90秒未收到心跳,則認為服務疑似故障,可能被剔除。Nacos中則是15秒超時,30秒剔除。
- Eureka如果發現超過85%比例的服務都心跳異常,會認為是自己的網絡異常,暫停剔除服務的功能。
- Eureka每隔60秒執行一次服務檢測和清理任務;Nacos是每隔5秒執行一次。
綜上,你會發現Eureka是盡量不剔除服務,避免“誤殺”,寧可放過一千,也不錯殺一個。這就導致當服務真的出現故障時,遲遲不會被剔除,給服務的調用者帶來困擾。
不僅如此,當Eureka發現服務宕機并從服務列表中剔除以后,并不會將服務列表的變更消息推送給所有微服務。而是等待微服務自己來拉取時發現服務列表的變化。而微服務每隔30秒才會去Eureka更新一次服務列表,進一步推遲了服務宕機時被發現的時間。
而Nacos中微服務除了自己定時去Nacos中拉取服務列表以外,Nacos還會在服務列表變更時主動推送最新的服務列表給所有的訂閱者。
綜上,Eureka和Nacos的相似點有:
- 都支持服務注冊發現功能
- 都有基于心跳的健康監測功能
- 都支持集群,集群間數據同步默認是AP模式,即最全高可用性
Eureka和Nacos的區別有:
- Eureka的心跳是30秒一次,Nacos則是5秒一次
- Eureka如果90秒未收到心跳,則認為服務疑似故障,可能被剔除。Nacos中則是15秒超時,30秒剔除。
- Eureka每隔60秒執行一次服務檢測和清理任務;Nacos是每隔5秒執行一次。
- Eureka只能等微服務自己每隔30秒更新一次服務列表;Nacos即有定時更新,也有在服務變更時的廣播推送
- Eureka僅有注冊中心功能,而Nacos同時支持注冊中心、配置管理
- Eureka和Nacos都支持集群,而且默認都是AP模式
3.遠程調用
我們知道微服務間遠程調用都是有OpenFeign幫我們完成的,甚至幫我們實現了服務列表之間的負載均衡。但具體負載均衡的規則是什么呢?何時做的負載均衡呢?
接下來我們一起來分析一下。
3.1.負載均衡原理
在SpringCloud的早期版本中,負載均衡都是有Netflix公司開源的Ribbon組件來實現的,甚至Ribbon被直接集成到了Eureka-client和Nacos-Discovery中。
但是自SpringCloud2020版本開始,已經棄用Ribbon,改用Spring自己開源的Spring Cloud LoadBalancer了,我們使用的OpenFeign的也已經與其整合。
接下來我們就通過源碼分析,來看看OpenFeign底層是如何實現負載均衡功能的。
3.1.1.源碼跟蹤
要弄清楚OpenFeign的負載均衡原理,最佳的辦法肯定是從FeignClient的請求流程入手。
首先,我們在com.hmall.cart.service.impl.CartServiceImpl
中的queryMyCarts
方法中打一個斷點。然后在swagger頁面請求購物車列表接口。
進入斷點后,觀察ItemClient
這個接口:
你會發現ItemClient是一個代理對象,而代理的處理器則是SentinelInvocationHandler
。這是因為我們項目中引入了Sentinel
導致。
我們進入SentinelInvocationHandler
類中的invoke
方法看看:
可以看到這里是先獲取被代理的方法的處理器MethodHandler
,接著,Sentinel就會開啟對簇點資源的監控:
開啟Sentinel的簇點資源監控后,就可以調用處理器了,我們嘗試跟入,會發現有兩種實現:
這其實就是OpenFeign遠程調用的處理器了。繼續跟入會進入SynchronousMethodHandler
這個實現類:
在上述方法中,會循環嘗試調用executeAndDecode()
方法,直到成功或者是重試次數達到Retryer中配置的上限。
我們繼續跟入executeAndDecode()
方法:
executeAndDecode()
方法最終會利用client
去調用execute()
方法,發起遠程調用。
這里的client的類型是feign.Client
接口,其下有很多實現類:
由于我們項目中整合了seata,所以這里client對象的類型是SeataFeignBlockingLoadBalancerClient
,內部實現如下:
這里直接調用了其父類,也就是FeignBlockingLoadBalancerClient
的execute
方法,來看一下:
整段代碼中核心的有4步:
- 從請求的
URI
中找出serviceId
- 利用
loadBalancerClient
,根據serviceId
做負載均衡,選出一個實例ServiceInstance
- 用選中的
ServiceInstance
的ip
和port
替代serviceId
,重構URI
- 向真正的URI發送請求
所以負載均衡的關鍵就是這里的loadBalancerClient,類型是org.springframework.cloud.client.loadbalancer.LoadBalancerClient
,這是Spring-Cloud-Common
模塊中定義的接口,只有一個實現類:
而這里的org.springframework.cloud.client.loadbalancer.BlockingLoadBalancerClient
正是Spring-Cloud-LoadBalancer
模塊下的一個類:
我們繼續跟入其BlockingLoadBalancerClient#choose()
方法:
圖中代碼的核心邏輯如下:
- 根據serviceId找到這個服務采用的負載均衡器(
ReactiveLoadBalancer
),也就是說我們可以給每個服務配不同的負載均衡算法。 - 利用負載均衡器(
ReactiveLoadBalancer
)中的負載均衡算法,選出一個服務實例
ReactiveLoadBalancer
是Spring-Cloud-Common
組件中定義的負載均衡器接口規范,而Spring-Cloud-Loadbalancer
組件給出了兩個實現:
默認的實現是RoundRobinLoadBalancer
,即輪詢負載均衡器。負載均衡器的核心邏輯如下:
核心流程就是兩步:
- 利用
ServiceInstanceListSupplier#get()
方法拉取服務的實例列表,這一步是采用響應式編程 - 利用本類,也就是
RoundRobinLoadBalancer
的getInstanceResponse()
方法挑選一個實例,這里采用了輪詢算法來挑選。
這里的ServiceInstanceListSupplier有很多實現:
其中CachingServiceInstanceListSupplier采用了裝飾模式,加了服務實例列表緩存,避免每次都要去注冊中心拉取服務實例列表。而其內部是基于DiscoveryClientServiceInstanceListSupplier
來實現的。
在這個類的構造函數中,就會異步的基于DiscoveryClient去拉取服務的實例列表:
3.1.2.流程梳理
根據之前的分析,我們會發現Spring在整合OpenFeign的時候,實現了org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient
類,其中定義了OpenFeign發起遠程調用的核心流程。也就是四步:
- 獲取請求中的
serviceId
- 根據
serviceId
負載均衡,找出一個可用的服務實例 - 利用服務實例的
ip
和port
信息重構url - 向真正的url發起請求
而具體的負載均衡則是不是由OpenFeign
組件負責。而是分成了負載均衡的接口規范,以及負載均衡的具體實現兩部分。
負載均衡的接口規范是定義在Spring-Cloud-Common
模塊中,包含下面的接口:
LoadBalancerClient
:負載均衡客戶端,職責是根據serviceId最終負載均衡,選出一個服務實例ReactiveLoadBalancer
:負載均衡器,負責具體的負載均衡算法
OpenFeign的負載均衡是基于Spring-Cloud-Common
模塊中的負載均衡規則接口,并沒有寫死具體實現。這就意味著以后還可以拓展其它各種負載均衡的實現。
不過目前SpringCloud
中只有Spring-Cloud-Loadbalancer
這一種實現。
Spring-Cloud-Loadbalancer
模塊中,實現了Spring-Cloud-Common
模塊的相關接口,具體如下:
BlockingLoadBalancerClient
:實現了LoadBalancerClient
,會根據serviceId選出負載均衡器并調用其算法實現負載均衡。RoundRobinLoadBalancer
:基于輪詢算法實現了ReactiveLoadBalancer
RandomLoadBalancer
:基于隨機算法實現了ReactiveLoadBalancer
,
這樣一來,整體思路就非常清楚了,流程圖如下:
暫時無法在飛書文檔外展示此內容
3.2.NacosRule
之前分析源碼的時候我們發現負載均衡的算法是有ReactiveLoadBalancer
來定義的,我們發現它的實現類有三個:
其中RoundRobinLoadBalancer
和RandomLoadBalancer
是由Spring-Cloud-Loadbalancer
模塊提供的,而NacosLoadBalancer
則是由Nacos-Discorvery
模塊提供的。
默認采用的負載均衡策略是RoundRobinLoadBalancer
,那如果我們要切換負載均衡策略該怎么辦?
3.2.1.修改負載均衡策略
查看源碼會發現,Spring-Cloud-Loadbalancer
模塊中有一個自動配置類:
其中定義了默認的負載均衡器:
這個Bean上添加了@ConditionalOnMissingBean
注解,也就是說如果我們自定義了這個類型的bean,則負載均衡的策略就會被改變。
我們在hm-cart
模塊中的添加一個配置類:
代碼如下:
package com.hmall.cart.config;import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;public class OpenFeignConfig {@Beanpublic ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, NacosDiscoveryProperties properties,LoadBalancerClientFactory loadBalancerClientFactory) {String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);return new NacosLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, properties);}}
注意:
這個配置類千萬不要加@Configuration
注解,也不要被SpringBootApplication掃描到。
由于這個OpenFeignConfig沒有加@Configuration
注解,也就沒有被Spring加載,因此是不會生效的。接下來,我們要在啟動類上通過注解來聲明這個配置。
有兩種做法:
- 全局配置:對所有服務生效
@LoadBalancerClients(defaultConfiguration = OpenFeignConfig.class)
- 局部配置:只對某個服務生效
@LoadBalancerClients({@LoadBalancerClient(value = "item-service", configuration = OpenFeignConfig.class)
})
我們選擇全局配置:
DEBUG重啟后測試,會發現負載均衡器的類型確實切換成功:
3.2.2.集群優先
RoundRobinLoadBalancer
是輪詢算法,RandomLoadBalancer
是隨機算法,那么NacosLoadBalancer
是什么負載均衡算法呢?
我們通過源碼來分析一下,先看第一部分:
這部分代碼的大概流程如下:
- 通過
ServiceInstanceListSupplier
獲取服務實例列表 - 獲取
NacosDiscoveryProperties
中的clusterName
,也就是yml文件中的配置,代表當前服務實例所在集群信息(參考2.2
小節,分級模型) - 然后利用stream的filter過濾找到被調用的服務實例中與當前服務實例
clusterName
一致的。簡單來說就是服務調用者與服務提供者要在一個集群
為什么?
假如我現在有兩個機房,都部署有item-service
和cart-service
服務:
假如這些服務實例全部都注冊到了同一個Nacos。現在,杭州機房的cart-service
要調用item-service
,會拉取到所有機房的item-service的實例。調用時會出現兩種情況:
- 直接調用當前機房的
item-service
- 調用其它機房的
item-service
本機房調用幾乎沒有網絡延遲,速度比較快。而跨機房調用,如果兩個機房相距很遠,會存在較大的網絡延遲。因此,我們應該盡可能避免跨機房調用,優先本地集群調用:
現在的情況是這樣的:
cart-service
所在集群是default
item-service
的8081、8083所在集群的default
item-service
的8084所在集群是BJ
cart-service
訪問item-service
時,應該優先訪問8081和8082,我們重啟cart-service
,測試一下:
可以看到原本是3個實例,經過篩選后還剩下2個實例。
查看Debug控制臺:
同集群的實例還剩下兩個,接下來就需要做負載均衡了,具體用的是什么算法呢?
3.2.3.權重配置
我們繼續跟蹤NacosLoadBalancer
源碼:
那么問題來了, 這個權重是怎么配的呢?
我們打開nacos控制臺,進入item-service
的服務詳情頁,可以看到每個實例后面都有一個編輯按鈕:
點擊,可以看到一個編輯表單:
我們將這里的權重修改為5:
訪問10次購物車接口,可以發現大多數請求都訪問到了8083這個實例。
4.服務保護
在SpringCloud的早期版本中采用的服務保護技術叫做Hystix
,不過后來被淘汰,替換為Spring Cloud Circuit Breaker
,其底層實現可以是Spring Retry
和Resilience4J
。
不過在國內使用較多還是SpringCloudAlibaba
中的Sentinel
組件。
接下來,我們就分析一下Sentinel
組件的一些基本實現原理以及它與Hystix
的差異。
4.1.線程隔離
首先我們來看下線程隔離功能,無論是Hystix還是Sentinel都支持線程隔離。不過其實現方式不同。
線程隔離有兩種方式實現:
- 線程池隔離:給每個服務調用業務分配一個線程池,利用線程池本身實現隔離效果
- 信號量隔離:不創建線程池,而是計數器模式,記錄業務使用的線程數量,達到信號量上限時,禁止新的請求
如圖:
兩者的優缺點如下:
Sentinel的線程隔離就是基于信號量隔離實現的,而Hystix兩種都支持,但默認是基于線程池隔離。
4.2.滑動窗口算法
在熔斷功能中,需要統計異常請求或慢請求比例,也就是計數。在限流的時候,要統計每秒鐘的QPS,同樣是計數。可見計數算法在熔斷限流中的應用非常多。sentinel中采用的計數器算法就是滑動窗口計數算法。
4.2.1.固定窗口計數
要了解滑動窗口計數算法,我們必須先知道固定窗口計數算法,其基本原理如圖:
說明:
- 將時間劃分為多個窗口,窗口時間跨度稱為
Interval
,本例中為1000ms; - 每個窗口維護1個計數器,每有1次請求就將計數器
+1
。限流就是設置計數器閾值,本例為3,圖中紅線標記 - 如果計數器超過了限流閾值,則超出閾值的請求都被丟棄。
示例:
說明:
- 第1、2秒,請求數量都小于3,沒問題
- 第3秒,請求數量為5,超過閾值,超出的請求被拒絕
但是我們考慮一種特殊場景,如圖:
說明:
- 假如在第5、6秒,請求數量都為3,沒有超過閾值,全部放行
- 但是,如果第5秒的三次請求都是在4.5~5秒之間進來;第6秒的請求是在5~5.5之間進來。那么從第4.5~5.之間就有6次請求!也就是說每秒的QPS達到了6,遠超閾值。
這就是固定窗口計數算法的問題,它只能統計當前某1個時間窗的請求數量是否到達閾值,無法結合前后的時間窗的數據做綜合統計。
因此,我們就需要滑動時間窗口算法來解決。
4.2.2.滑動窗口計數
固定時間窗口算法中窗口有很多,其跨度和位置是與時間區間綁定,因此是很多固定不動的窗口。而滑動時間窗口算法中只包含1個固定跨度的窗口,但窗口是可移動動的,與時間區間無關。
具體規則如下:
- 窗口時間跨度
Interval
大小固定,例如1秒 - 時間區間跨度為
Interval / n
,例如n=2,則時間區間跨度為500ms - 窗口會隨著當前請求所在時間
currentTime
移動,窗口范圍從currentTime-Interval
時刻之后的第一個時區開始,到currentTime
所在時區結束。
如圖所示:
限流閾值依然為3,綠色小塊就是請求,上面的數字是其currentTime
值。
- 在第1300ms時接收到一個請求,其所在時區就是1000~1500
- 按照規則,currentTime-Interval值為300ms,300ms之后的第一個時區是500~1000,因此窗口范圍包含兩個時區:500~1000、1000~1500,也就是粉紅色方框部分
- 統計窗口內的請求總數,發現是3,未達到上限。
若第1400ms又來一個請求,會落在1000~1500時區,雖然該時區請求總數是3,但滑動窗口內總數已經達到4,因此該請求會被拒絕:
假如第1600ms又來的一個請求,處于1500~2000時區,根據算法,滑動窗口位置應該是1000~1500和1500~2000這兩個時區,也就是向后移動:
這就是滑動窗口計數的原理,解決了我們之前所說的問題。而且滑動窗口內劃分的時區越多,這種統計就越準確。
4.3.令牌桶算法
限流的另一種常見算法是令牌桶算法。Sentinel中的熱點參數限流正是基于令牌桶算法實現的。其基本思路如圖:
說明:
- 以固定的速率生成令牌,存入令牌桶中,如果令牌桶滿了以后,多余令牌丟棄
- 請求進入后,必須先嘗試從桶中獲取令牌,獲取到令牌后才可以被處理
- 如果令牌桶中沒有令牌,則請求等待或丟棄
基于令牌桶算法,每秒產生的令牌數量基本就是QPS上限。
當然也有例外情況,例如:
- 某一秒令牌桶中產生了很多令牌,達到令牌桶上限N,緩存在令牌桶中,但是這一秒沒有請求進入。
- 下一秒的前半秒涌入了超過2N個請求,之前緩存的令牌桶的令牌耗盡,同時這一秒又生成了N個令牌,于是總共放行了2N個請求。超出了我們設定的QPS閾值。
因此,在使用令牌桶算法時,盡量不要將令牌上限設定到服務能承受的QPS上限。而是預留一定的波動空間,這樣我們才能應對突發流量。
4.4.漏桶算法
漏桶算法與令牌桶相似,但在設計上更適合應對并發波動較大的場景,以解決令牌桶中的問題。
簡單來說就是請求到達后不是直接處理,而是先放入一個隊列。而后以固定的速率從隊列中取出并處理請求。之所以叫漏桶算法,就是把請求看做水,隊列看做是一個漏了的桶。
如圖:
說明:
- 將每個請求視作"水滴"放入"漏桶"進行存儲;
- "漏桶"以固定速率向外"漏"出請求來執行,如果"漏桶"空了則停止"漏水”;
- 如果"漏桶"滿了則多余的"水滴"會被直接丟棄。
漏桶的優勢就是流量整型,桶就像是一個大壩,請求就是水。并發量不斷波動,就如圖水流時大時小,但都會被大壩攔住。而后大壩按照固定的速度放水,避免下游被洪水淹沒。
因此,不管并發量如何波動,經過漏桶處理后的請求一定是相對平滑的曲線:
sentinel中的限流中的排隊等待功能正是基于漏桶算法實現的。
5.作業
嘗試用自己的語言回答下列面試題:
- SpringCloud有哪些常用組件?分別是什么作用?
- 服務注冊發現的基本流程是怎樣的?
- Eureka和Nacos有哪些區別?
- Nacos的分級存儲模型是什么意思?
- OpenFeign是如何實現負載均衡的?
- 什么是服務雪崩,常見的解決方案有哪些?
- Hystix和Sentinel有什么區別和聯系?
- 限流的常見算法有哪些?
- 什么是CAP理論和BASE思想?
- 項目中碰到過分布式事務問題嗎?怎么解決的?
- AT模式如何解決臟讀和臟寫問題的?
- TCC模式與AT模式對比,有哪些優缺點
- RabbitMQ是如何確保消息的可靠性的?
- RabbitMQ是如何解決消息堆積問題的?