前言
在微服務中,對服務進行拆分之後,必然會帶來微服務之間的通訊需求,而每個微服務為了保證高可用性,又會去部署叢集,那麼面對一個叢集微服務進行通訊的時候,如何進行負載均衡也是必然需要考慮的問題。那麼有需求自然就有供給,由此一大批優秀的開源的負載均衡元件應運而生,本文就讓我們一起來分析一下 Spring Cloud Netflix
套件中的負載均衡元件 Ribbon
。
一個問題引發的思考
首先我們來看一個問題,假如說我們現在有兩個微服務,一個 user-center
,一個 user-order
,我現在需要在 user-center
服務中呼叫 user-order
服務的一個介面。
這時候我們可以使用 HttpClient
,RestTemplate
等發起 http
請求,user-center
服務埠為 8001
,如下圖所示:
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("/order")
public String queryOrder(){
return restTemplate.getForObject("http://localhost:8002/order/query",String.class);
}
}
而 user-order
服務中只是簡單的定義了一個介面,user-order
服務埠為 8002
:
@RestController
@RequestMapping(value = "/order")
public class UserOrderController {
@GetMapping(value = "/query")
public String queryAllOrder(){
return "all orders";
}
}
這時候只需要將兩個服務啟動,訪問 http://localhost:8001/user/order
就可以獲取到所有的訂單資訊。
可以看到,這樣是可以在兩個微服務之間進行通訊的,但是,假如說我們的 user-order
服務是一個叢集呢?這時候怎麼訪問呢?因為 user-order
服務已經是叢集,所以必然需要一種演算法來決定應該請求到哪個 user-order
服務中,最簡單的那麼自然就是隨機或者輪詢機制,輪詢或者隨機其實就是簡單的負載均衡演算法,而 Ribbon
就是用來實現負載均衡的一個元件,其內部支援輪詢,等演算法。
Ribbon的簡單使用
接下來我們看看 Ribbon
的簡單使用。
- 首先改造
user-order
服務,在user-order
服務中定義一個服務名配置:
spring.application.name=user-order-service
- 將
user-order
服務中的UserOrderController
稍微改造一下,新增一個埠的輸出來區分:
@RestController
@RequestMapping(value = "/order")
public class UserOrderController {
@Value("${server.port}")
private int serverPort;
@GetMapping(value = "/query")
public String queryAllOrder(){
return "訂單來自:" + serverPort;
}
}
-
通過 VM 引數
-Dserver.port=8002
和-Dserver.port=8003
分別來啟動兩個user-order
服務。 -
接下來改造
user-center
服務,在user-center
服務中引入Ribbon
的相關依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
user-center
服務中新增一個Ribbon
相關配置,列舉出需要訪問的所有服務:
user-order-service.ribbon.listOfServers=\
localhost:8002,localhost:8003
- 對
user-center
服務中的UserController
進行改造:
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("/order")
public String queryOrder(){
//獲取一個 user-order 服務
ServiceInstance serviceInstance = loadBalancerClient.choose("user-order-service");
String url = String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()) + "/order/query";
return restTemplate.getForObject(url,String.class);
}
}
這時候我們再次訪問 http://localhost:8001/user/order
就可以看到請求的 user-order
服務會在 8002
和 8003
之間進行切換。
Ribbon 原理分析
看了上面 Ribbon
的使用示例,會不會覺得有點麻煩,每次還需要自己去獲取 ip
和埠,然後格式化 url
,但是其實實際開發過程中我們並不會通過這麼原始的方式來編寫程式碼,接下來我們再對上面的示例進行一番改造:
@RestController
@RequestMapping(value = "/user")
public class UserController3 {
@Autowired
private RestTemplate restTemplate;
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("/order")
public String queryOrder(){
return restTemplate.getForObject("http://user-order-service/order/query",String.class);
}
}
在這個示例中,主要就是一個關鍵主鍵起了作用:@LoadBalanced。
@LoadBalanced 註解
進入 @LoadBalanced
註解中,我們可以看到,這個註解其實沒有任何邏輯,只是加了一個 @Qualifier
註解:
這個註解大家應該很熟悉了,常用語同一個 Bean
有多個不同名稱注入的場景。
@Qualifier註解
下面我們通過一個例子來演示一下 Qualifier
註解的用法。
新建一個空的 TestDemo
類,並新增一個 TestConfiguration
類來建立不同名稱的 TestDemo
:
@Configuration
public class TestConfiguration {
@Bean("testDemo1")
public TestDemo testDemo(){
return new TestDemo();
}
@Bean("testDemo2")
public TestDemo testDemo2(){
return new TestDemo();
}
}
這時候我們如果需要注入 TestDemo
,那麼有很多種辦法,具體的使用就需要看業務需要來決定。
- 方法一:直接使用
@Autowired
,並使用List
集合來接收Bean
,這樣所有TestDemo
型別的Bean
都會被注入。 - 方法二:通過使用
@Resource(name = "testDemo1")
註解來指定名稱,這樣就可以只注入一個Bean
。 - 方法三:通過使用
@Resource
和@Qualifier(value = "testDemo1")
來指定一個Bean
,其實這種方式和方法二的效果基本一致。 - 方法四:使用
@Autowired
和@Qualifier
註解來注入,不指定任何名稱,如下所示:
@RestController
@RequestMapping(value = "/test")
public class TestQualifierController {
@Autowired(required = false)
@Qualifier
private List<TestDemo> testDemo = Collections.emptyList();
@GetMapping("/all")
public String allDemo(){
for (TestDemo testDemo : testDemos){
System.out.println(testDemo.toString());
}
return "succ";
}
}
這時候執行之後我們發現不會有任何 Bean
被注入到集合中,這是因為當使用這種方式來注入時,Spring
會認為當前只需要注入被 @Qualifier
註解標記的 Bean
,而我們上面定義的兩個 TestDemo
都沒有被 @Qualifier
修飾。
這時候,我們只需要在 TestConfiguration
稍微改造,在 TestDemo
的定義上加上 @Qualifier
修飾即可:
@Configuration
public class TestConfiguration {
@Bean("testDemo1")
@Qualifier
public TestDemo testDemo(){
return new TestDemo();
}
@Bean("testDemo2")
@Qualifier
public TestDemo testDemo2(){
return new TestDemo();
}
}
這時候再去執行,就會發現,testDemo1
和 testDemo2
都會被注入。
LoadBalancerAutoConfiguration 自動裝配
SpringCloud
是基於 SpringBoot
實現的,所以我們常用的這些分散式元件都會基於 SpringBoot
自動裝配來實現,我們進入 LoadBalancerAutoConfiguration
自動裝配類可以看到,RestTemplate
的注入加上了 @LoadBalanced
,這就是為什麼我們前面的例子中加上了 @LoadBalanced
就能被自動注入的原因:
RestTemplateCustomizer
上面我們看到,RestTemplate
被包裝成為了 RestTemplateCustomizer
,而 RestTemplateCustomizer
的注入如下:
可以看到這裡面加入了一個攔截器 LoadBalancerInterceptor
,事實上即使不看這裡,我們也可以猜測到,我們直接使用服務名就可以進行通訊的原因必然是底層有攔截器對其進行轉換成 ip
形式,並在底層進行負載均衡選擇合適的服務進行通訊。
LoadBalancerInterceptor
LoadBalancerInterceptor
是 Ribbon
中預設的一個攔截器,所以當我們呼叫 RestTemplate
的 getObject
方法時,必然會呼叫攔截器中的方法。
從原始碼中可以看到,LoadBalancerInterceptor
中只有一個 intercept()
方法:
RibbonLoadBalancerClient#execute
繼續跟進 execute
方法會進入到 RibbonLoadBalancerClient
類(由 RibbonAutoConfiguration
自動裝配類初始化)中:
這個方法中也比較好理解,首先獲取一個負載均衡器,然後再通過 getServer
方法獲取一個指定的服務,也就是當我們有多個服務時,到這裡就會選出一個服務進行通訊。
進入 getServer
方法:
我們看到,最終會呼叫 ILoadBalancer
中的 chooseServer
方法,而 ILoadBalancer
是一個頂層介面,這時候具體會呼叫哪個實現類那麼就需要先來看一下類圖:
這裡直接看類圖也無法看出到底會呼叫哪一個,但是不論呼叫哪一個,我們猜測他肯定會有一個地方去初始化這個類,而在 Spring
當中一般就是自動裝配類中初始化或者 Configuration
中初始化,而 ILoadBalancer
正是在 RibbonClientConfiguration
類中被載入的:
ZoneAwareLoadBalancer 負載均衡器
ZoneAwareLoadBalancer 的初始化會呼叫其父類 DynamicServerListLoadBalancer
進行初始化,然後會呼叫 restOfInit
方法進行所有服務的初始化。
如何獲取所有服務
使用 Ribbon
後,我們通訊時並沒有指定某一個 ip
和埠,而是通過服務名來呼叫服務,那麼這個服務名就可能對應多個真正的服務,那麼我們就必然需要先獲取到所有服務的 ip
和埠等資訊,然後才能進行負載均衡處理。
獲取所有服務有兩種方式:
- 從配置檔案獲取
- 從
Eureka
註冊中心獲取(需要引入註冊中心)。
初始化服務的方式是通過啟動一個 Scheduled
定時任務來實現的,預設就是 30s
更新一次,其實在很多原始碼中都是通過這種方式來定時更新的,因為原始碼要考慮的使用的簡單性所以不太可能引入一個第三方中介軟體來實現定時器。
具體的原始碼如下所示:enableAndInitLearnNewServersFeature()
方法啟動的定時任務最終仍然你是呼叫 updateListOfServers()
方法來更新服務。
最終在獲取到服務之後會呼叫父類 BaseLoadBalancer
中的將所有服務設定到 allServerList
集合中(BaseLoadBalancer
類中維護了一些負載均衡需要使用到的服務相關資訊)。
如何判斷服務是否可用
當我們獲取到配置檔案(或者 Eureka
註冊中心)中的所有服務,那麼這時候能直接執行負載均衡策略進行服務分發嗎?顯然是不能的,因為已經配置好的服務可能會當機(下線),從而導致服務不可用,所以在 BaseLoadBalancer
中除了有一個 allServerList
集合來維護所有伺服器,還有一個集合 upServerList
用來維護可用服務集合,那麼如何判斷一個服務是否可用呢?答案就是通過心跳檢測來判斷一個服務是否可用。
心跳檢測 Task
在講心跳檢測之前,我們先看一下 BaseLoadBalancer
中的 setServersList
方法,有一段邏輯比較重要:
這段邏輯我們看到,預設情況下,如果 Ping
的策略是 DummyPing
,那麼預設 upServerList = allServerList
,而實際上,假如我們沒有進行進行特殊配置,其實預設的就是 DummyPing
,這也是在 RibbonClientConfiguration
類中被載入的:
在 BaseLoadBalancer
初始化過程中,也會啟動一個 Scheduled
定時任務去定時更新任務,最終和 forceQuickPing()
方法一樣,呼叫一個預設策略來觸發心跳檢測,而預設策略就是 DummyPing
,也就是預設所有服務都是可用的。
雖然預設不執行真正的心跳檢測操作,但是 Netflix
中提供了 PingUrl
等其他策略,PingUrl
其實就是發起一個 http
請求,如果有響應就認為服務可用,沒響應就認為服務不可用。
修改心跳檢測策略可以通過如下配置切換(user-order-service
為客戶端的服務名),既然是可配置的,那麼也可以自己實現一個策略,只需要實現 IPing
介面即可。
user-order-service.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.PingUrl
Ribbon 的負載均衡演算法
當獲取到可用服務之後,那麼最後應該選擇哪一個服務呢?這就需要使用到負載均衡策略,在 Ribbon
中,可以通過配置修改,也可以自定義負載均衡策略(實現 IRule
介面)。
- RandomRule:隨機演算法
- RoundRobinRule:輪詢演算法
- ZoneAvoidanceRule:結合分割槽統計資訊篩選出合適的分割槽(預設的負載均衡演算法)
- RetryRule:在
deadline
時間內,如果請求不成功,則重新發起請求知道找到一個可用的服務。 - WeightedResponseTimeRule:根據伺服器的響應時間計算權重值,伺服器響應時間越長,這個伺服器的權重就越小,會有定時任務對權重值進行更新。
- AvailabilityFilteringRule:過濾掉短路(連續
3
次連線失敗)的服務和高併發的服務。 - BestAvailableRule:選擇併發數最低的伺服器
負載均衡演算法可通過以下配置進行修改:
user-order-service.ribbon.NFLoadBalancerRuleClassName=Rule規則的類名
總結
本文主要講述了微服務體系下的 Spring Cloud Netflix
套件中 Ribbon
的使用,並結合部分原始碼講述了 Ribbon
的底層原理,重點講述了 Ribbon
中是如何獲取服務以及如何判定一個服務是否可用,最後也介紹了 Ribbon
中預設提供的 7
種負載均衡策略。