四、負載均衡Ribbon

上一章講述了服務注冊和發現組件Eureka,同時追蹤源碼深入講解了Eureka的機制,并通過案例講解了如何構建高可用的EurekaServer。本章講解如何使用 RestTemplate和Ribbon相結合作為服務消費者去消費服務,同時從源碼的角度來深入講解 Ribbon。

一、RestTemplate簡介

??RestTemplate是Spring Resources中一個訪問第三方RESTful API接口的網絡請求框架。RestTemplate的設計原則和其他SpringTemplate (例如 JdbcTemplate、 JmsTemplate)類似,都是為執行復雜任務提供了一個具有默認行為的簡單方法。
??RestTemplate是用來消費REST服務的,所以RestTemplate 的主要方法都與REST的Http協議的一些方法緊密相連,例如HEAD、GET、POST、PUT、DELETE和OPTIONS等方法, 這些方法在RestTemplate類對應的方法為headForHeaders()、getForObject()、postForObject()、put()和delete()等。

二、Ribbon簡介

??負載均衡是指將負載分攤到多個執行單元上,常見的負載均衡有兩種方式。一種是獨立進程單元,通過負載均衡策略,將請求轉發到不同的執行單元上,例如Ngnix。另一種是將負載均衡邏輯以代碼的形式封裝到服務消費者的客戶端上,服務消費者客戶端維護了一份服務提供者的信息列表,有了信息列表,通過負載均衡策略將請求分攤給多個服務提供者,從而達到負載均衡的目的。
??Ribbon是Netflix公司開源的一個負載均衡的組件,它屬于上述的第二種方式,是將負載均衡邏輯封裝在客戶端中,并且運行在客戶端的進程里。Ribbon是一個經過了云端測試的IPC庫,可以很好地控制HTTP和TCP 客戶端的負載均衡行為。
??在SpringCloud構建的微服務系統中, Ribbon作為服務消費者的負載均衡器,有兩種使用方式,一種是和RestTemplate相結合,另一種是和Feign相結合。Feign已經默認集成了Ribbon, 關于Feign的內容將會在下一章進行詳細講解。
??Ribbon有很多子模塊,但很多模塊沒有用于生產環境 ,目前Netflix公司用于生產環境的Ribbon子模塊如下:

  • ribbon-loadbalancer: 可以獨立使用或與其他模塊一起使用的負載均衡器API。
  • ribbon-eureka: Ribbon結合Eureka客戶端的API,為負載均衡器提供動態服務注冊列表信息。
  • ribbon-core: Ribbon的核心API

三、使用RestTemplate和Ribbo和Ribbon來消費服務

??本案例是上一節案例的基礎上進行改造的,先回顧一下上一節中的代碼結構,它包括一個服務注冊中心eureka-server、一個服務提供者eureka-client。eureka-client向eureka-server注冊服務,并且eureka-client提供了一個“ /hi"API接口,用于提供服務。
??啟動eureka-server, 端口為8671。啟動兩個eureka-client實例,端口分別為 8762和8763。 啟動完成后,在瀏覽器上訪問 http://localhost:8671/,瀏覽器顯示 eureka-client 的兩個實例已經成功向服務注冊中心注冊,它們的端口分別為8672和8673,如下所示。

??創建完成 eureka-ribbon-client 的 Module 工程之后 , 在其pom文件中引入相關的依賴,包括繼承了主Maven工程的pom文件,引入了EurekaClient的起步依賴spring-cloud-starter- eureka、Ribbon的起步依賴spring-cloud-starter-ribbon,以及Web的起步依賴spring-boot-starter-web, 代碼如下:

<parent>
        <groupId>com.hand</groupId>
        <artifactId>macro-service</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath/>
</parent>
<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

macro-service為主模塊的配置,內容如下:

<groupId>com.hand</groupId>
   <artifactId>macro-service</artifactId>
   <version>1.0-SNAPSHOT</version>
   <packaging>pom</packaging>
  <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>1.5.10.RELEASE</version>
       <relativePath/>
   </parent>
   <properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
       <spring.cloud.vension>Dalston.SR1</spring.cloud.vension>
       <java.version>1.8</java.version>
   </properties>
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>
   <dependencyManagement>
       <dependencies>
           <dependency>
               <groupId>org.springframework.cloud</groupId>
               <artifactId>spring-cloud-dependencies</artifactId>
               <version>${spring.cloud.vension}</version>
               <type>pom</type>
               <scope>import</scope>
           </dependency>
       </dependencies>
   </dependencyManagement>

??在工程的配置文件appIication.yml做程序的相關配置,包括指定程序名為 eureka-ribbon--client,程序的端口號為8674,服務的注冊地址http://localhost:8761/eureka/,代碼如下:

spring:
  application:
    name: eureka-ribbon-client
server:
  port: 8674
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8671/eureka/

??另外,作為EurekaClient需要在程序的入口類加上注解@EnableEurekaClient開啟EurekaClient功能,代碼如下:

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

??寫一個RESTful API接口,在該API接口內部需要調用eureka-client的API接口"/hi”, 即服務消費。由于eureka-client為兩個實例,它們的端口為8672和8673。在調用eureka-client的API接口“/hi”時希望做到輪流訪問這兩個實例,這時就需要將RestTemplate和Ribbon相結合,進行負載均衡。
??首先需要在程序的IoC容器中注入一個 restTemplate的Bean,并在這個Bean上加上@LoadBalanced注解,此時RestTemplate就結合了Ribbon開啟了負載均衡功能 ,代碼如下:

@Configuration
public class RibbonConfig {
    
    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

??寫一個RibbonService類,在該類的 hi()方法用 restTemplate 調用eureka-client的API接口,此時Uri上不需要使用硬編碼(例如IP地址),只需要寫服務名eureka-client 即可,代碼如下:

@Service
public class RibbonService {

    @Autowired
    private RestTemplate restTemplate;

    public String hi(String name) {
        return restTemplate.getForObject("http://eureka-client/hi?name=" + name, String.class);
    }
}

寫一個RibbonController類,為該類加上@RestController注解,開啟RestController的功能, 寫一個“/hi” Get方法的接口,調用RibbonService類的hi()方法,代碼如下:

@RestController
public class RibbonController {

    @Autowired
    private RibbonService ribbonService;

    @GetMapping(value = "/hi")
    public String hi(@RequestParam String name){
        return ribbonService.hi(name);
    }
}

啟動eureka-ribbon-client工程,在瀏覽器上訪問http://localhost:8671,顯示的EurekaServer 的主界面如下圖所示。在主界面上發現有兩個服務被注冊,分別為eureka-client和eureka-ribbon-client,其中eureka-client有兩個實例,端口為8672和8673,而eureka-ribbon-client的端口為8674


在瀏覽器上多次訪問 http://localhost:8674/hi?name=ben,瀏覽器會輪流顯示如下內容:

hi ben, i am from port:8672
hi ben, i am from port:8673

四、LoadBalancerClient簡介

??負載均衡器的核心類為LoadBalancerClient, LoadBalancerCiient可以獲取負載均衡的服務提供者的實例信息。為了演示,在RibbonController重新寫一個接口“/testRibbon”,通過LoadBalancerCIient去選擇一個eureka-client的服務實例的信息,并將該信息返回,繼續在eureka-ribbon-client工程上修改,代碼如下:

@RestController
public class RibbonController {
    ...//省略代碼
    @Autowired
    private LoadBalancerClient loadBalancer;

    @GetMapping ("/testRibbon")
    public String testRibbon() {

        ServiceInstance instance = loadBalancer.choose("eureka-client");
        return instance.getHost() + ":" + instance.getPort();
    }
}

??重新啟動工程,在瀏覽器上多次訪問http://localhost:8764/testRibbon,瀏覽器會輪流顯示如下內容 :

localhost:8672
localhost:8673

??可見,LoadBalancerClient 的 choose(”eureka-client'’)方法可以輪流得到 eureka-client 的兩個 服務實例的信息。
??負載均衡器LoadBalancerClient是從EurekaClient獲取服務注冊列表信息的,并將服務注冊列表信息緩存了一份。在LoadBalancerCJient 調用choose()方法時,根據負載均衡策略選擇一個服務實例的信息,從而進行了負載均衡。 LoadBalancerClient也可以不從EurekaClient獲取注冊列表信息, 這時需要自己維護一份服務注冊列表信息。需要修改application.xml的配置信息,通過stores.ribbon.listOfServers來配置這些服務實例的Uri。

#禁止Ribbon從Eureka獲取注冊列表信息
ribbon:
  eureka:
    enabled: false

#手動配置服務列表
stores:
  ribbon:
    listOfServers: example1.com,example2.com
@RestController
public class RibbonController {
    @Autowired
    private LoadBalancerClient loadBalancer;

    @GetMapping ("/testSelfConfigRibbon")
    public String testSelfConfigRibbon() {
        ServiceInstance instance = loadBalancer.choose("stores");
        return instance.getHost() + ":" + instance.getPort();
    }

啟動工程 ,在瀏覽器上多次訪問內容:http://localhost:8769/testRibbon,瀏覽器會交替出現以下內容:

example1.com:80
example2.com:80

??由此,我們知道在Ribbon中的負載均衡客戶端為LoadBalancerClient。在SpringCloud項目中,負載均衡器Ribbon會默認從EurekaClient的服務注冊列表中獲取服務的信息,并緩存一份。根據緩存的服務注冊列表信息,可以通過LoadBalancerClient來選擇不同的服務實例, 從而實現負載均衡。如果禁止Ribbon從Eureka獲取注冊列表信息,則需要自己去維護一份服務注冊列表信息。根據自己維護服務注冊列表的信息,Ribbon也可以實現負載均衡。

五、源碼解析Ribbon

??為了深入理解Ribbon,通過查看源碼來分析Ribbon如何和RestTemplate相結合來做負載均衡。開啟負載均衡的關鍵在@LoadBalanced這個注解上,首先從這個注解入手。

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

??上面是@LoadBalanced的定義,這就是一個普通的標記注解,作用就是修飾RestTemplate讓其擁有負載均衡的能力,全局搜索發現在LoadBalancerAutoConfiguration.java這個類里用到了。

@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 loadBalancedRestTemplateInitializer(
            final List<RestTemplateCustomizer> customizers) {
        return new SmartInitializingSingleton() {
            @Override
            public void afterSingletonsInstantiated() {
                for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                    for (RestTemplateCustomizer customizer : customizers) {
                        customizer.customize(restTemplate);
                    }
                }
            }
        };
    }
    //這里的restTemplates是所有的被@LoadBalanced注解的集合,這就是標記注解的作用(Autowired是可以集合注入的)
    @Autowired(required = false)
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(
            LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
    }
    //生成一個LoadBalancerInterceptor的Bean
    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient,
                LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }
        //給注解了@LoadBalanced的RestTemplate加上攔截器
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(
                final LoadBalancerInterceptor loadBalancerInterceptor) {
            return new RestTemplateCustomizer() {
                @Override
                public void customize(RestTemplate restTemplate) {
                    List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                            restTemplate.getInterceptors());
                    list.add(loadBalancerInterceptor);
                    restTemplate.setInterceptors(list);
                }
            };
        }
    }
        ...//省略后面代碼
}

??看到這里,我們應該大致知道@loadBalanced的作用了,就是起到一個標記RestTemplate的作用,當服務啟動時,標記了的RestTemplate對象里面就會被自動加入LoadBalancerInterceptor攔截器,這樣當RestTemplate像外面發起http請求時,會被LoadBalancerInterceptor的intercept函數攔截,而intercept里面又調用了LoadBalancerClient接口實現類execute方法,我們接著往下看;

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    ...//省略部分代碼
    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        //這是以服務名為地址的原始請求:例:http://HI-SERVICE/hi
        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));
    }
}

這里的LoadBalancerClient的實現是RibbonLoadBalancerClient,調用的是RibbonLoadBalancerClient.execute()方法。在execute內首先執行getLoadBalancer(serviceId)獲取ILoadBalancer的實現者,然后調用getServer(loadBalancer)方法通過負載均衡策略獲取服務。

@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    //通過serviceId找到ILoadBalancer的實現者
    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(loadBalancer)方法,發現是調用ILoadBalancer實現類對象的chooseServer()方法。

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

這里ILoadBalancer接口有三個實現類,通過查看源碼發現,BaseLoadBalancer和ZoneAwareLoadBalancer類里都有具體的實現方法,到底調用的是哪個類的方法呢?


查看RibbonClientConfiguration.java類發現如下代碼:

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
       ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
           IRule rule, IPing ping) {
       if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
           return this.propertiesFactory.get(ILoadBalancer.class, config, name);
       }
       ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder()
                .withClientConfig(config).withRule(rule).withPing(ping)
                .withServerListFilter(serverListFilter).withDynamicServerList(serverList)
                .buildDynamicServerListLoadBalancer();
       return balancer;
}

由此可知,攔截器里默認調用的是ZoneAwareLoadBalancer.chooseServer()方法。

    public Server chooseServer(Object key) {
        //ENABLED默認值為true
        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 {
            LoadBalancerStats lbStats = getLoadBalancerStats();
            Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
            logger.debug("Zone snapshots: {}", zoneSnapshot);
            if (triggeringLoad == null) {
                triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
            }

            if (triggeringBlackoutPercentage == null) {
                triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
            }
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
            logger.debug("Available zones: {}", availableZones);
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                logger.debug("Zone chosen: {}", zone);
                if (zone != null) {
                    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);
        }
    }

但是由于我是模擬單服務器測試的,所以是單區域,通過調試可以看到空間數為1,如下圖。所以這里會去調用ZoneAwareLoadBalancer父類的chooseServer()方法,也就是BaseLoadBalancer的chooseServer()方法。


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

接下來就是調用rule.choose(key);這里是選擇負載均衡策略。IRule的實現類如下:


IRule的默認實現類有以下7種。在大多數情況下,這些默認的實現類是可以滿足需求的,如果有特殊的諦求,可以自己實現。

  • BestAvailableRule: 選擇最小請求數。
  • ClientConfigEnabledRoundRobinRule:輪詢。
  • RandornRule: 隨機選擇一個server。
  • RoundRobinRule: 輪詢選擇server。
  • RetryRule: 根據輪詢的方式重試。
  • ZoneAvoidanceRule:根據server的zone區域和可用性來輪詢選擇。
  • WeightedResponseTirneRule: 根據響應時間去分配一個weight,weight越低,被選擇的可能性就越低。

??綜上所述,Ribbon的負載均衡,主要通過LoadBalancerClient來實現的,而LoadBalancerClient具體交給了ILoadBalancer來處理,ILoadBalancer通過配置IRule等信息,并向EurekaClient獲取注冊列表的信息,得到注冊列表后,ILoadBalancer根據IRule的策略進行負載均衡。
??RestTemplate 被@LoadBalance注解后,能使用負載均衡,主要是維護了一個被@LoadBalance注解的RestTemplate列表,并給列表中的RestTemplate添加攔截器,進而交給負載均衡器去處理。

總結:本章節學習了Ribbon負載均衡搭配RestTemplate實現的方式,通過學習Ribbon的源碼,深入了解了Ribbon的實現原理和方式,使自己受益匪淺。下一章學習聲明式調用Feign的有關內容。

源代碼:https://github.com/Cheerman/macro-service.git

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容