1. Ribbon简介与使用

目前主流的负载方案分为以下两种:

  • 集中式负载均衡,在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件的(比如 F5),也有软件的(比如 Nginx)。
  • 客户端负载均衡,根据自己的请求情况做负载,Ribbon就属于客户端自己做负载。

SpringCloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
SpringCloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个SpringCloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的

Ribbon模块

名 称 说 明
ribbon-loadbalancer 负载均衡模块,可独立使用,也可以和别的模块一起使用。
Ribbon 内置的负载均衡算法都实现在其中。
ribbon-eureka 基于Eureka封装的模块,能够快速、方便地集成Eureka。
ribbon-transport 基于Netty实现多协议的支持,比如HTTP、Tcp、Udp等。
ribbon-httpclient 基于Apache HttpClient封装的REST客户端,集成了负载均衡模块,可以直接在项目中使用来调用接口。
ribbon-example Ribbon使用代码示例,通过这些示例能够让你的学习事半功倍。
ribbon-core 些比较核心且具有通用性的代码,客户端API的一些配置和其他API的定义

Ribbon使用

Ribbon的负载均衡,主要通过LoadBalancerClient来实现的(RibbonLoadBalancerClient),而LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等信息,并向EurekaClient获取注册列表的信息,并默认10秒一次向EurekaClient发送“ping”,进而检查是否更新服务列表,最后,得到注册列表后,ILoadBalancer根据IRule的策略进行负载均衡。我们使用Ribbon来实现一个最简单的负载均衡调用功能:服务端,启动两个服务,一个8081端口,一个8082端口。

  1. import org.springframework.web.bind.annotation.GetMapping;
  2. import org.springframework.web.bind.annotation.RestController;
  3. import javax.management.MBeanServer;
  4. import javax.management.MalformedObjectNameException;
  5. import javax.management.ObjectName;
  6. import javax.management.Query;
  7. import javax.swing.text.html.parser.Entity;
  8. import java.lang.management.ManagementFactory;
  9. import java.util.Iterator;
  10. import java.util.Map;
  11. import java.util.Properties;
  12. import java.util.Set;
  13. @RestController
  14. public class RibbonTestController {
  15. @GetMapping("/ribbon/hello")
  16. public String testRibbom() throws MalformedObjectNameException {
  17. // 獲取當前服務的端口號
  18. MBeanServer beanServer= ManagementFactory.getPlatformMBeanServer();
  19. Set<ObjectName> objectNames = beanServer.queryNames(new ObjectName("*:type=Connector,*"),
  20. Query.match(Query.attr("protocol"), Query.value("HTTP/1.1")));
  21. String port = objectNames.iterator().next().getKeyProperty("port");
  22. System.out.println("=====================");
  23. System.out.println("port:"+port);
  24. return port+":hello";
  25. }
  26. }

客户端:创建maven项目,引入依赖。

  1. <dependency>
  2. <groupId>com.netflix.ribbon</groupId>
  3. <artifactId>ribbon</artifactId>
  4. <version>2.2.2</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.netflix.ribbon</groupId>
  8. <artifactId>ribbon-core</artifactId>
  9. <version>2.2.2</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>com.netflix.ribbon</groupId>
  13. <artifactId>ribbon-loadbalancer</artifactId>
  14. <version>2.2.2</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>io.reactivex</groupId>
  18. <artifactId>rxjava</artifactId>
  19. <version>1.0.10</version>
  20. </dependency>

编写客户端调用服务。

import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import com.google.common.collect.Lists;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.LoadBalancerBuilder;
import com.netflix.loadbalancer.RandomRule;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.reactive.LoadBalancerCommand;
import com.netflix.loadbalancer.reactive.ServerOperation;
import rx.Observable;
public class RibbonTest {
 public static void main(String[] args) throws Exception {

   //服務列表
   List<Server>  serverList=Lists.newArrayList(new Server("localhost",8081),new Server("localhost",8082));
    //构建负载均衡实例
   ILoadBalancer  loadBalance=LoadBalancerBuilder.newBuilder().withRule(new RandomRule())
                           .buildFixedServerListLoadBalancer(serverList);
    // 调用 10 次来测试效果
    for(int i=0;i<10;i++) {
        String result=LoadBalancerCommand.<String>builder()
         .withLoadBalancer(loadBalance).build()
         .submit(new ServerOperation<String>() {

            public Observable<String> call(Server server) {
                try {
                  String addr="http://"+server.getHost()+":"+server.getPort()+"/ribbon/hello";
                  System.out.println("调用地址为:"+addr);
                  URL url=new URL(addr);
                  HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                              conn.setRequestMethod("GET");
                              conn.connect();
                              InputStream is= conn.getInputStream();                            
                              byte[] data=new byte[is.available()];
                              is.read(data);
                              return  Observable.just(new String(data));
                }catch(Exception e) {
                  return Observable.error(e);
                }
            }
        }).toBlocking().first();
        System.out.println(" 调用结果:" + result);
    }   
 }
}

