Spring Cloud學習|第二篇:負載均衡-Ribbon

1.簡介

負載均衡是指將負載分攤至多個執行單元上,常見的負載均衡有如下兩種

1.服務器負載均衡.如Nginx:通過Nginx負載均衡策略,將請求轉發至後端服務,如下圖所示

在這裏插入圖片描述

​ 2.客戶端負載均衡:以代碼的形式封裝至服務消費者服務上,消費者維護一份服務提供者信息列表,通過負載均衡策略將分攤給多個服務提供者,從而達到負載均衡目的
在這裏插入圖片描述

2.使用RestTemplate與Ribbon進行消費服務

在上一篇基礎上完成該案例演示,服務具體信息如下表所示

服務名 服務端口 作用
eureka-server 8761 註冊中心
eureka-client 8762,8763 服務提供者
eureka-ribbon-client 8764 負載均衡客戶端
  • 新建服務eureka-ribbon-client

  • 引入依賴

    <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>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
  • 配置application.yml

    spring:
      application:
        name: eureka-ribbon-client
    
    server:
      port: 8764
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka/
    
  • 書寫啓動類

    注意啓動類中需加載RestTemplate

    @EnableEurekaClient
    @SpringBootApplication
    public class EurekaRibbonClientApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(EurekaRibbonClientApplication.class, args);
      }
    
      @LoadBalanced
      @Bean
      public RestTemplate restTemplate(){
        return new RestTemplate();
      }
    }
    
  • 寫一個Restful API接口,用於遠程調用87628763兩個服務

    @Service
    public class RibbonService {
    
      @Resource
      private RestTemplate restTemplate;
    
      public String hi(String name){
        return restTemplate.getForObject("http://eureka-client/hi?name={1}", String.class, name);
      }
    }
    
  • 啓動註冊中心、兩個服務提供者以及ribbon-client-service服務,查看註冊中心
    在這裏插入圖片描述

  • 訪問ribbon-client-service服務,會輪流輸出兩個服務提供者信息,表示負載均衡起作用了

3.LoadBalancerClient介紹

負載均衡器的核心類爲LoadBalancerClient,我們可以通過LoadBalancerClient控制訪問的服務,在此,新增一接口"/testLoadBalancer",通過LoadBalancerClient訪問服務提供者。

  • 新增接口

    @RestController
    public class EurekaRibbonClient {
    
     @Resource
     private LoadBalancerClient loadBalancerClient;
    
     @GetMapping("/testLoadBalancer")
     public String testRibbon() {
       ServiceInstance instance = loadBalancerClient.choose("eureka-client");
       return instance.getHost() + ":" + instance.getPort();
     }
    }
    
  • 啓動服務,訪問http://localhost:8764/testLoadBalancer,輸出結果如下

    127.0.0.1:8762
    127.0.0.1:8763
    
  • Ribbon禁止從Eureka註冊中心獲取服務註冊信息,而是自己維護服務實例列表

    (1)配置application.yml

    ribbon: # 禁止Ribbon從Euraka獲取註冊信息
      eureka:
        enabled: false
    stores: # 設置本地服務註冊信息
      ribbon:
        listOfServers: example.com,goole.com
    
    

    (2)書寫程序

    @RestController
    public class EurekaRibbonClient {
    
      @Resource
      private LoadBalancerClient loadBalancerClient;
    
      @GetMapping("/testLoadBalancer")
      public String testRibbon() {
        ServiceInstance instance = loadBalancerClient.choose("stores");
        return instance.getHost() + ":" + instance.getPort();
      }
    }
    

    (3) 啓動工程,訪問http://localhost:8764/testLoadBalancer,結果展示如下

    example.com:80
    google.com:80
    
  • 結論

    (1) Ribbon通過LoadBalancerClient從註冊中心獲取所有註冊服務信息,並緩存至Ribbon本地

    (2) LoadBalancerClient的choose根據傳入的serviceId從註冊服務列表中獲取服務實例信息(ip及端口)

    (3) 如果禁止Ribbon從Eureka獲取註冊列表信息,則需自己維護一份服務註冊列表信息,根據自己維護的註冊列表信息,實現負載均衡

