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

Ribbon基本使用及其原理

  • 一、负载均衡
  • 二、Ribbon
    • 1. 简介
    • 2. 基本使用
      • 2.1 引入 pom.xml 依赖
      • 2.2 服务提供方
      • 2.3 服务调用方
      • 2.4 配置文件application.yaml
      • 2.5 测试
    • 3. 负载均衡算法
      • 3.1 内置7种负载均衡算法
      • 3.2 自定义负载均衡算法
  • 三、Ribbon实现原理
    • 1. Ribbon底层原理猜想
    • 2. Ribbon底层原理与源码分析

一、负载均衡

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

负载均衡其意思就是分摊到多个操作单元上分别执行。
Spring Cloud Netflix-Ribbon基本使用及其原理_第1张图片

二、Ribbon

1. 简介

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

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

2. 基本使用

本实例基于独立的SpringCloud Ribbon实现了一个简单的客户端负载均衡案例。
Spring Cloud Netflix-Ribbon基本使用及其原理_第2张图片

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

2.1 引入 pom.xml 依赖

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starterartifactId>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
    <version>2.4.3version>
dependency>

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-ribbonartifactId>
    <version>2.2.9.RELEASEversion>
dependency>
<dependency>
    <groupId>com.squareup.okhttp3groupId>
    <artifactId>okhttpartifactId>
    <version>4.9.1version>
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种负载均衡算法

Spring Cloud Netflix-Ribbon基本使用及其原理_第3张图片

  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 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底层原理猜想

Spring Cloud Netflix-Ribbon基本使用及其原理_第4张图片

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

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

RestTemplate 拦截器机制
RestTemplate 中有一个属性是 List 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。

你可能感兴趣的:(微服务与分布式,ribbon,spring,cloud,微服务)