Spring Cloud(十四):Ribbon實現客戶端負載均衡及其實現原理介紹

年後到如今一直很忙,都沒什麼時間記錄東西了,其實以前工做中積累了不少知識點,一直都堆在備忘錄裏,只是由於近幾個月經歷了一些事情,沒有太多的經從來寫了,可是一些重要的東西,我仍是但願能堅持記錄下來。正好最近公司用到了一些本篇文章的知識點,因此就抽空記錄一下。html

本文代碼github地址:https://github.com/shaweiwei/RibbonTest/tree/masternginx

簡介

ribbon 是一個客戶端負載均衡器,它和nginx的負載均衡相比,區別是一個是客戶端負載均衡,一個是服務端負載均衡。ribbon能夠單獨使用,也能夠配合eureka使用。git

使用

單獨使用

1.首先咱們先在原來的基礎上新建一個Ribbon模塊,以下圖:github

如今咱們單獨使用ribbon,在Ribbon模塊下添加依賴,以下圖所示:算法

<dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-ribbon</artifactId>
       <version>1.4.0.RELEASE</version>
</dependency>

 修改application.yml文件,以下所示:spring

複製代碼
server:
  port: 8082
spring:
  application:
    name: Ribbon-Consumer
#providers這個是本身命名的,ribbon,listOfServer這兩個是規定的
providers:
  ribbon:
    listOfServers: localhost:8080,localhost:8081
複製代碼

在Ribbon模塊下新建一個測試類以下代碼 * Created by cong on 2018/5/8. */瀏覽器

複製代碼
@RestController
public class ConsumerController {

  //注入負載均衡客戶端  @Autowired
private LoadBalancerClient loadBalancerClient; @RequestMapping("/consumer") public String helloConsumer() throws ExecutionException, InterruptedException {
     //這裏是根據配置文件的那個providers屬性取的 ServiceInstance serviceInstance = loadBalancerClient.choose("providers");
      //負載均衡算法默認是輪詢,輪詢取得服務 URI uri = URI.create(String.format("http://%s:%s", serviceInstance.getHost(), serviceInstance.getPort())); return uri.toString();
  }
複製代碼

運行結果以下:併發

  會輪詢的獲取到兩個服務的URL 訪問第一次,瀏覽器出現http://localhost:8080  訪問第二次就會出現http://localhost:8081app

在eureka環境下使用

下面這個例子是在以前這篇文章的例子上改的,Spring Cloud(二):Spring Cloud Eureka Server高可用註冊服務中心的配置負載均衡

先看下寫好的結構

先介紹下大體功能,EurekaServer提供服務註冊功能,RibbonServer裏會調用ServiceHello裏的接口,ServiceHello和ServiceHello2是一樣的服務,只是爲了方便分佈式部署。

EurekaServer

pom依賴

<dependencies>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-eureka-server</artifactId>
      </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
  </dependencies>

BootApplication

@SpringBootApplication
@EnableEurekaServer
public class BootApplication {
    public static void main(String[] args) {
        SpringApplication.run(BootApplication.class, args);
    }
    
}

application.properties

server.port=8760
spring.application.name=eureka-server
#eureka.instance.hostname=peer1
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka

RibbonServer

BootApplication

紅色部分代碼是關鍵

@SpringBootApplication
@EnableDiscoveryClient
@RestController
@RibbonClients(value={
        @RibbonClient(name="service-hi",configuration=RibbonConfig.class)
})
public class BootApplication {
    public static void main(String[] args) {
        SpringApplication.run(BootApplication.class, args);
    }
}

RibbonConfig

@Configuration
public class RibbonConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
    
    @Bean
    public IRule ribbonRule() {
        return new RoundRobinRule();
    }
}

TestController

@RestController
public class TestController {


    @Autowired
    @LoadBalanced private  RestTemplate restTemplate;
    @Autowired
    SpringClientFactory springClientFactory;

    @RequestMapping("/consumer")
    public String helloConsumer() throws ExecutionException, InterruptedException {
        ILoadBalancer loadBalancer = springClientFactory.getLoadBalancer("service-hi");
        List<Server> servers = loadBalancer.getReachableServers();
        System.out.println(",......"+servers.size());
        return restTemplate.getForEntity("http://service-hi/hi",String.class).getBody();
    }
}

application.properties

server.port=8618
spring.application.name=ribbon-service
eureka.client.serviceUrl.defaultZone=http://localhost:8760/eureka/

ServiceHello

 

 BootApplication

@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class BootApplication {
    public static void main(String[] args) {
        SpringApplication.run(BootApplication.class, args);
    }
    
    
    
    @RequestMapping(value="/hi",method=RequestMethod.GET)
    public String hi(){
        return "hi";
    }
    

}

application.properties

server.port=8788
spring.application.name=service-hi
eureka.client.serviceUrl.defaultZone=http://localhost:8760/eureka/
#service-hi.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

ServiceHello2

和ServiceHello同樣,只是端口不一樣,另外爲了區分,接口hi返回的值也改爲不同。

查看結果

而後就是分別啓動各個服務。

查看eureka信息,能夠看到服務都啓動了。

瀏覽器裏輸入http://localhost:8618/consumer,多調用幾回,能夠看到分別結果是hi和hi2交替出現。

這說明負載均衡實現了,並且我選擇的負載均衡策略是輪詢,因此hi和hi2確定是交替出現。

負載均衡策略

Ribbon的核心組件是IRule,是全部負載均衡算法的父接口,其子類有:

每個類就是一種負載均衡算法

RoundRobinRule 輪詢
RandomRule 隨機
AvailabilityFilteringRule 會先過濾掉因爲屢次訪問故障而處於斷路器跳閘狀態的服務,還有併發的鏈接數超過閾值的服務,而後對剩餘的服務列表進行輪詢
WeightedResponseTimeRule 權重 根據平均響應時間計算全部服務的權重,響應時間越快服務權重越大被選中的機率越高。剛啓動時,若是統計信息不足,則使用輪詢策略,等信息足夠,切換到 WeightedResponseTimeRule
RetryRule 重試 先按照輪詢策略獲取服務,若是獲取失敗則在指定時間內重試,獲取可用服務
BestAvailableRule 選過濾掉屢次訪問故障而處於斷路器跳閘狀態的服務,而後選擇一個併發量最小的服務
ZoneAvoidanceRule 符合判斷server所在區域的性能和server的可用性選擇服務

原理與源碼分析

ribbon實現的關鍵點是爲ribbon定製的RestTemplate,ribbon利用了RestTemplate的攔截器機制,在攔截器中實現ribbon的負載均衡。負載均衡的基本實現就是利用applicationName從服務註冊中心獲取可用的服務地址列表,而後經過必定算法負載,決定使用哪個服務地址來進行http調用。

Ribbon的RestTemplate

RestTemplate中有一個屬性是List<ClientHttpRequestInterceptor> interceptors,若是interceptors裏面的攔截器數據不爲空,在RestTemplate進行http請求時,這個請求就會被攔截器攔截進行,攔截器實現接口ClientHttpRequestInterceptor,須要實現方法是

ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
      throws IOException;

也就是說攔截器須要完成http請求,並封裝一個標準的response返回。

ribbon中的攔截器

在Ribbon 中也定義了這樣的一個攔截器,而且注入到RestTemplate中,是怎麼實現的呢?

在Ribbon實現中,定義了一個LoadBalancerInterceptor,具體的邏輯先不說,ribbon就是經過這個攔截器進行攔截請求,而後實現負載均衡調用。

攔截器定義在org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.LoadBalancerInterceptorConfig#ribbonInterceptor

 