4.Ribbon源碼簡單分析

  • Ribbon負載均衡過程

    (1) 通過LoadBalancerAutoConfiguration類中如下代碼,維護一個RestTemplate列表,同時初始化時,給每個restTemplate對象增加一個攔截器

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();
    

    (2) 攔截器主要對每個請求路徑進行解析,最後將解析出的serviceId(serviceName)將給LoadBalancerClient處理

    @Override
    	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
    			final ClientHttpRequestExecution execution) throws IOException {
    		final URI originalUri = request.getURI();
    		String serviceName = originalUri.getHost();
    		Assert.state(serviceName != null,
    				"Request URI does not contain a valid hostname: " + originalUri);
    		return this.loadBalancer.execute(serviceName,
    				this.requestFactory.createRequest(request, body, execution));
    	}
    

    (3) LoadBalancerClient則會根據傳入的serviceId獲取服務註冊列表、緩存服務註冊列表、檢測服務註冊列表是否變化、ping下游服務是否可用、根據配置的負載均衡策略進行服務調用

  • LoadBalancerClient實現功能過程

    (1) 核心類LoadBalancerClient的choose()最終通過ILoadBalancer的實現類DynamicServerListLoadBalancer實現

    (2) DynamicServerListLoadBalancer構造方法中需要初始:IClientConfigIRuleIPingServerListServerListFilter五個屬性

    屬性類 作用
    IClientConfig 獲取配置負載均衡客戶端
    IRule 配置負載均衡策略
    IPing 檢測負載均衡的服務是否可用
    ServerList 獲取註冊中心服務註冊列表
    ServerListFilter 根據配置去過濾或動態獲取符合條件的server列表方法

    (3) 負載均衡策略

    負載均衡策略的選擇是根據IRule子類完成的,默認走的是輪詢策略,常見的幾個實現類如下表所示:

    類名 作用
    BestAvailableRule 選擇最小請求數
    ClientConfigEnabledRoundRobinRule 輪詢
    RandomRule 隨機
    RetryRule 根據輪詢方式重試
    WeightedResponseTimeRule 根據響應時間分配權重,權重越低,被選擇可能性越低
    ZoneAvoidanceRule 根據server的zone區域和可用性來輪訓選擇

    IRule有3個方法,分別是choose(serviceId),setLoadBalancer(),getLoadBalancer()三個方法,分別是根據servcieId獲取服務實例信息,設置和獲取ILoadBalancer

    (4) 檢測要負載均衡的服務是否可用(IPing),Iping通過其子類完成服務檢測,主要由如下幾個子類實現:

    類名 作用
    PingUrl 真實地去ping某個url
    PingConstant 固定返回某個服務是否可用,默認爲true
    NoOpPing 不去ping,直接返回true,即可用
    DummyPing 直接返回true,並實現了initWithNiwsConfig方法
    NIWSDiscoveryPing 根據DiscoveryEnabledServer的instanceInfo的InstanceStatus去判斷,如果InstanceStatus.UP,則可用,否則不可用

    (5) 獲取註冊服務列表(serverList)

    方法名 作用
    DynamicServerListLoadBalancer構造函數 初始化上述所需屬性
    DynamicServerListLoadBalancer->initWithNiwsConfig() 初始化信息
    initWithNiwsConfig->restOfInit->updateListOfServers() 獲取服務註冊列表
    updateListOfServers->serverListImpl.getUpdatedListOfServers() 具體獲取註冊列表服務實現
    serverListImpl實現ServerList,實現類DiscoveryEnabledServer->obtainServersViaDiscovery->eurekaClientProvider.get() 獲取Eureka中註冊的服務
    LegacyEurekaClientProvider->get() 具體實現

    最終獲取服務列表代碼

    class LegacyEurekaClientProvider implements Provider<EurekaClient> {
    
        private volatile EurekaClient eurekaClient;
    
        @Override
        public synchronized EurekaClient get() {
            if (eurekaClient == null) {
                eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient();
            }
    
            return eurekaClient;
        }
    }
    

    (6) 負載均衡獲取服務服務註冊時間
    BaseLoadBalancer構造函數中有一方法setupPingTask(),方法具體實現如下,根據如下代碼可知,每隔10秒向EurekaClient發送一次心跳檢測。

    void setupPingTask() {
        if (canSkipPing()) {
            return;
        }
        if (lbTimer != null) {
            lbTimer.cancel();
        }
        lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
                                           true);
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
        forceQuickPing();
    }
    

    (7) 查看定時任務代碼,分析心跳檢測具體實現
    查看pingTask()方法,主要實現邏輯在runPinger(),而在runPinger()方法中通過pingerStrategy.pingServers(ping, allServers)獲取服務可用性,檢測是否與之前相同,如果相同則不拉取,如果不同則調用notifyServerStatusChangeListener(changedServers);向註冊中心拉取服務列表,最終實現本地服務註冊列表的更新

    class PingTask extends TimerTask {
        public void run() {
            try {
                new Pinger(pingStrategy).runPinger();
            } catch (Exception e) {
                logger.error("LoadBalancer [{}]: Error pinging", name, e);
            }
        }
    }
    ----------------------------------------------------------------------------------------
     public void runPinger() throws Exception {
        	...省略
            results = pingerStrategy.pingServers(ping, allServers);
    
            final List<Server> newUpList = new ArrayList<Server>();
            final List<Server> changedServers = new ArrayList<Server>();
    
            for (int i = 0; i < numCandidates; i++) {
                boolean isAlive = results[i];
                Server svr = allServers[i];
                boolean oldIsAlive = svr.isAlive();
    
                svr.setAlive(isAlive);
    
                if (oldIsAlive != isAlive) {
                    changedServers.add(svr);
                    logger.debug("LoadBalancer [{}]:  Server [{}] status changed to {}", 
                                 name, svr.getId(), (isAlive ? "ALIVE" : "DEAD"));
                }
    
                if (isAlive) {
                    newUpList.add(svr);
                }
            }
            upLock = upServerLock.writeLock();
            upLock.lock();
            upServerList = newUpList;
            upLock.unlock();
    
            notifyServerStatusChangeListener(changedServers);
        } finally {
            pingInProgress.set(false);
        }
    }
    

