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


年后到現在一直很忙,都沒什么時間記錄東西了,其實之前工作中積累了很多知識點,一直都堆在備忘錄里,只是因為近幾個月經歷了一些事情,沒有太多的經歷來寫了,但是一些重要的東西,我還是希望能堅持記錄下來。正好最近公司用到了一些本篇文章的知識點,所以就抽空記錄一下。

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

簡介

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

使用

單獨使用

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

現在我們單獨使用ribbon,在Ribbon模塊下添加依賴,如下圖所示:

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

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

復制代碼
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:8081

在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來實現不同的負載方式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM