第三章 Ribbon負載均衡及源碼解析筆記

現在服務提供方已經可以通過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>其中TresponseType傳入類型,想拿到返回類型需要使用這個包裝類對象的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.ZoneAwareLoadBalancerILoadBalancer的具體實現,看下他構造方法:

@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);
}

IClientConfigclient的配置類,具體指的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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章