現在服務提供方已經可以通過Eureka進行註冊了,但對於服務的消費者,目前並沒有處理,對於服務的消費方,也應該連接上eureka,進行服務的獲取,這個時候就應該使用Ribbon這個組件了。
現在服務提供方已經可以通過Eureka進行註冊了,但對於服務的消費者,目前並沒有處理,對於服務的消費方,也應該連接上eureka,進行服務的獲取,這個時候就應該使用Ribbon這個組件了。
代碼Git地址:https://gitee.com/hankin_chj/springcloud-micro-service.git
一、Ribbon基本使用
1、Ribbon配置介紹
1.1、對應的pom文件如下
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
注意:如果是Edgware或之前的版本,ribbon文件如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency
在用到負載均衡之前,先要到eureka中獲取相關的服務,所以我們這一塊依然需要用到eureka,但eureka中已經內置集成了ribbon,所以在pom文件中,並不需要顯示引入ribbon的依賴。
1.2、bootstrap.properties配置文件可以指定某個微服務配置
springcloud-service2020\springcloud-web\src\main\resources\bootstrap.properties
spring.application.name=micro-web
server.port=8083
#eureka.client.serviceUrl.defaultZone=http\://localhost\:8763/eureka/
eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:8763/eureka/
#服務續約,心跳的時間間隔
eureka.instance.lease-renewal-interval-in-seconds=30
#如果從前一次發送心跳時間起,90 秒沒接受到新的心跳,剔除服務
eureka.instance.lease-expiration-duration-in-seconds=90
#表示 eureka client 間隔多久去拉取服務註冊信息,默認爲 30 秒
eureka.client.registry-fetch-interval-seconds=30
# 關閉ribbon訪問註冊中心Eureka Server發現服務,但是服務依舊會註冊。
#true使用eureka false不使用
ribbon.eureka.enabled=true
#指定調用的節點 localhost:8001 localhost:8002 localhost:8003
micro-order.ribbon.listOfServers=localhost:8001,localhost:8002,localhost:8003
#單位ms ,請求連接超時時間
micro-order.ribbon.ConnectTimeout=1000
#單位ms ,請求處理的超時時間
micro-order.ribbon.ReadTimeout=2000
micro-order.ribbon.OkToRetryOnAllOperations=true
# 切換實例的重試次數
micro-order.ribbon.MaxAutoRetriesNextServer=2
#對當前實例的重試次數 當Eureka中可以找到服務,但是服務連不上時將會重試
micro-order.ribbon.MaxAutoRetries=2
#
micro-order.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
micro-order.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.PingUrl
2、爲consumer集成Ribbon
2.1、consumer服務修改pom文件,增加eureka的支持:
<!-- eureka中已經內置集成了ribbon,所以並不需要顯示引入ribbon的依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.2、consumer服務修改RestConfig配置類
在獲取RestTemplate對象的時候加入Ribbon的配置信息@LoadBalanced
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* 服務提供方目前已經使用了密碼驗證,這個時候服務的消費方如果想直接訪問就不可能了,
* 這個時候一個以頭的信息進行處理,然後使用Base64進行加密處理後才能得到正確的訪問路徑
*/
@Bean
public HttpHeaders getHeaders() { // 要進行一個Http頭信息配置
HttpHeaders headers = new HttpHeaders(); // 定義一個HTTP的頭信息
String auth = "admin:admin"; // 認證的原始信息
byte[] encodedAuth = Base64.getEncoder()
.encode(auth.getBytes(Charset.forName("US-ASCII"))); // 進行一個加密的處理
String authHeader = "Basic " + new String(encodedAuth);
headers.set("Authorization", authHeader);
return headers;
}
}
2.3、consumer服務增加Eureka服務註冊信息
consumer服務修改application.yml文件,增加Eureka服務註冊相關信息
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://admin:admin@eureka1:7001/eureka,http://admin:admin@eureka2:7002/eureka,http://admin:admin@eureka3:7003/eureka
2.4、啓動類增加Eureka客戶端的配置註解
Consumer服務員修改項目啓動類ConsumerApp,增加Eureka客戶端的配置註解@EnableEurekaClient。
2.5、修改ConsumerProductController控制器
現在在eureka中註冊的服務名稱都是大寫字母:SPRINGCLOUD-MICRO-PRODUCT
@RestController
@RequestMapping("/consumer")
public class ConsumerProductSecurityController {
// 普通方式訪問product服務接口
// public static final String PRODUCT_GET_URL = "http://localhost:8080/prodcut/get/";
// public static final String PRODUCT_LIST_URL="http://localhost:8080/prodcut/list/";
// public static final String PRODUCT_ADD_URL = "http://localhost:8080/prodcut/add/";
// TODO 通過使用ribbon訪問product服務
public static final String PRODUCT_GET_URL = "http://SPRINGCLOUD-MICRO-PRODUCT/prodcut/get/";
public static final String PRODUCT_LIST_URL="http://SPRINGCLOUD-MICRO-PRODUCT/prodcut/list/";
public static final String PRODUCT_ADD_URL = "http://SPRINGCLOUD-MICRO-PRODUCT/prodcut/add/";
@Resource
private RestTemplate restTemplate;
@Resource
private HttpHeaders httpHeaders;
/**
* 添加了spring security以後,訪問接口需要密碼驗證,需要在請求頭HttpHeaders中添加驗證信息
*/
@RequestMapping("/product/get2")
public Object getProduct2(long id) {
Product product = restTemplate.exchange(PRODUCT_GET_URL + id,HttpMethod.GET,
new HttpEntity<Object>(httpHeaders), Product.class).getBody();
return product;
}
@RequestMapping("/product/list2")
public Object listProduct2() {
List<Product> list = restTemplate.exchange(PRODUCT_LIST_URL,HttpMethod.GET,
new HttpEntity<Object>(httpHeaders), List.class).getBody();
return list;
}
@RequestMapping("/product/add2")
public Object addPorduct2(Product product) {
Boolean result = restTemplate.exchange(PRODUCT_ADD_URL, HttpMethod.POST,
new HttpEntity<Object>(product,httpHeaders), Boolean.class).getBody();
return result;
}
}
啓動服務,訪問地址:http://localhost/consumer/product/list2
可以返回結果,這個時候Ribbon與Eureka已經整合成功。
[{"productId":null,"productName":"java編程","productDesc":"springcloud"},
{"productId":null,"productName":"Springboot","productDesc":"springcloud"},
{"productId":null,"productName":"西遊記","productDesc":"springcloud"},
{"productId":null,"productName":"水滸傳","productDesc":"springcloud"},
{"productId":null,"productName":"西廂記","productDesc":"springcloud"}]
3、ribbon服務調用restTemplate使用說明
3.1、restTemplate調用代碼:
@Slf4j
@Service
@Scope(proxyMode = ScopedProxyMode.INTERFACES)
public class UserServiceImpl implements UserService {
private AtomicInteger s = new AtomicInteger();
private AtomicInteger f = new AtomicInteger();
public static String SERVIER_NAME = "micro-order";
@Autowired
private RestTemplate restTemplate;
@Override
public String queryContents() {
s.incrementAndGet();
//TODO 注意:getForObject是 getForEntity() 的封裝,包含了請求頭等信息
String results = restTemplate.getForObject("http://"
+ SERVIER_NAME + "/queryUser", String.class);
return results;
}
}
通過getForObject方法可以掉到用micro-order服務的,queryUser接口。然後在調用期間會存在負載均衡,micro-order服務對應有幾個服務實例就會根據負載均衡算法選擇某一個去調用。
3.2、Get請求說明:
getForEntity:此方法有三種重載形式,分別爲:
getForEntity(String url, Class<T> responseType)
getForEntity(String url, Class<T> responseType, Object... uriVariables)
getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
getForEntity(URI url, Class<T> responseType)
注意:此方法返回的是一個包裝對象ResponseEntity<T>其中T爲responseType傳入類型,想拿到返回類型需要使用這個包裝類對象的getBody()方法。
getForObject:此方法也有三種重載形式,這點與 getForEntity 方法相同:
getForObject(String url, Class<T> responseType)
getForObject(String url, Class<T> responseType, Object... uriVariables)
getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
getForObject(URI url, Class<T> responseType)
注意:此方法返回的對象類型爲 responseType 傳入類型
3.2、Post請求
post請求 get請求都有*ForEntity 和*ForObject 方法,其中參數列表有些不同,除了這兩個方法外,還有一個postForLocation方法,其中postForLocation以post請求提交資源,並返回新資源的URI
postForEntity:此方法有三種重載形式,分別爲:
postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables)
postForEntity(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
postForEntity(URI url, Object request, Class<T> responseType)
注意:此方法返回的是一個包裝對象ResponseEntity<T>其中T爲responseType傳入類型,想拿到返回類型需要使用這個包裝類對象的getBody()方法。
postForObject:此方法也有三種重載形式,這點與 postForEntity 方法相同:
postForObject(String url, Object request, Class<T> responseType, Object... uriVariables)
postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
postForObject(URI url, Object request, Class<T> responseType)
注意:此方法返回的對象類型爲responseType傳入類型
postForLocation:此方法中同樣有三種重載形式,分別爲:
postForLocation(String url, Object request, Object... uriVariables)
postForLocation(String url, Object request, Map<String, ?> uriVariables)
postForLocation(URI url, Object request)
注意:此方法返回的是新資源的URI,相比getForEntity、getForObject、postForEntity、postForObject方法不同的是這個方法中無需指定返回類型,因爲返回類型就是URI,通過Object... uriVariables、Map<String, ?> uriVariables進行傳參依舊需要佔位符,參看postForEntity部分代碼。
二、Ribbon負載均衡的實現
通過上面的代碼發現我們用到了一個註解@LoadBalanced,根據這名字大概就能知道Ribbon是可以實現負載均衡的。
1、複製多個product服務實例
第一步:將springcloud-micro-product服務複製兩份:
分別爲springcloud-micro-product2與springcloud-micro-product3。
第二步:將springcloud數據庫複製兩份,分別爲springcloud2與springcloud3數據庫,裏面分別執行spingcloud數據庫的腳本。
1.1、product2服務修改application.yml文件如下:
注意:切記application:name: springcloud-micro-product這個不能修改,這樣做的目的是同一個服務下面有多個服務實例。
server:
port: 8081
mybatis:
mapper-locations: # 所有的mapper映射文件
- classpath:mapping/*.xml
spring:
application:
name: springcloud-micro-product
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 配置當前要使用的數據源的操作類型
driver-class-name: com.mysql.cj.jdbc.Driver # 配置MySQL的驅動程序類
url: jdbc:mysql://localhost:3306/springcloud2?serverTimezone=GMT%2B8 # 數據庫連接地址
username: root # 數據庫用戶名
password: root # 數據庫連接密碼
eureka: # eureka客戶端配置
client:
register-with-eureka: true
service-url:
#defaultZone: http://admin:admin@localhost:7001/eureka
defaultZone: http://admin:admin@eureka1:7001/eureka,http://admin:admin@eureka2:7002/eureka,http://admin:admin@eureka3:7003/eureka
instance:
instance-id: springcloud-micro-product2
prefer-ip-address: true
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
info: # 增加服務信息
app.name: microcloud-provider-product2
company.name: hankin
build.artifactId: $project.artifactId$
build.modelVersion: $project.modelVersion$
# security: # 使用自定義的security模塊
# user:
# name: admin # 認證用戶名
# password: admin # 認證密碼
# roles:
# - USER # 授權角色
1.2、product3服務修改application.yml文件如下
server:
port: 8082
mybatis:
mapper-locations: # 所有的mapper映射文件
- classpath:mapping/*.xml
spring:
application:
name: springcloud-micro-product
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 配置當前要使用的數據源的操作類型
driver-class-name: com.mysql.cj.jdbc.Driver # 配置MySQL的驅動程序類
url: jdbc:mysql://localhost:3306/springcloud3?serverTimezone=GMT%2B8 # 數據庫連接地址
username: root # 數據庫用戶名
password: root # 數據庫連接密碼
eureka: # eureka客戶端配置
client:
register-with-eureka: true
service-url:
#defaultZone: http://admin:admin@localhost:7001/eureka
defaultZone: http://admin:admin@eureka1:7001/eureka,http://admin:admin@eureka2:7002/eureka,http://admin:admin@eureka3:7003/eureka
instance:
instance-id: springcloud-micro-product3
prefer-ip-address: true
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
info: # 增加服務信息
app.name: microcloud-provider-product3
company.name: hankin
build.artifactId: $project.artifactId$
build.modelVersion: $project.modelVersion$
# security: # 使用自定義的security模塊
# user:
# name: admin # 認證用戶名
# password: admin # 認證密碼
# roles:
# - USER # 授權角色
#logging:
# level:
# com.chj.mapper: debug
1.3、分別啓動3個服務提供方
啓動完成,分別訪問:
http://localhost:8080/prodcut/get/1
http://localhost:8081/prodcut/get/1
http://localhost:8082/prodcut/get/1
確認3個服務是能正確提供訪問的,查詢結果如下:
{"productId":null,"productName":"java編程","productDesc":"springcloud"}
{"productId":null,"productName":"java編程2","productDesc":"springcloud2"}
"productId":null,"productName":"java編程3","productDesc":"springcloud3"}
1.4、啓動consumer
訪問:http://localhost/consumer/product/list
現在發現每一次獲取數據都是通過不同的微服務獲得的,所以現在同一個消費端就可以通過Ribbon實現了負載均衡配置處理。
2、代碼方式配置負載算法
2.1、使用@RibbonClients 加載配置
/*
* 這個是針對 micro-order服務的 ribbon配置
* */
@Configuration
@RibbonClients(value = {
@RibbonClient(name = "micro-order",configuration = RibbonLoadBalanceMicroOrderConfig.class)
})
public class LoadBalanceConfig {
}
2.2、負載算法使用
/*
* 這個類最好不要出現在啓動類的@ComponentScan掃描範圍
* 如果出現在@ComponentScan掃描訪問,那麼這個配置類就是每個服務共用的配置了
* */
@Configuration
public class RibbonLoadBalanceMicroOrderConfig {
// @RibbonClientName
private String name = "micro-order";
@Bean
@ConditionalOnClass
public IClientConfig defaultClientConfigImpl() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(name);
config.set(CommonClientConfigKey.MaxAutoRetries,2);
config.set(CommonClientConfigKey.MaxAutoRetriesNextServer,2);
config.set(CommonClientConfigKey.ConnectTimeout,2000);
config.set(CommonClientConfigKey.ReadTimeout,4000);
config.set(CommonClientConfigKey.OkToRetryOnAllOperations,true);
return config;
}
/*
* 判斷服務是否存活,不建議使用
* */
// @Bean
public IPing iPing() {
//這個實現類會去調用服務來判斷服務是否存活
return new PingUrl();
}
@Bean
public IRule ribbonRule() {
//線性輪訓
new RoundRobinRule();
//可以重試的輪訓
new RetryRule();
//根據運行情況來計算權重
new WeightedResponseTimeRule();
//過濾掉故障實例,選擇請求數最小的實例
new BestAvailableRule();
return new RandomRule();
}
}
3、自定義Ribbon路由
前面已經使用Ribbon實現了路由,通過測試,也不難發現默認Ribbon使用的路由策略是輪詢,可以看下源代碼BaseLoadBalancer(ribbon-loadbalancer-2.2.5.jar)。
public class BaseLoadBalancer extends AbstractLoadBalancer implements
PrimeConnections.PrimeConnectionListener, IClientConfigAware {
private static Logger logger = LoggerFactory.getLogger(BaseLoadBalancer.class);
private final static IRule DEFAULT_RULE = new RoundRobinRule();
private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();
private static final String DEFAULT_NAME = "default";
private static final String PREFIX = "LoadBalancer_";
3.1、全局路由配置
這種負載均衡的策略其實也是可以由用戶來修改的,如果想要去修改,可以使用自定義的LoadBalance
consumer服務修改RestConfig:
@Configuration
public class RestConfig {
@Bean
@LoadBalanced // 負載均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* 服務提供方目前已經使用了密碼驗證,這個時候服務的消費方如果想直接訪問就不可能了,
* 這個時候一個以頭的信息進行處理,然後使用Base64進行加密處理後才能得到正確的訪問路徑
*/
@Bean
public HttpHeaders getHeaders() { // 要進行一個Http頭信息配置
HttpHeaders headers = new HttpHeaders(); // 定義一個HTTP的頭信息
String auth = "admin:admin"; // 認證的原始信息
byte[] encodedAuth = Base64.getEncoder()
.encode(auth.getBytes(Charset.forName("US-ASCII"))); // 進行一個加密的處理
String authHeader = "Basic " + new String(encodedAuth);
headers.set("Authorization", authHeader);
return headers;
}
// 自定義的LoadBalance
@Bean
public IRule ribbonRule() { // 其中IRule就是所有規則的標準
return new com.netflix.loadbalancer.RandomRule(); // 隨機的訪問策略
}
}
這個時候重啓測試發現,默認的路由規則已經變成了隨機。
3.2、單獨設置某個Ribbon的路由
有時候,某個消費者可能需要訪問多個多個服務提供方,而希望每個服務提供方提供的路由規則並不相同,這個時候就不能讓Spring掃描到IRULE,需要通過@RibbonClient 來指定服務與配置的關係。
1)consumer服務修改RestConfig,刪除IRULE
// @Bean
// public IRule ribbonRule() { // 其中IRule就是所有規則的標準
// return new com.netflix.loadbalancer.RandomRule(); // 隨機的訪問策略
// }
2)consumer服務新增一個路由規則的配置類
注意這個類不應該放到SpringCloud掃描不到的位置,否則又回變成全局的IRULE,所以這個時候應該單獨使用一個新的包,這個包和啓動並不在同一個包目錄結構下:
package com.ribbon.config;
/**
* 自定義的LoadBalance(非全局)
* 有時候,某個消費者可能需要訪問多個多個服務提供方,而希望每個服務提供方提供的路由規則並不相同,
* 這個時候就不能讓Spring掃描到IRULE,需要通過@RibbonClient 來指定服務與配置的關係
*/
public class RibbonConfig {
@Bean
public IRule ribbonRule() { // 其中IRule就是所有規則的標準
return new com.netflix.loadbalancer.RandomRule(); // 隨機的訪問策略
}
}
3)consumer修改啓動類,使用@RibbonClient指定配置類
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name ="SPRINGCLOUD-MICRO-PRODUCT" ,configuration = RibbonConfig.class)
public class ConsumerApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerApp.class,args);
}
}
這裏的name只服務的名稱,如果需要有多個服務提供方,可以使用@RibbonClients進行配置。
重啓動訪問依然是隨機返回不同的數據庫數據結果。
3.3、服務提供方的信息獲取
在服務的消費方,也是可以獲取到服務提供方的具體信息
Consumer服務修改ConsumerProductController引入LoadBalancerClient,可以通過loadBalancerClient的choose("MICROCLOUD-PROVIDER-PRODUCT") ;方法取得服務提供方的實例信息:
@RestController
@RequestMapping("/consumer")
public class ConsumerProductSecurityController {
// TODO 通過使用ribbon訪問product服務
public static final String PRODUCT_GET_URL = "http://SPRINGCLOUD-MICRO-PRODUCT/prodcut/get/";
public static final String PRODUCT_LIST_URL= "http://SPRINGCLOUD-MICRO-PRODUCT/prodcut/list/";
public static final String PRODUCT_ADD_URL = "http://SPRINGCLOUD-MICRO-PRODUCT/prodcut/add/";
@Resource
private RestTemplate restTemplate;
@Resource
private HttpHeaders httpHeaders;
// 服務提供方的信息獲取
@Resource
private LoadBalancerClient loadBalancerClient;
/**
* 添加了spring security以後,訪問接口需要密碼驗證,需要在請求頭HttpHeaders中添加驗證信息
*/
@RequestMapping("/product/get2")
public Object getProduct2(long id) {
ServiceInstance serviceInstance = this.loadBalancerClient.choose("SPRINGCLOUD-MICRO-PRODUCT") ;
System.out.println("【*** ServiceInstance ***】host = " + serviceInstance.getHost()
+ ":port = " + serviceInstance.getPort()+ ":serviceId = " + serviceInstance.getServiceId());
Product product = restTemplate.exchange(PRODUCT_GET_URL + id,HttpMethod.GET,
new HttpEntity<Object>(httpHeaders), Product.class).getBody();
return product;
}
@RequestMapping("/product/list2")
public Object listProduct2() {
List<Product> list = restTemplate.exchange(PRODUCT_LIST_URL,HttpMethod.GET,
new HttpEntity<Object>(httpHeaders), List.class).getBody();
return list;
}
@RequestMapping("/product/add2")
public Object addPorduct2(Product product) {
Boolean result = restTemplate.exchange(PRODUCT_ADD_URL, HttpMethod.POST,
new HttpEntity<Object>(product,httpHeaders), Boolean.class).getBody();
return result;
}
}
4、脫離Eureka使用Ribbon
Ribbon是一個獨立組件,可以脫離springcloud 使用的,之前所用Ribbon都是從Eureka中獲取服務並通過@LoadBalanced來實現負載均衡的,其實Ribbon也可以脫離Eureka來使用。
複製springcloud-micro-consumer成一個新的模塊springcloud-micro-consumer-ribbon
4.1、Ribbon服務修改pom文件
刪除eureka的依賴添加ribbon的依賴:
<!-- api依賴 -->
<dependency>
<groupId>com.chj</groupId>
<artifactId>springcloud-micro-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- eureka中已經內置集成了ribbon,所以並不需要顯示引入ribbon的依賴 -->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
4.2、Ribbon服務修改application.yml配置文件
server:
port: 80
ribbon:
eureka:
enabled: false
MICROCLOUD-PROVIDER-PRODUCT:
ribbon:
listOfServers: http://localhost:8080,http://localhost:8081,http://localhost:8082
4.3、Ribbon修改RestConfig,刪除@LoadBalanced註解
@Configuration
public class RestConfig {
@Bean
// @LoadBalanced // 負載均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public HttpHeaders getHeaders() { // 要進行一個Http頭信息配置
HttpHeaders headers = new HttpHeaders(); // 定義一個HTTP的頭信息
String auth = "admin:admin"; // 認證的原始信息
byte[] encodedAuth = Base64.getEncoder()
.encode(auth.getBytes(Charset.forName("US-ASCII"))); // 進行一個加密的處理
String authHeader = "Basic " + new String(encodedAuth);
headers.set("Authorization", authHeader);
return headers;
}
// @Bean
// public IRule ribbonRule() { // 其中IRule就是所有規則的標準
// return new com.netflix.loadbalancer.RandomRule(); // 隨機的訪問策略
// }
}
4.4、Ribbon修改ConsumerProductController,修改服務的調用URI
@RestController
@RequestMapping("/consumer")
public class ConsumerProductSecurityController {
public static final String PRODUCT_TOPIC = "SPRINGCLOUD-MICRO-PRODUCT";
@Resource
private RestTemplate restTemplate;
@Resource
private HttpHeaders httpHeaders;
// 服務提供方的信息獲取
@Resource
private LoadBalancerClient loadBalancerClient;
@RequestMapping("/product/list3")
public Object listProduct3() {
ServiceInstance serviceInstance = this.loadBalancerClient.choose(PRODUCT_TOPIC) ;
System.out.println("【*** ServiceInstance ***】host = " + serviceInstance.getHost()
+ ":port = " + serviceInstance.getPort() +":serviceId = " +serviceInstance.getServiceId());
URI url = URI.create(String.format("http://%s:%s/prodcut/list/",
serviceInstance.getHost(), serviceInstance.getPort()));
List<Product> list = restTemplate.exchange(url,HttpMethod.GET,
new HttpEntity<Object>(httpHeaders), List.class).getBody();
return list;
}
}
啓動springcloud-micro-consumer-ribbon服務,訪問:http://localhost/consumer/product/list3
結果和上面一樣可以實現隨機訪問服務提供端,返回不同的數據結果。
三、Ribbon源碼解析
1、註解@RibbonClient
@RibbonClient註解可以實現Ribbon客戶端,ribbon需要設置客戶端的名稱,以及相關的路由配置類
@Configuration
@Import(RibbonClientConfigurationRegistrar.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RibbonClient {
String value() default "";
String name() default "";
Class<?>[] configuration() default {};
}
value和name是等價了,用於設置客戶端的實例名稱,而configuration用於指定配置類,接下來還導入了個RibbonClientConfigurationRegistrar,他實現了ImportBeanDefinitionRegistrar接口,也不用多說,它是spring-context-5.1.5.RELEASE.jar包的工具接口,用於spring動態註冊BeanDefinition的接口,在這是用於註冊Ribbon所需的BeanDefinition(比如Ribbon客戶端實例)。
1.1、RibbonClientConfigurationRegistrar源碼分析
org.springframework.cloud.netflix.ribbon.RibbonClientConfigurationRegistrar#registerBeanDefinitions():
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
//TODO 獲取@RibbonClient的參數,獲取clientName後進行configuraction註冊
Map<String, Object> attrs = metadata.getAnnotationAttributes(RibbonClients.class.getName(), true);
if (attrs != null && attrs.containsKey("value")) {
AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
for (AnnotationAttributes client : clients) {
registerClientConfiguration(registry, getClientName(client), client.get("configuration"));
}
}
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name; //TODO 獲取ribbonclient的value或者name屬性
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
} else {
name = "default." + metadata.getClassName();
}
//TODO 註冊
registerClientConfiguration(registry, name,attrs.get("defaultConfiguration"));
}
Map<String, Object> client = metadata.getAnnotationAttributes(RibbonClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
//TODO 註冊
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
兩個情況註冊到會調用同一個方法,這裏用到了建造者模式:
private void registerClientConfiguration(BeanDefinitionRegistry registry,Object name, Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RibbonClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + ".RibbonClientSpecification",builder.getBeanDefinition());
}
Registry.registerClientConfiguration方法會註冊一個RibbonClientSpecification的bean,名稱是ribbon的名稱加上RibbonClientSpecification;RibbonClientSpecification實現了NamedContextFactory.Specification,是提供給SpringClientFactory使用的,他用於初始化ribbon的相關實例使用。
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
public interface Specification {
String getName();
Class<?>[] getConfiguration();
}
public class RibbonClientSpecification implements NamedContextFactory.Specification {
private String name;
private Class<?>[] configuration;
public RibbonClientSpecification() {}
1.2、SpringClientFactory在哪?
RibbonAutoConfiguration配置見:spring-cloud-netflix-ribbon-2.0.0.RELEASE.jar!/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration
RibbonAutoConfiguration源碼如下所示:
@Configuration
@ConditionalOnClass({ IClient.class, RestTemplate.class, AsyncRestTemplate.class, Ribbon.class})
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class})
public class RibbonAutoConfiguration {
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Autowired
private RibbonEagerLoadProperties ribbonEagerLoadProperties;
@Bean
public HasFeatures ribbonFeature() {
return HasFeatures.namedFeature("Ribbon", Ribbon.class);
}
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
那麼最重要的就回到了LoadBalancerClient,他是Ribbon項目最核心的接口,他有一個實現類RibbonLoadBalancerClient,LoadBalancerClient接口源碼如下:
public interface LoadBalancerClient extends ServiceInstanceChooser {
//TODO 從servericeId 所代表的服務列表中選擇一個服務器來發送網絡請求
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
//TODO 構建網絡請求URI
URI reconstructURI(ServiceInstance instance, URI original);
}
2、LoadBalancerClient源碼分析
LoadBalancerClient 還繼承了一個接口ServiceInstanceChooser
org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser
public interface ServiceInstanceChooser {
//根據serviceId從服務器列表中選擇一個ServiceInstance
ServiceInstance choose(String serviceId);
}
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient 是LoadBalancerClient 的實現類,核心方法execute源碼如下:
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//TODO 每次發送請求都回獲取一個ILoadBalancer ,會涉及負載均衡(IRULS),
服務器列表集羣(ServerList) 和檢驗服務是否存活(IPing)等細節實現
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
另外RibbonLoadBalancerClient#getServer(com.netflix.loadbalancer.ILoadBalancer)方法:
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
這方法直接調用了ILoadBalancer 的chooseServer方法來使用負載君和策略,從已知的服務列表中選出一個服務器實例;接下來重點就到了ILoadBalancer 這接口了,他定義負載均衡操作的接口,由前面說過的SpringClientFactory獲得而來。而SpringClientFactory又是再RibbonAutoConfiguration定義:
3、ILoadBalancer接口分析
接口的實現類有如下所示:
com.netflix.loadbalancer.ZoneAwareLoadBalancer是ILoadBalancer的具體實現,看下他構造方法:
@Deprecated
public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
ServerList<T> serverList, ServerListFilter<T> filter) {
super(clientConfig, rule, ping, serverList, filter);
}
public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList<T> serverList,
ServerListFilter<T> filter,ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
}
IClientConfig:client的配置類,具體指的DefaultClientConfigImpl,
IRule:負載均衡的策略類,默認輪詢RoundRobinRule。
IPing:服務可用性檢查,默認DummyPing。
ServerList:服務列表獲取,ConfigurationBasedServerList。
ServerListFilter:服務列表過濾 ZonePreferenceServerListFilter。
ZoneAwareLoadBalancer其中一個重要的方法就是chooseServer()方法。
擴展閱讀:https://www.cnblogs.com/junjiang3/p/9061867.html
4、chooseServer()方法源碼分析:
com.netflix.loadbalancer.ZoneAwareLoadBalancer.chooseServer(Object key)方法源碼分析:
@Override
public Server chooseServer(Object key) {
//TODO 如果就一個zone,直接返回
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
try {
//TODO 獲取當前有關負載均衡的服務器狀態集合
LoadBalancerStats lbStats = getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
//TODO 獲取平均負載閾值
if (triggeringLoad == null) {
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
}
//TODO 獲取平均實例故障率閾值
if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
//TODO 根據兩個閾值來獲取所有可用的服務列表
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
//TODO 隨機從可用服務區列表中選擇一個服務器
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
//TODO 得到zoone對應的BaseLoadBalancer
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
父類com.netflix.loadbalancer.BaseLoadBalancer#chooseServer方法實現:
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
//根據具體的路由算法獲取服務
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
5、IRULE負載均衡的實現
具體服務的的負載還是由IRULE實現,前面在入門部分也講過怎麼更換具體的路由實現方法,IRULE具體在RibbonClientConfiguration進行配置,IRule的接口choose方法負責選區一個具體的服務。
BestAvailableRule:選擇最小請求數的服務器
RoundRobinRule:輪詢方式
ClientConfigEnabledRoundRobinRule:使用RoundRobinRule選擇服務器
RetryRule:根據選的輪詢的方式重試
WeightedResponseTimeRule:根據響應時間去計算一個權重,weight越低,被選擇的可能性就越低。
ZoneAvoidanceRule:根據server的zone區域和可用性來輪詢選擇。
RibbonClientConfiguration.ribbonRule(IClientConfig config)方法代碼如下所示:
@Configuration
@EnableConfigurationProperties
@Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {
public static final int DEFAULT_CONNECT_TIMEOUT = 1000;
public static final int DEFAULT_READ_TIMEOUT = 1000;
@RibbonClientName
private String name = "client";
@Autowired
private PropertiesFactory propertiesFactory;
@Bean
@ConditionalOnMissingBean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(this.name);
config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
return config;
}
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}
6、ZoneAvoidanceRule的具體實現
ZoneAvoidanceRule是默認的IRule實例,它的父類是PredicateBasedRule,他使用PredicateBasedRule來根據服務區的運行狀況和服務器的可用性來選擇服務器。
6.1、PredicateBasedRule類分析,具體依次做了以下事情:
1)先使用ILoadBalancer獲取服務器列表
2)使用AbstractServerPredicate進行服務器過濾
3)最後輪詢從剩餘的服務器列表中選擇最終的服務器
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
public abstract AbstractServerPredicate getPredicate();
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
PredicateBasedRule.getPredicate()方法又是一個抽象的實現,具體實現見:ZoneAvoidanceRule的getPredicate()方法,又通過createCompositePredicate(zonePredicate, availabilityPredicate)進行賦值:
public class ZoneAvoidanceRule extends PredicateBasedRule {
private static final Random random = new Random();
private CompositePredicate compositePredicate;
public ZoneAvoidanceRule() {
super();
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this);
compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}
//將兩個Predicate組合成一個CompositePredicate
private CompositePredicate createCompositePredicate(ZoneAvoidancePredicate p1, AvailabilityPredicate p2) {
return CompositePredicate.withPredicates(p1, p2).addFallbackPredicate(p2)
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue()) .build();
}
@Override
public AbstractServerPredicate getPredicate() {
return compositePredicate;
}
createCompositePredicate方法兩個參數分析:
ZoneAvoidancePredicate:判斷一個服務器的運行狀況是否可用,去除不可用服務器的所有服務器。
AvailabilityPredicate:用於過濾連接數過多的服務器。
6.2、在來看下chooseRoundRobinAfterFiltering方法
前面已經說過了它是過濾的方法,然後AvailabilityPredicate裏面並沒有這方法,他直接繼承了他的父類AbstractServerPredicate.chooseRoundRobinAfterFiltering(java.util.List<com.netflix.loadbalancer.Server>):
1)首先看下ZoneAvoidanceRule的父類PredicateBasedRule的代碼實現:
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
public abstract AbstractServerPredicate getPredicate();
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
2)chooseRoundRobinAfterFiltering方法的實現是在AbstractServerPredicate類中完成:
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
//過濾服務器列表
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
//(i+1)%n 輪詢選擇一個服務實例
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
6.3、使用serverOnlyPredicate來依次實現服務過濾
由於前面loadBalancerKey直接傳入的null,方法getEligibleServers會使用serverOnlyPredicate來依次實現服務過濾,代碼如下:com.netflix.loadbalancer.AbstractServerPredicate.getEligibleServers()
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
if (loadBalancerKey == null) {
return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));
} else {
List<Server> results = Lists.newArrayList();
for (Server server: servers) {
if (this.apply(new PredicateKey(loadBalancerKey, server))) {
results.add(server);
}
}
return results;
}
}
serverOnlyPredicate則會調用apply方法,並將Server 對象分裝PredicateKey當作參數傳入,
代碼見:com.netflix.loadbalancer.AbstractServerPredicate.getServerOnlyPredicate()方法
public Predicate<Server> getServerOnlyPredicate() {
return serverOnlyPredicate;
}
private final Predicate<Server> serverOnlyPredicate = new Predicate<Server>() {
@Override
public boolean apply(@Nullable Server input) {
return AbstractServerPredicate.this.apply(new PredicateKey(input));
}
};
AbstractServerPredicate並沒有實現apply方法,具體的實現又回到了子類CompositePredicate的apply方法,它會依次調用ZoneAvoidancePredicate與AvailabilityPredicate的apply方法:
6.4、ZoneAvoidancePredicate#apply方法實現:
源碼見:com.netflix.loadbalancer.ZoneAvoidancePredicate.apply();
public class ZoneAvoidancePredicate extends AbstractServerPredicate {
@Override
public boolean apply(@Nullable PredicateKey input) {
if (!ENABLED.get()) {
return true;
}
String serverZone = input.getServer().getZone();
if (serverZone == null) {
//TODO 如果服務器沒有zone的相關信息,直接返回
return true;
}
LoadBalancerStats lbStats = getLBStats();
//TODO LoadBalancerStats 存儲每個服務器節點的執行特徵和運行記錄,這些信息可供動態負責均衡使用
if (lbStats == null) {
//TODO 如果沒有服務器的記錄,直接返回
return true;
}
if (lbStats.getAvailableZones().size() <= 1) {
//TODO 如果根本就一個服務器,直接返回
return true;
}
//TODO PredicateKey 封裝了Server的信息,判斷下服務器區的記錄是否用當前區的信息
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
//TODO 如果沒有直接返回
if (!zoneSnapshot.keySet().contains(serverZone)) {
// TODO The server zone is unknown to the load balancer, do not filter it out
return true;
}
logger.debug("Zone snapshots: {}", zoneSnapshot);
//TODO 獲取可用的服務器列表
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
//TODO 判斷當前服務器是否在可用的服務器列表中
if (availableZones != null) {
return availableZones.contains(input.getServer().getZone());
} else {
return false;
}
}
6.5、獲取可用的服務器列表getAvailableZones方法實現
源碼見:com.netflix.loadbalancer.ZoneAvoidanceRule#getAvailableZones()方法
最後落到這方法上,這方法是用來篩選服務區列表的:
public static Set<String> getAvailableZones( Map<String, ZoneSnapshot> snapshot,
double triggeringLoad,double triggeringBlackoutPercentage) {
if (snapshot.isEmpty()) {
return null;
}
Set<String> availableZones = new HashSet<String>(snapshot.keySet());
if (availableZones.size() == 1) {
return availableZones;
}
Set<String> worstZones = new HashSet<String>();
double maxLoadPerServer = 0;
boolean limitedZoneAvailability = false;
//TODO 遍歷所有的服務區
for (Map.Entry<String, ZoneSnapshot> zoneEntry : snapshot.entrySet()) {
String zone = zoneEntry.getKey();
ZoneSnapshot zoneSnapshot = zoneEntry.getValue();
//TODO 獲取服務器中的服務實例數
int instanceCount = zoneSnapshot.getInstanceCount();
if (instanceCount == 0) {
//TODO 如果服務器中沒有服務實例,那麼移除該服務區
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
double loadPerServer = zoneSnapshot.getLoadPerServer();
//TODO 服務區的實例平均負載小於0,或者實例故障率(斷路器端口次數/實例數)大於等於閾值(默認0.99999),則去掉該服務區
if (((double) zoneSnapshot.getCircuitTrippedCount())
/ instanceCount >= triggeringBlackoutPercentage
|| loadPerServer < 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
//TODO 如果該服務區的平均負載和最大負載的差小於一定的數量,則將該服務器加入到最壞服務區集合
if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) {
// they are the same considering double calculation
// round error
worstZones.add(zone);
} else if (loadPerServer > maxLoadPerServer) {
//TODO 否則,如果該zone的平均負載還大於最大負載
maxLoadPerServer = loadPerServer;
worstZones.clear();
worstZones.add(zone);
}
}
}
}
//TODO 如果最大的平均負載小於設定的閾值則直接返回
if (maxLoadPerServer < triggeringLoad && !limitedZoneAvailability) {
// zone override is not needed here
return availableZones;
}
//TODO 否則,從最好的服務器集合裏面隨機挑選一個
String zoneToAvoid = randomChooseZone(snapshot, worstZones);
if (zoneToAvoid != null) {
availableZones.remove(zoneToAvoid);
}
return availableZones;
}
ZoneSnapshot參數說明:
public class ZoneSnapshot {
//實例數
final int instanceCount;
//平均負載
final double loadPerServer;
//斷路器端口數量
final int circuitTrippedCount;
//活動請求數量
final int activeRequestsCount;
}
6.6、CompositePredicate的apply方法
會依次調用ZoneAvoidancePredicate與AvailabilityPredicate的apply方法,那接下來就是AvailabilityPredicate的apply方法了:
1)CompositePredicate.apply方法實現如下:
2)AvailabilityPredicate.apply方法源碼分析:
@Override
public boolean apply(@Nullable PredicateKey input) {
LoadBalancerStats stats = getLBStats();
if (stats == null) {
return true;
}
//TODO 獲得關於該服務器的記錄
return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
}
private boolean shouldSkipServer(ServerStats stats) {
//TODO 如果該服務器的斷路器已經打開,或者他的連接數大於設定的閾值,那麼就需要將服務器過濾掉
if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped())
|| stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
return true;
}
return false;
}