這是SpringCloud實戰系列中第三篇文章,了解前面第兩篇文章更有助於更好理解本文內容:
①SpringCloud 實戰:引入Eureka組件,完善服務治理
②SpringCloud 實戰:引入Feign組件,發起服務間調用
簡介
Ribbon 是由 Netflix 發布的一個客戶端負載均衡器,它提供了對 HTTP 和 TCP 客戶端行為的大量控制。Ribbon 可以基於某些負載均衡的算法,自動為客戶端選擇發起理論最優的網絡請求。常見的負載均衡算法有:輪詢,隨機,哈希,加權輪詢,加權隨機等。
客戶端負載均衡的意思就是發起網絡請求的端根據自己的網絡請求情況來做相應的負載均衡策略,與之相對的非客戶端負載均衡就有比如硬件F5、軟件Nginx,它們更多是介於消費者和提供者之間的,並非客戶端。
改造eureka-provider項目
在使用之前我們先把第二節里面的 eureka-provider 項目改造一下,在HelloController 里面新增一個接口,輸出自己項目的端口信息,用於區別驗證待會兒客戶端負載均衡時所調用的服務。
-
新增接口方法,返回自己的端口號信息:
@Controller public class HelloController{ @Value("${server.port}") private int serverPort; ... @ResponseBody @GetMapping("queryPort") public String queryPort(){ return "hei, jinglingwang, my server port is:"+serverPort; } }
-
分別以8082,8083,8084端口啟動該項目:eureka-provider
下圖是 IDEA 快速啟動三個不同端口項目方法截圖,當然你也可以用其他辦法 -
然后啟動,訪問三個接口測試一下是否正常返回了對應端口
至此,服務提供者的接口准備工作就做好了。
新建Ribbon-Client 項目
我們使用 Spring Initializr 生成SpringCloud項目基礎框架,然后修改pom.xml里面的SpringBoot和SpringCloud的版本,對應版本修改請求如下:
...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version> <!--修改了版本jinglingwang.cn-->
<relativePath/> <!-- lookup parent from repository -->
</parent>
... 略
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR4</spring-cloud.version><!--修改了版本-->
</properties>
... 略
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
為什么要單獨修改版本呢?因為從 Spring Cloud Hoxton.M2 版本開始,Spring Cloud 已經不再默認使用Ribbon來做負載均衡了,而是使用 spring-cloud-starter-loadbalancer
替代。所以我們在使用 Spring Initializr 生成項目框架的時,如果使用最新版本Spring Cloud將不再提供Ribbon相關的組件。需要我們自己引入或者使用低一點的版本。
之后就是在ribbon-client項目引入eureka-client依賴和openfeign的依賴,這個過程省略,如果不會的話請看前兩篇文章。
Ribbon 的三種使用方式
我們在新建的ribbon-client項目里面來使用三種方式調用eureka-provider
的queryPort接口,因為eureka-provider服務啟動了三個節點,到時候只要觀察三種方式的響應結果,就可以判斷負載均衡是否有生效。
一、使用原生API
直接使用LoadBalancerClient來獲得對應的實例,然后發起URL請求,編寫對應的RibbonController:
@RestController
public class RibbonController{
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping("ribbonApi")
public String ribbonApi() throws Exception{
ServiceInstance instance = loadBalancer.choose("eureka-provider");
System.out.println(instance.getUri());
URL url = new URL("http://localhost:" + instance.getPort() + "/queryPort");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
InputStream inputStream = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
StringBuffer buffer = new StringBuffer();
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
reader.close();
return Observable.just(buffer.toString()).toBlocking().first();
}
}
啟動Ribbon-Client服務,訪問http://localhost:7071/ribbonApi 接口,多次刷新接口發現采用的是輪詢方式,運行效果圖如下:
二、結合RestTemplate使用
使用 RestTemplate 的話,我們只需要再結合@LoadBalanced注解一起使用即可:
@Configuration
public class RestTemplateConfig{
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
編寫RibbonController:
@RestController
public class RibbonController{
@Autowired
private RestTemplate restTemplate;
@GetMapping("queryPortByRest")
public String queryPortByRest(){
return restTemplate.getForEntity("http://eureka-provider/queryPort",String.class).getBody();
}
}
啟動ribbon-client服務,訪問http://localhost:7071/queryPortByRest 接口,多次刷新接口發現采用的也是輪詢方式,運行效果圖如下:
三、結合Feign使用
新建一個Feign:
@FeignClient(value = "eureka-provider")
public interface ProviderFeign{
/**
* 調用服務提供方,其中會返回服務提供者的端口信息
* @return jinglingwang.cn
*/
@RequestMapping("/queryPort")
String queryPort();
}
編寫調用接口:
@RestController
public class RibbonController{
...略
@Autowired
private ProviderFeign providerFeign;
...
@GetMapping("queryPort")
public String queryPort(){
// 通過feign ribbon-client 調用 eureka-provider
return providerFeign.queryPort();
}
}
啟動ribbon-client服務,訪問 http://localhost:7071/queryPort 接口,多次刷新接口發現采用的也是輪詢方式,運行效果圖如下:
自定義Ribbon配置
為指定的客戶端自定義負載均衡規則
在配置之前先做一點准備工作,我們把之前的服務eureka-provider再起3個節點,啟動之前把端口改為8085、8086、8087,三個節點的服務名改為eureka-provider-temp。這樣做的目的是等會兒我們新建一個Feign,但是名字和之前的區分開,相當於兩個不同的服務,並且都是多節點的。
以上准備工作做完之后你會在IDEA中看到如下圖的6個服務:
在注冊中心也可以觀察到2個不同的服務,一共6個節點:
eureka-provide 和 eureka-provide-temp 他們唯一的區別就是服務名不一樣、端口不一樣。
JavaBean的配置方式
現在開始為Feign配置ribbon:
-
新建一個Feign,命名為:ProviderTempFeign
@FeignClient(value = "eureka-provider-temp") public interface ProviderTempFeign{ @RequestMapping("/queryPort") String queryPort(); }
-
使用JAVA Bean的方式定義配置項
public class ProviderTempConfiguration{ @Bean public IRule ribbonRule(){ System.out.println("new ProviderTempConfiguration RandomRule"); return new RandomRule(); // 定義一個隨機的算法 } @Bean public IPing ribbonPing() { // return new PingUrl(); return new NoOpPing(); } }
-
使用注解
@RibbonClient
配置負載均衡客戶端:@RibbonClient(name = "eureka-provider-temp",configuration = ProviderTempConfiguration.class) public class ProviderTempRibbonClient{ }
-
在Controller新增一個接口,來調用新增Feign(eureka-provider-temp)的方法
@GetMapping("queryTempPort") public String queryTempPort(){ return providerTempFeign.queryPort(); }
-
再為另一個Feign(
eureka-provider
)也配置一下ribbon,對外接口還是上面已經寫好了public class ProviderConfiguration{ @Bean public IRule ribbonRule(){ System.out.println("new ProviderConfiguration BestAvailableRule"); return new BestAvailableRule(); // 選擇的最佳策略 } @Bean public IPing ribbonPing() { // return new PingUrl(); return new NoOpPing(); } } @RibbonClient(name = "eureka-provider",configuration = ProviderConfiguration.class) public class ProviderRibbonClient{ }
-
啟動服務之后分別訪問兩個接口(http://localhost:7071/queryPort 和 http://localhost:7071/queryTempPort),觀察接口的端口返回情況
如果以上過程順利的話,你訪問queryPort接口的時候返回的端口不是隨機的,幾乎沒怎么變化,訪問queryTempPort接口的時候,接口返回的端口是隨機的,說明我們以上配置是可行的。而且第一次訪問接口的時候,我們在控制台打印了出對應的算法規則,你可以觀察一下。
配置文件的配置方式
以上的配置也可以寫到配置文件中,效果是一樣的:
# 通過配置文件 分別為每個客戶端配置
eureka-provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.BestAvailableRule
eureka-provider.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.NoOpPing
eureka-provider-temp.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
eureka-provider-temp.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.NoOpPing
配置的規則是:
CommonClientConfigKey.class
類中查看。常用的有:
NFLoadBalancerClassName
NFLoadBalancerRuleClassName
NFLoadBalancerPingClassName
NIWSServerListClassName
NIWSServerListFilterClassName
為所有的客戶端自定義默認的配置
這里需要用到的注解是@RibbonClients
@Configuration()
public class DefaultRibbonConfiguration{
@Bean
public IRule iRule() {
// 輪詢
return new RoundRobinRule();
}
@Bean
public IPing ribbonPing() {
return new DummyPing();
}
}
@RibbonClients(defaultConfiguration = DefaultRibbonConfiguration.class)
public class DefaultRibbonClient{
****}
啟動我們的ribbon-client服務,測試訪問下我們的http://localhost:7071/queryPort 接口,發現返回的數據每次都不一樣,變為輪詢的方式返回接口信息了。
測試到這里的時候,配置文件中的相關配置我並沒有注釋掉,Java Bean方式的@RibbonClient被注釋掉了,也就是說測試的時候同時配置了配置文件和@RibbonClients,最后測試下來是@RibbonClients配置生效了,配置文件中配置的策略沒有生效。
測試下來,@RibbonClients 的優先級最高,之后是配置文件,再是@RibbonClient,最后是Spring Cloud Netflix 默認值。
同時使用@RibbonClients和@RibbonClient
如果同時使用@RibbonClients和@RibbonClient,全局默認配置和自定義單個ribbon配置,會按照哪個配置生效呢?
我把配置文件中的相關配置都注釋,然后把兩個配置 @RibbonClient 的地方都放開,然后重啟項目,訪問http://localhost:7071/queryPort 和 http://localhost:7071/queryTempPort
測試結果是都報錯,報錯信息如下:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.netflix.loadbalancer.IRule' available: expected single matching bean but found 2: providerRule,iRule
報錯信息的意思是預期需要一個bean,但是結果找到了兩個(providerRule 和 iRule),結果不知道該用哪一個了,所以拋出異常。
那這個問題怎么解決呢?
首先直接說結論吧,就是給你想要生效的那個bean加@Primary
注解,代碼如下所示,如果eureka-provider
不加還是會繼續報錯:
public class ProviderTempConfiguration{
@Primary
@Bean("providerTempRule")
public IRule ribbonRule(){
System.out.println("new ProviderTempConfiguration RandomRule");
return new RandomRule();
}
...
}
再說下排查這個問題的思路:
-
通過查看異常輸出棧的錯誤日志信息,定位到拋出異常的地方
-
之后繼續往前面找相關的邏輯,加斷點,慢慢調試,發現有一個字段(
autowiredBeanName
)為空,才會進入到后面拋異常的邏輯 -
斷點也顯示matchingBeans里面有兩條數據,說明確實是匹配到了2個bean
-
然后我們進入到
determineAutowireCandidate
方法,發現里面有個看起來很不一般的字段:primaryCandidate
,如果這個字段不為空,會直接返回,那這個字段的值是怎么確認的呢? -
繼續進入到
determinePrimaryCandidate
方法,發現這個方法的主要功能就是從給定的多個bean中確定一個主要的候選對象bean,說白了就是選一個bean,那這個方法是怎么選的呢?上源代碼:@Nullable protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) { String primaryBeanName = null; // candidates 是匹配到的多個bean // requiredType 是要匹配的目標依賴類型 for (Map.Entry<String, Object> entry : candidates.entrySet()) { // 遍歷map String candidateBeanName = entry.getKey(); Object beanInstance = entry.getValue(); if (isPrimary(candidateBeanName, beanInstance)) { // 最重要的邏輯,看是不是主要的bean,看到這有經驗的其實都知道要加@Primary注解了 if (primaryBeanName != null) { boolean candidateLocal = containsBeanDefinition(candidateBeanName); boolean primaryLocal = containsBeanDefinition(primaryBeanName); if (candidateLocal && primaryLocal) { throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), "more than one 'primary' bean found among candidates: " + candidates.keySet()); } else if (candidateLocal) { primaryBeanName = candidateBeanName; } } else { primaryBeanName = candidateBeanName; } } } return primaryBeanName; }
-
進入到
isPrimary(candidateBeanName, beanInstance)
方法,最后實際就是返回的以下邏輯:@Override public boolean isPrimary() { return this.primary; }
-
所以解決上面的問題,只需要在我們的ProviderTempConfiguration類里面為bean 再添加一個
@Primary
注解
Ribbon超時時間
全局默認配置
# 全局ribbon超時時間
#讀超時
ribbon.ReadTimeout=3000
#連接超時
ribbon.ConnectTimeout=3000
#同一台實例最大重試次數,不包括首次調用
ribbon.MaxAutoRetries=0
#重試負載均衡其他的實例最大重試次數,不包括首次調用
ribbon.MaxAutoRetriesNextServer=1
為每個client單獨配置
# 為每個服務單獨配置超時時間
eureka-provider.ribbon.ReadTimeout=4000
eureka-provider.ribbon.ConnectTimeout=4000
eureka-provider.ribbon.MaxAutoRetries=0
eureka-provider.ribbon.MaxAutoRetriesNextServer=1
自定義Ribbon負載均衡策略
Ribbon定義了以下幾個屬性支持自定義配置:
<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter
這里以自定義負載均衡策略規則為例,只需要實現IRule
接口或者繼承AbstractLoadBalancerRule
:
public class MyRule implements IRule{
private static Logger log = LoggerFactory.getLogger(MyRule.class);
private ILoadBalancer lb;
@Override
public Server choose(Object key){
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
// 是輪詢、隨機、加權、hash?自己實現從server list中選擇一個server
// 這里寫簡單點,總是請求第一台服務,這樣的邏輯是不會用到真實的環境的
server = allList.get(0);
}
return server;
}
@Override
public void setLoadBalancer(ILoadBalancer lb){
this.lb = lb;
}
@Override
public ILoadBalancer getLoadBalancer(){
return lb;
}
}
然后就可以用Java Bean的方式或者配置文件的方式進行配置了,其他像自定義ping的策略也差不多。
Ribbon總結
- Ribbon 沒有類似@EnableRibbon這樣的注解
- 新版的SpringCloud已經不使用Ribbon作為默認的負載均衡器了
- 可以使用
@RibbonClients
或@RibbonClient
注解來負載均衡相關策略的配置 - 實現對應的接口就可以完成自定義負載均衡策略
- Ribbon 配置的所有key都可以在
CommonClientConfigKey
類中查看
代碼示例:Github ribbon client