@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
   @Bean
    //定義ribbon的攔截器
   public LoadBalancerInterceptor ribbonInterceptor(
         LoadBalancerClient loadBalancerClient,
         LoadBalancerRequestFactory requestFactory) {
      return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
   }

   @Bean
   @ConditionalOnMissingBean
    //定義注入器,用來將攔截器注入到RestTemplate中,跟上面配套使用
   public RestTemplateCustomizer restTemplateCustomizer(
         final LoadBalancerInterceptor loadBalancerInterceptor) {
      return restTemplate -> {
               List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                       restTemplate.getInterceptors());
               list.add(loadBalancerInterceptor);
               restTemplate.setInterceptors(list);
           };
   }
}

ribbon中的攔截器注入到RestTemplate

定義了攔截器,天然須要把攔截器注入到、RestTemplate才能生效,那麼ribbon中是如何實現的?上面說了攔截器的定義與攔截器注入器的定義,那麼確定會有個地方使用注入器來注入攔截器的。

在org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration#loadBalancedRestTemplateInitializerDeprecated方法裏面,進行注入,代碼以下。

 

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

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

   @Bean
   public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
         final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
       //遍歷context中的注入器,調用注入方法。
      return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        });
   }
   //......
   }

遍歷context中的注入器,調用注入方法,爲目標RestTemplate注入攔截器,注入器和攔截器都是咱們定義好的。

還有關鍵的一點是:須要注入攔截器的目標restTemplates究竟是哪一些?由於RestTemplate實例在context中可能存在多個,不可能全部的都注入攔截器,這裏就是@LoadBalanced註解發揮做用的時候了。

LoadBalanced註解

嚴格上來講,這個註解是spring cloud實現的,不是ribbon中的,它的做用是在依賴注入時,只注入實例化時被@LoadBalanced修飾的實例。

例如咱們定義Ribbon的RestTemplate的時候是這樣的

@Bean
    @LoadBalanced
    public RestTemplate rebbionRestTemplate(){
        return new RestTemplate();
    }


所以才能爲咱們定義的RestTemplate注入攔截器。

那麼@LoadBalanced是如何實現這個功能的呢?其實都是spring的原生操做,@LoadBalance的源碼以下

/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}


很明顯,‘繼承’了註解@Qualifier,咱們都知道之前在xml定義bean的時候,就是用Qualifier來指定想要依賴某些特徵的實例,這裏的註解就是相似的實現,restTemplates經過@Autowired注入,同時被@LoadBalanced修飾,因此只會注入@LoadBalanced修飾的RestTemplate,也就是咱們的目標RestTemplate。

攔截器邏輯實現
LoadBalancerInterceptor源碼以下。

 

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        // for backwards compatibility
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    @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, requestFactory.createRequest(request, body, execution));
    }
}

攔截請求執行

@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
   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);
}

咱們重點看getServer方法,看看是如何選擇服務的

protected Server getServer(ILoadBalancer loadBalancer) {
   if (loadBalancer == null) {
      return null;
   }
    //
   return loadBalancer.chooseServer("default"); // TODO: better handling of key
}


代碼配置隨機loadBlancer,進入下面代碼

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

 


這裏配置的是RandomRule,因此進入RandomRule代碼

public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        return null;
    }
    Server server = null;

    while (server == null) {
        if (Thread.interrupted()) {
            return null;
        }
        //獲取可用服務列表
        List<Server> upList = lb.getReachableServers();
        List<Server> allList = lb.getAllServers();

        //隨機一個數
        int serverCount = allList.size();
        if (serverCount == 0) {
            /*
             * No servers. End regardless of pass, because subsequent passes
             * only get more restrictive.
             */
            return null;
        }

        int index = rand.nextInt(serverCount);
        server = upList.get(index);

        if (server == null) {
            /*
             * The only time this should happen is if the server list were
             * somehow trimmed. This is a transient condition. Retry after
             * yielding.
             */
            Thread.yield();
            continue;
        }

        if (server.isAlive()) {
            return (server);
        }

        // Shouldn't actually happen.. but must be transient or a bug.
        server = null;
        Thread.yield();
    }

    return server;

}

 

 

隨機負載規則很簡單,隨機整數選擇服務,最終達到隨機負載均衡。咱們能夠配置不一樣的Rule來實現不一樣的負載方式。

相關文章
相關標籤/搜索