【Spring Cloud】Ribbon 工作原理与负载均衡策略

本节介绍一下Ribbon的工作原理以及负载均衡的策略都有哪些以及如何配置如何使用,并提供一个简单的例子来介绍Ribbon的基本使用,在这里是单独拿出来写用例测试的,实际生产一般是配置feign一起使用。

我们先来介绍Ribbon的特点以及工作模型,以及负载均衡策略。

Ribbon的特点以及工作模型

目前主流的负载方案分为两种,一种是集中式负载均衡,在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件的,比如F5,也有软件的,比如Nginx。 另一种则是客户端自己做负载均衡,根据自己的请求情况做负载,Ribbon 就是属于客户端自己做负载的。

Ribbon 是 Netflix 开源的一款用于客户端负载均衡的工具软件。默认的策略是轮询,我们可以自定义负载策略来覆盖默认的,当然也可以通过配置指定使用哪些策略。

Ribbon 的特点:

  • 丰富的组件库:整套负载均衡由7个具体策略组成,不管你是什么特殊需求,都有合适的策略供你选择
  • 适配性好:SpringCloud里的五小强(eureka,feign,gateway,zuul,hystrix),谁拿都能用

Ribbon 工作模型

如图所示:

一个 HttpRequest 请求发过来,先被转发到 Eureka 上。此时 Eureka 仍然通过服务发现获取了所有服务节点的物理地址,他不知道该调用哪一个服务节点比较好,只好把请求转到了 Ribbon 上。

然后 Ribbon 主要通过以下两种模式进行工作:

  • IPing IPing 是 Ribbon 的一套 healthcheck 机制。故名思议,就是要 Ping 一下目标机器看是否还在线,一般情况下 IPing 并不会主动向服务节点发起 healthcheck 请求,Ribbon 后台通过静默处理返回true默认表示所有服务节点都处于存活状态(和Eureka集成的时候会检查服节点UP状态)。
  • IRule Ribbon的组件库,各种负载均衡策略都继承自IRule接口。所有经过 Ribbon 的请求都会先请示IRule 一把,找到负载均衡策略选定的目标机器,然后再把请求转发出去。

负载均衡策略

负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡。

策略类 命名 描述
RandomRule 随机策略 随机选择server
RoundRobinRule 轮询策略 按照顺序选择server(ribbon默认策略)
RetryRule 重试策略 在一个配置时间段内,当选择server不成功,则一直尝试选择一个可用的server
BestAvailableRule 最低并发策略 逐个考察server,如果server断路器打开,则忽略,再选择其中并发链接最低的server
AvailabilityFilteringRule 可用过滤策略 过滤掉一直失败并被标记为circuit tripped的server,过滤掉那些高并发链接的server(active connections超过配置的阈值)
ResponseTimeWeightedRule 响应时间加权重策略 根据server的响应时间分配权重,响应时间越长,权重越低,被选择到的概率也就越低。响应时间越短,权重越高,被选中的概率越高,这个策略很贴切,综合了各种因素,比如:网络,磁盘,io等,都直接影响响应时间
ZoneAvoidanceRule 区域权重策略 综合判断server所在区域的性能,和server的可用性,轮询选择server并且判断一个AWS Zone的运行性能是否可用,剔除不可用的Zone中的所有server

微服务架构中使用客户端负载均衡

通过 Spring Cloud Ribbon 的封装,我们在微服务架构中使用客户端负载均衡,只需要如下两步:

  • 服务提供者,只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
  • 服务消费者,直接通过调用被 @LoadBalanced 注解修饰过的 RestTemplate 来实现面向服务的接口调用。

这样,我们就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。

使用Demo

基于zookeeper注册中心+ribbon的方式实现一个简单的客户端负载均衡案例。

服务提供方

首先是一个服务提供方。代码如下。

application.properties配置文件

spring.application.name=discovery-service
server.port=0

service-B.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

bootstrap.properties配置文件

spring.cloud.zookeeper.connect-string=192.168.0.15:2181

引导程序,提供了一个ribbonService的rest接口服务,注册程序到zookeeper中。

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

    @RequestMapping("/ribbonService")
    public String ribbonService(){
        return "hello too ribbon";
    }
}
 
Java
复制

服务调用方

服务调用方就是进行负载均衡的一方,利用ribbo的RestTemplate进行负载调用服务。

RibbonConfig,配置ribbon的RestTemplate,通过@LoadBalanced注解实现,具体原理稍后分析。

@Configuration
public class RibbonConfig {

    /**
     * 实例化ribbon使用的RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate rebbionRestTemplate(){
        return new RestTemplate();
    }
    
    /**
    * 配置随机负载策略,需要配置属性service-B.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
    */
    @Bean
    public IRule ribbonRule() {
        return new RandomRule();
    }
}

 
Java
复制

引导程序

@SpringBootApplication(scanBasePackages = "garine.learn.ribbon.loadblance")
@EnableDiscoveryClient
@RestController
public class TestRibbonApplocation {
    public static void main(String[] args) {
        SpringApplication.run(TestRibbonApplocation.class, args);
    }

    @Autowired
    @LoadBalanced
    RestTemplate restTemplate;

    @GetMapping("/{applicationName}/ribbonService")
    public String ribbonService(@PathVariable("applicationName") String applicationName){
        return restTemplate.getForObject("http://" + applicationName+"/ribbonService", String.class);
    }
}
 
Java
复制

配置文件同上,服务名称修改即可。

测试

  1. 启动两个discovery-service,由于端口设置为0,所以是随机端口。
  2. 启动服务调用方
  3. 浏览器访问服务调用方的提供的接口,路径参数需要加上调用的服务名称,例如http://localhost:8080/discovery-service/ribbonService,然后服务调用方使用ribbon的RestTemplate调用服务提供方的接口。
  4. 结果返回:hello too ribbon ,同时服务提供方启动的两个服务都可能被调用,取决于怎么配置负载策略。

上面就是一个简单使用ribbon的例子,结合feign使用基本上是做类似上面所写的工作,那么ribbon到底是怎么实现的呢?

原理与源码分析

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;
 
Java
复制

也就是说拦截器需要完成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);
           };
   }
}
 
Java
复制

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);
                }
            }
        });
   }
   //......
   }
 
Java
复制

遍历context中的注入器,调用注入方法,为目标RestTemplate注入拦截器,注入器和拦截器都是我们定义好的。

还有关键的一点是:需要注入拦截器的目标restTemplates到底是哪一些?因为RestTemplate实例在context中可能存在多个,不可能所有的都注入拦截器,这里就是@LoadBalanced注解发挥作用的时候了。

LoadBalanced注解

严格上来说,这个注解是spring cloud实现的,不是ribbon中的,它的作用是在依赖注入时,只注入实例化时被@LoadBalanced修饰的实例。

例如我们定义Ribbon的RestTemplate的时候是这样的

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

因此才能为我们定义的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 {
}
 
Java
复制

很明显,‘继承’了注解@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));
    }
}
 
Java
复制

拦截请求执行

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

我们重点看getServer方法,看看是如何选择服务的

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

代码配置随机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;
        }
    }
}
 
Java
复制

这里配置的是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;

}
 
Java
复制

随机负载规则很简单,随机整数选择服务,最终达到随机负载均衡。我们可以配置不同的Rule来实现不同的负载方式。

以上就是Ribbon的简单工作原理以及一个小demo,欢迎评论区留言交流讨论。

 

版权声明:
作者:白纸画
链接:https://www.fanyicloud.cn/article-24.fan
来源:白纸画的博客
文章版权归作者所有,未经允许请勿转载。

THE END
二维码
打赏
< <上一篇
下一篇>>