上述这个例子主要演示了Ribbon如何去做负载操作,调用接口用的最底层的HttpURLConnection。当然你也可以用别的客户端,或者直接用RibbonClient执行程序,可以看到控制台输出的结果如下:

调用地址为:http://localhost:8082/ribbon/hello
 调用结果:8082:hello
调用地址为:http://localhost:8081/ribbon/hello
 调用结果:8081:hello
调用地址为:http://localhost:8081/ribbon/hello
 调用结果:8081:hello
调用地址为:http://localhost:8081/ribbon/hello
 调用结果:8081:hello
调用地址为:http://localhost:8081/ribbon/hello
 调用结果:8081:hello
调用地址为:http://localhost:8082/ribbon/hello
 调用结果:8082:hello
调用地址为:http://localhost:8081/ribbon/hello
 调用结果:8081:hello
调用地址为:http://localhost:8082/ribbon/hello
 调用结果:8082:hello
调用地址为:http://localhost:8081/ribbon/hello
 调用结果:8081:hello
调用地址为:http://localhost:8081/ribbon/hello
 调用结果:8081:hello

2. Ribbon集成RestTemplate

RestTemplate是从Spring3.0开始支持的一个HTTP请求工具,它提供了常见的REST请求方案的模版。

RestTemplate访问服务示例

编写配置类:

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

编写控制层:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class RestTemplateClientController {

    @Autowired
    private RestTemplate restTemplate;
    @GetMapping("/rest/hello")
    public String  restTest(){
       return restTemplate.getForObject("http://localhost:8082/ribbon/hello",String.class);
    }

}

RestTemplate负载均衡示例

前面我们调用接口都是通过具体的接口地址来进行调用,RestTemplate可以结合Eureka/Nacos来动态发现服务并进行负载均衡的调用。在SpringCloud项目中集成Ribbon只需要在pom.xml中加入下面的依赖即可,其实也可以不用配置,因为Eureka/Nacos中已经引用了Ribbon,代码如下所示。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

修改RestTemplate的配置,增加能够让RestTemplate具备负载均衡能力的注解“@LoadBalanced”。

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

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

}

修改接口调用的代码,将“${IP}+${PORT}”改成服务名称,也就是注册到Eureka/Nacos中的名称,代码如下所示。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class RestTemplateClientController {

    @Autowired
    private RestTemplate restTemplate;
    @GetMapping("/rest/hello")
    public String  restTest(){
        String serverName="service-product";
       return restTemplate.getForObject("http://"+serverName+"/ribbon/hello",String.class);
    }

}

接口调用的时候,框架内部会将服务名称替换成具体的服务IP信息,然后进行调用。

注解原理(@LoadBalanced)

为什么在RestTemplate上加了一个“@LoadBalanced”之后,RestTemplate就能够跟Eureka结合了,不但可以使用服务名称去调用接口,还可以负载均衡?其原因:RestTemplate被“@LoadBalance”注解后,能使用负载均衡,主要是维护了一个被“@LoadBalance”注解的RestTemplate列表,并给列表中的RestTemplate添加拦截器,进而交给负载均衡器去处理。
🌲 Ribbon基础入门 - 图1
从源代码(spring-cloud-commons.jar,**org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration**)里面通过查看“LoadBalancerAutoConfiguration”的源码,可以看到这里也是维护了一个“@LoadBalanced”的RestTemplate列表,代码如下所示。

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

通过查看拦截器的配置可以知道,拦截器用的是LoadBalancerInterceptor,RestTemplate Customizer用来添加拦截器,代码如下所示。

@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
    @Bean
    public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient,
            LoadBalancerRequestFactory requestFactory) {
        return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    }
    @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);
            }
        };
    }
}

拦截器的代码在“org.springframework.cloud.client.loadbalancer.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) {
        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));
    }

}

3. Ribbon负载均衡策略

名 称 说 明
BestAvailabl 选择一个最小的并发请求的Server,逐个考察Server,如果Server被标记为错误,则跳过,然后再选择ActiveRequestCount中最小的Server。
AvailabilityFilteringRule 过滤掉那些一直连接失败的且被标记为circuit tripped的后端Server,并过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate来包含过滤Server的逻辑。其实就是检查Status里记录的各个Server的运行状态。
ZoneAvoidanceRule 使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个Server,前一个判断判定一个Zone的运行性能是否可用,剔除不可用的Zone(的所有Server),AvailabilityPredicate用于过滤掉连接数过多的Server。
RandomRule 随机选择一个Server。
RoundRobinRule 轮询选择,轮询index,选择index对应位置的Server。
RetryRule 对选定的负载均衡策略机上重试机制,也就是说当选定了某个策略进行请求负载时在一个配置时间段内若选择Server不成功,则一直尝试使用subRule的方式选择一个可用的Server。
ResponseTimeWeightedRule 作用同WeightedResponseTimeRule,ResponseTime-Weighted Rule后来改名为WeightedResponseTimeRule。
WeightedResponseTimeRule 根据响应时间分配一个Weight(权重),响应时间越长,Weight越小,被选中的可能性越低。