5.Ribbon配置

5.1 設置全局策略

1.創建配置類
2.根據需求創建所需策略類

public class RibbonConfiguration{
	@Bean
	public IRule ribbonRule(){
	 	return new RandomRule();
}
}

5.2 定製化策略

1.創建配置類
2.自定義註解
3.啓動類中排除自定義配置類

  • 配置類
    @Configuration
    @AvoidScan
    public class RibbonConfiguration {
    
      @Bean
      public IRule ribbonRule(){
        return new RandomRule();
      }
    }
    
  • 啓動類,排除自定義配置
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    @RibbonClient(name="provider-service",configuration = RibbonConfiguration.class)
    @ComponentScan(excludeFilters = {@ComponentScan.Filter(type= FilterType.ANNOTATION,value = {AvoidScan.class})})
    public class RibbonFeignApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(RibbonFeignApplication.class, args);
      }
    }
    

5.3 配置方式配置策略

下表爲配置策略相關類

配置項 說明
clientName.ribbon.NFLoadBalancerClassName 指定ILoadBalancer的實現類
clientName.ribbon.NFLoadBalancerRuleClassName 指定IRule的實現類
clientName.ribbon.NFLoadBalancerPingClassName 指定IPing的實現類
clientName.ribbon.NFWSServerListClassName 指定ServerList的實現類
clientName.ribbon.NIWSServerListFilterClassName 指定ServerListFilter的實現類
provider-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    ConnectTimeout: 30000
    ReadTimeout: 30000
    MaxAutoRetries: 1 #對第一次請求的服務重試次數
    MaxAutoRetriesNextServer: 1 #要重試的下一個服務的最大數量(不包括第一個服務)
    OkToRetryOnAllOperations: true
ribbon:
  eager-load: # ribbon的飢餓加載,應對第一次調用加載時間長,超時而調用不成功情況
    enabled: true
    clients: provider-service

6.參考資料

  • 《重新定義Springcloud實戰》
  • 《Springcloud微服務實戰》
  • 《深入理解Spring Cloud與微服務構建》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章