Spring Cloud Netflix-Ribbon基本使用及其原理

本文详细介绍了Ribbon在SpringCloud中的使用,包括如何配置、使用RestTemplate进行服务调用,以及Ribbon的负载均衡算法。通过示例展示了服务提供方和服务调用方的配置,并探讨了Ribbon的内置负载算法和自定义算法。最后,文章剖析了Ribbon的底层工作原理,强调了RestTemplate拦截器在负载均衡过程中的作用。
摘要由CSDN通过智能技术生成

一、负载均衡

在分布式的时代,服务必定是多个实例的,系统再进行服务间通信时,必定需要根据当前服务实例集合,选择一个实例进行通信。而已负载均衡(Load Balance)就是基于这个产生的。

负载均衡其意思就是分摊到多个操作单元上分别执行。
在这里插入图片描述

二、Ribbon

1. 简介

Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松的将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。

Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的。

2. 基本使用

本实例基于独立的SpringCloud Ribbon实现了一个简单的客户端负载均衡案例。
在这里插入图片描述

  • finpc为业务聚合服务。
  • order为订单服务。
  • sys为系统服务。
  • finpc、order、sys都要引入service-api依赖

2.1 引入 pom.xml 依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.4.3</version>
</dependency>
<!--spring cloud ribbon-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.1</version>
</dependency>

2.2 服务提供方

@RestController
@RequestMapping("/user")
public class UserService implements IUserService {
    private static final Map<Long, UserBean> USER_BEAN_MAP = new ConcurrentHashMap<>();

    @Value("${server.port}")
    private String port;

    static {
        UserBean userBean = new UserBean();
        userBean.setId(111L);
        userBean.setName("test");
        userBean.setSex("男生");
        USER_BEAN_MAP.put(111L, userBean);
    }

    @Override
    @GetMapping("/get")
    public UserBean get(Long id) {
        System.out.println("----------------port=" + port);
        return UserService.USER_BEAN_MAP.get(id);
    }
}

2.3 服务调用方

服务调用方就是进行负载均衡的一分,通过对服务提供方的服务列表,利用 Ribbon 进行负载调用服务。

@Configuration
public class RibbonConfiguration {

    /**
     * 实例化ribbon使用的RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        /*使用OkHttp进行服务通信*/
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }

    /**
     * 默认 RestTemplate
     * @return
     */
    @Bean
    public RestTemplate defaultRestTemplate() {
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}

@RestController
@RequestMapping("/finpc")
public class FinpcController {

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private RestTemplate restTemplate;

    @Resource(name = "defaultRestTemplate")
    private RestTemplate defaultRestTemplate;

    @GetMapping("/getUser/{id}")
    public UserBean getUser(@PathVariable() Long id) {
        UserBean forObject = restTemplate.getForObject("http://lizq-sys/user/get?id={id}", UserBean.class, id);
        return forObject;
    }

    @GetMapping("/getUser1/{id}")
    public UserBean getUser1(@PathVariable() Long id) {
    	// 直接通过Ribbon客户端进行服务选择,然后进行Http通信
        ServiceInstance instance = loadBalancerClient.choose("lizq-sys");
        URI storesUri = URI.create(String.format("http://%s:%s/user/get?id=%s", instance.getHost(), instance.getPort(), id));
        UserBean forObject = defaultRestTemplate.getForObject(storesUri, UserBean.class);
        return forObject;
    }
}

2.4 配置文件application.yaml

server:
  port: 8080
spring:
  application:
    name: lizq-finpc
# lizq-sys为需要负载的服务名
lizq-sys:
  ribbon:
    listOfServers: http://localhost:8888,http://localhost:8889

2.5 测试

  • 启动两个lizq-sys服务,端口分别为8888、8889
  • 启动服务调用方
  • 发起请求:http://localhost:8080/finpc/getUser/111、http://localhost:8080/finpc/getUser1/111

上面就是一个简单使用 Ribbon 的例子,Ribbon 作为一个独立的组件,可以不依赖与任何第三方进行使用。

3. 负载均衡算法

Ribbon内置了7种负载均衡算法,并且保留了扩展,用户可通过实现 IRule 接口,实现自定义负载均衡算法。

3.1 内置7种负载均衡算法

在这里插入图片描述

  1. BestAviableRule
    跳过熔断的Server,在剩下的Server中选择并发请求最低的Server。
  2. ZoneAvoidanceRule
    随机选择一个server。
  3. AvailabilityFilteringRule
    剔除因为连续链接、读失败或链接超过最大限制导致熔断的Server,在剩下读Server中进行轮询。
  4. RoundRobinRule
    roundRobin方式轮询选择server,默认
  5. RandomRule
    随机选择一个server。
  6. RetryRule
    可重试的策略,可以对其他策略进行重试。
  7. ResponseTimeWeightedRule
    根据响应时间加权,响应时间越短权重越大,被选中的可能性越高。