4. Ribbon自定义负载均衡策略

通过实现IRule接口可以自定义负载策略,主要的选择服务逻辑在choose方法中。我们这边只是演示怎么自定义负载策略,所以没写选择的逻辑,直接返回服务列表中第一个服务。具体代码如下所示:

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import java.util.*;

public class MyRule implements IRule {

    private ILoadBalancer iLoadBalancer;

    @Override
    public Server choose(Object o) {
        List<Server> serverList=iLoadBalancer.getAllServers();
        System.out.println("====服务列表");
        for(Server server:serverList){
            System.out.println(server.getHostPort());
        }
        return serverList.get(0);
    }

    @Override
    public void setLoadBalancer(ILoadBalancer iLoadBalancer) {
        this.iLoadBalancer=iLoadBalancer;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return this.iLoadBalancer;
    }

}

在SpringCloud中,可通过配置的方式使用自定义的负载策略,service-product是调用的服务名称。

service-product.ribbon.NFLoadBalancerRuleClassName=com.smart.rule.MyRule

5. Ribbon配置详解

常用配置

# 禁用 Eureka
ribbon.eureka.enabled=false
# 禁用 Eureka 后手动配置服务地址
service-product.ribbon.listOfServers=localhost:8081,localhost:8082
# Ribbon默认的策略是轮询
service-product.ribbon.NFLoadBalancerRuleClassName=com.smart.rule.MyRule
# 请求连接的超时时间
ribbon.ConnectTimeout=2000
# 请求处理的超时时间
ribbon.ReadTimeout=5000
# 也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
service-product.ribbon.ConnectTimeout=2000
service-product.ribbon.ReadTimeout=5000
# 最大连接数
ribbon.MaxTotalConnections=500
# 每个host最大连接数
ribbon.MaxConnectionsPerHost=500
<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer(负载均衡器操作接口)
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule(负载均衡算法)
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing(服务可用性检查)
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList(服务列表获取)
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerList•Filter(服务列表的过滤)

代码配置Ribbon

通过代码方式来配置之前自定义的负载策略。

import com.netflix.loadbalancer.IRule;
import com.smart.rule.MyRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class IRuleConfig {

    @Bean
    public IRule  getIRule(){
        return new MyRule();
    }

}

可以去掉之前配置文件中的策略配置,然后重启服务,访问接口即可看到和之前一样的效果。

重试机制

在集群环境中,用多个节点来提供服务,难免会有某个节点出现故障。用Nginx做负载均衡的时候,如果你的应用是无状态的、可以滚动发布的,也就是需要一台台去重启应用,这样对用户的影响其实是比较小的,因为Nginx在转发请求失败后会重新将该请求转发到别的实例上去。
由于Eureka是基于AP原则构建的,牺牲了数据的一致性,每个Eureka服务都会保存注册的服务信息,当注册的客户端与Eureka的心跳无法保持时,有可能是网络原因,也有可能是服务挂掉了。在这种情况下,Eureka中还会在一段时间内保存注册信息。这个时候客户端就有可能拿到已经挂掉了的服务信息,故Ribbon就有可能拿到已经失效了的服务信息,这样就会导致发生失败的请求。这种问题我们可以利用重试机制来避免。重试机制就是当Ribbon发现请求的服务不可到达时,重新请求另外的服务。

  1. RetryRule重试解决上述问题,最简单的方法就是利用Ribbon自带的重试策略进行重试,此时只需要指定某个服务的负载策略为重试策略即可。

    service-product.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
    
  2. Spring Retry重试除了使用Ribbon自带的重试策略,我们还可以通过集成Spring Retry来进行重试操作。在pom.xml中添加Spring Retry的依赖,如下所示。

    <dependency>
     <groupId>org.springframework.retry</groupId>
     <artifactId>spring-retry</artifactId>
    </dependency>
    

    配置重试次数等信息:

    # 对当前实例的重试次数
    ribbon.maxAutoRetries=1
    # 切换实例的重试次数
    ribbon.maxAutoRetriesNextServer=3
    # 对所有操作请求都进行重试
    ribbon.okToRetryOnAllOperations=true
    # 对Http响应码进行重试
    ribbon.retryableStatusCodes=500,404,502
    

    6. 代码示例

    Ribbon.demo.zip

    7. 问题

    1. Ribbon饥饿加载

  • 描述服务都成功启动的时候第一次访问会有报错的情况发生,但是之后又恢复正常访问。
  • 原因:主要是Ribbon进行客户端负载均衡的Client并不是在服务启动的时候就初始化好的,而是在调用的时候才会去创建相应的Client,所以第一次调用的耗时不仅仅包含发送HTTP请求的时间,还包含了创建RibbonClient的时间,这样一来如果创建时间速度较慢,同时设置的超时时间又比较短的话,很容易就会出现上面所描述的显现。
  • 解决方案