3.2 自定义负载均衡算法

package com.netflix.loadbalancer;

public interface IRule {
    Server choose(Object key);
    void setLoadBalancer(ILoadBalancer loadBalancer);
    ILoadBalancer getLoadBalancer();
}

IRule 是负载均衡策略的抽象,ILoadBalancer 通过调用 IRulechoose(Object key) 方法返回 Server。

IPHash 算法: 根据调用者的ip hash后进行服务选择。

在 application.yaml 中添加配置:

# lizq-sys为需要负载的服务名
lizq-sys:
  ribbon:
    listOfServers: http://localhost:8888,http://localhost:8889
    NFLoadBalancerRuleClassName: com.lizq.finpc.rule.IpHashRule

com.lizq.finpc.rule.IpHashRule 实现

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.Server;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.List;

public class IpHashRule extends AbstractLoadBalancerRule {
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    @Override
    public Server choose(Object key) {
        if (getLoadBalancer() == null) {
            return null;
        }
        Server server = null;
        while (server == null) {
            // 表示可用的服务列表.(默认情况下单纯只用Ribbon时,不会对目标服务做心跳检测)
            List<Server> upList = getLoadBalancer().getReachableServers();
            // List<Server> allList = getLoadBalancer().getAllServers();
            int serverCount = upList.size();
            if (CollectionUtils.isEmpty(upList)) {
                return null;
            }
            int index = ipAddressHash(serverCount);
            server = upList.get(index);
        }
        return server;
    }

    /**
     * @param serverCount
     * @return
     */
    private int ipAddressHash(int serverCount) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String remoteAddr = requestAttributes.getRequest().getRemoteAddr();
        int code = Math.abs(remoteAddr.hashCode());
        return code % serverCount;
    }
}

三、Ribbon实现原理

1. Ribbon底层原理猜想

在这里插入图片描述

2. Ribbon底层原理与源码分析

Ribbon 实现的关键点是利用了 RestTemplate 的拦截器机制,在拦截器中实现 Ribbon 的负载均衡。负载均衡的基本实现就是利用applicationName 从配置文件或服务注册中心获取可用的服务地址列表,然后通过一定算法负载,返回服务地址后进行 Http 调用。

RestTemplate 拦截器机制
RestTemplate 中有一个属性是 List<ClientHttpRequestInterceptor> interceptors,如果 interceptors 里面的拦截器数据不为空,在RestTemplate 进行 Http 请求时,这个请求就会被拦截器拦截。

拦截器需要实现 ClientHttpRequestInterceptor 接口

package org.springframework.http.client;

import java.io.IOException;
import org.springframework.http.HttpRequest;

@FunctionalInterface
public interface ClientHttpRequestInterceptor {
    ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution execution) throws IOException;
}

Ribbon 中的 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) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        // 通过LoadBalance算法选择serviceName对应的所有的可用服务
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
}

/**
 * 拦截请求执行
 */
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);
    }

public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
	// 获取当前serviceId对应的LoadBalance
    ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
	// 通过loadBalance选择 Server
    Server server = this.getServer(loadBalancer, hint);
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    } else {
        RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
        // 执行请求
        return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
    }
}

/**
 * 通过loadBalance选择 Server
 */
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
	// 执行LoadBalance中的choose(Object key) 方法,返回对应的Server
    return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}

定义注入器、拦截器

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
static class LoadBalancerInterceptorConfig {
    LoadBalancerInterceptorConfig() {
    }

    @Bean
    public LoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
        return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    }

    @Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
    	// 定义注入器,用来将拦截器注入到RestTemplate中
        return (restTemplate) -> {
            List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
            list.add(loadBalancerInterceptor);
            restTemplate.setInterceptors(list);
        };
    }
}

将 Ribbon 拦截器注入到 RestTemplate

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

	/**
	 * 获取所有的 @LoadBalanced 标注的RestTemplate 对象
	 */
   @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 中的注入器(RestTemplateCustomizer),调用注入方法,为目标 RestTemplate 注入拦截器。

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

@LoadBalanced 注解

这个注解是 Spring Cloud 实现的,不是 Ribbon 中的,它的作用是在依赖注入时,只注入被 @LoadBalanced 修饰的实例。

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

那么 @LoadBalanced 是如何实现这个功能的呢?其实都是 Spring 的原生操作。@LoadBalanced 继承了 @Qualifier 注解,@Qualifier注解用来指定想要依赖某些特征的实例,这里的注解就是类似的实现,RestTemplates 通过 @Autowired 注入,同时被@LoadBalanced 修饰,所以只会注入 @LoadBalanced 修饰的 RestTemplate,也就是我们的目标 RestTemplate。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值