SpringCloud 02 —— 负载均衡之Ribbon

负载均衡概念

略,应该都懂不知道的可以自行搜索...

Ribbon 介绍

Ribbon是Netflix下的负载均衡项目,通过Spring Cloud的封装的工具Spring Cloud Ribbon,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。主要提供了以下特性:

  • 负载均衡器,可支持插拔式的负载均衡原则
  • 对多种协议提供支持,例如HTTP、TCP、UDP等
  • 集成了负载均衡功能的客户端

它不需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要

Ribbon主要由4个子模块组成

  • ribbon-core:ribbon的核心,主要包含负载均衡器、负载均衡接口、客户端接口、内置负载均衡实现API
  • ribbon-httpclient:为负载均衡提供了REST客户端
  • ribbon-loadbalancer:负载均衡模块,可以独立使用,也可和其它模块一起使用
  • ribbon:在 ribbon 模块和 Hystrix 基础之上,集成了 负载均衡、容错处理、缓存/批处理的API

Ribbon 负载均衡器主键

Ribbon 的负载均衡器主要与集群中的各个服务进行通信,负载均衡器主要提供已下功能:

  • 维护服务器的IP、DNS的信息
  • 根据特定的逻辑在服务列表中循环

为了实现负载均衡的基础功能,RIbbon的负载均衡器提供了已下三大模块:

  • Rule: Ribbon提供的逻辑主键,决定了服从服务器列表中返回那个服务器实例
  • Ping: 主要使用定时器来确保服务器的网络可以连接
  • ServerList: 服务器列表,可以静态的配置服务器列表,也可以动态指定服务器列表

Ribbon提供的负载均衡算法

  • RoundRobinRule(轮询算法)
  • RandomRule(随机算法)
  • AvailabilityFilteringRule():会先过滤由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问
  • WeightedResponseTimeRule():根据平均响应的时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高,刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够会切换到WeightedResponseTimeRule
  • RetryRule():先按照RoundRobinRule的策略获取服务,如果获取失败则在制定时间内进行重试,获取可用的服务。
  • BestAviableRule():会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务

Ribbon 服务实体

这里我们将创建一个负载均衡连接两个web服务。

先建服务端ribbon-server提供一个RESTFUL的接口,然后在创建Ribbon客户端调动,并将负载的效果显示出来。

新建模块后导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

创建启动类

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

添加application.yml配置

server:
  port: 8773
spring:
  application:
    name: ribbon-server
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka

这里为了更好的演示RESTFUL接口,和前面一样创建一些示例(可以把前面eureka创建的直接拿来用)

User实体对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    /**
     * 主键Id
     */
    private long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 昵称
     */
    private String rolename;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 备注
     */
    private String remark;
}

UserService 接口

public interface UserService {

    /**
     * 模拟数据库获取所有用户
     * @return
     */
    List<User> getUsers();

    /**
     * 模拟数据库根据id获取用户
     * @return
     */
    User getUserById(long id);
}

实现类

@Service
public class UserServiceImpl implements UserService {

    @Override
    public List<User> getUsers() {
        return initUser();
    }

    @Override
    public User getUserById(long id) {
        List<User> userList = getUsers().stream().filter(user -> user.getId() == id).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(userList)) {
            return new User(0, null, null, null, "用户不存在!");
        }
        return userList.get(0);
    }

    /**
     * 模拟数据库初始化数据
     *
     * @return
     */
    private List<User> initUser() {

        List<User> userList = new ArrayList<>();
        User user1 = new User(1, "007", "詹姆斯邦德", "zms@gmail.com", "12345");
        User user2 = new User(2, "9527", "华安", "h_a@gmail.com", "321");
        User user3 = new User(3, "47", "代号47", "47@gmail.com", "请扫描头顶二维码");

        userList.add(user1);
        userList.add(user2);
        userList.add(user3);

        return userList;
    }
}

RESTFUL 接口

@RestController
@RequestMapping("user")
public class RibbonController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id:\\d+}")
    public User getUserById(@PathVariable Long id, HttpServletRequest req){
        String url = req.getRequestURL().toString();
        User user = userService.getUserById(id);
        user.setRemark(user.getRemark()+":提供服务的是:"+url);
        return user;
    }
}

然后我们启动项目验证提供的服务是否可用,后面再创建负载客户端。

首先启动前面8761端口的eureka服务器,再启动ribbon server。

访问127.0.0.1:8761,看到已经注册:

接口验证正常

然后在启动方式中设置并行运行,然后修改端口为8774之后再启动。

server:
  port: 8774
spring:
  application:
    name: ribbon-server
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka

再次查看Eureka控制台,两个均已注册

Ribbon客户端

在前面两个服务体创建好了之后,新建ribbon-client模块。导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

新建启动类

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

创建配置

server:
  port: 8772
spring:
  application:
    name: ribbon-client
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka

因为服务端是接口返回的User 类,所以接收端也需要同样的User类.把上面的User类复制粘贴过来

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    /**
     * 主键Id
     */
    private long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 昵称
     */
    private String rolename;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 备注
     */
    private String remark;
}

创建Ribbon配置类,我们只需要在RestTemplate上加上@LoadBalanced注解即可完成负载均衡。

@Configuration
public class EurekaRibbonConfig {

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

*@LoadBalanced 原理

在 RestTemplate 上加了一个 @LoadBalanced 注解就可以负载均衡。这是因为 Spring Cloud 做了大量的底层封装,做了很多简化。
内部的主要逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行了替换,或者根据具体的负载策略选择服务地址,然后去调用,这就是 @LoadBalanced 的原理。
Spring Web 为 HttpClient 提供了 Request 拦载器 ClientHttpRequestInterceptor,位于 spring-web jar 包下。
在 spring-cloud-commons 包中提供了负载均衡自动配置类 LoadBalancerAutoConfiguration ,里面维护了一个 @LoadBalanced 注解的 RestTemplate 列表,里面的静态类 LoadBalancerInterceptorConfig 注册了 负载均衡拦截器 LoadBalancerInterceptor,RestTemplateCustomizer 来添加拦截器列表。
负载均衡拦截器 LoadBalancerInterceptor 实现了 ClientHttpRequestInterceptor,主要逻辑在 intercept() 方法中,执行交给了 LoadBalancerClient,通过 LoadBalancerRequestFactory 来构建一个 LoadBalancerRequest 对象,createRequest 方法中通过 ServiceRequestWrapper 来执行替换 URI 的逻辑,核心是通过 reconstructURI() 方法实现,该方法的业务实现是在 RibbonLoadBalancerClient 类中 。

创建Ribbon Service

@Service
@Slf4j
public class EurekaRibbonService {

    @Autowired
    RestTemplate restTemplate;

    public User findUserById(Long id) {
        // http://服务提供者的serviceId/url
        return restTemplate.getForObject("http://ribbon-server/user/" + id, User.class);
    }
}

restTemplate.getForObject("http://ribbon-server/user/" + id, User.class)这里需要添加正确的服务名称,RestTemplate 这样请求的好处就是不需要关心应用的IP,通过Eureka 注册中心发现是此服务名的服务提供者就会去请求。非常方便应用的水平拓展。

Ribbon RESTFUL 请求层

@RestController
public class EurekaRibbonController {
    @Resource
    private EurekaRibbonService eurekaRibbonService;

    /**
     * 根据id获取用户信息
     * @param id
     * @return
     */
    @GetMapping("/user/{id:\\d+}")
    public User findUserById(@PathVariable long id){
        return eurekaRibbonService.findUserById(id);
    }
}

\\d+是一个正则表达式,这个正则表达式的含义就是请求的id只能是正整数

启动应用进行验证.

用apiDebug访问127.0.0.1:8772/user/1,多发几次发现返回的服务提供者变化

通过返回结果可以看到8773和8774一直在变化,轮询着变化,原因是什么呢?下面我们说一下负载均衡的策略。

负载均衡策略

Ribbon 默认的负载策略是轮询,同时也提供了很多其他的策略能够让用户根据业务需求来选择。负载均衡策略的根接口是 com.netflix.loadbalancer.IRule

  • BestAvailableRule
    选择最小并发请求的服务器,每个客户端都会获得一个随机的服务器列表,如果服务器被标记为错误,则跳过。
  • AvailabilityFilteringRule
    用于过滤连接一直失败或读取失败而被标记为 circuit breaker tripped 状态的服务;或持有超过可配置限制的活动连接(默认值为 Integer.MAX_VALUE),即过滤掉高并发的后端服务。实际就是检查获取到的服务列表里,各个 Server 的 Status 。
  • ZoneAvoidanceRule
    根据区域和可用性来过滤服务器。使用 ZoneAvoidancePredicate 来判断 Zone 的使用是否达到阀值,过滤出最差 Zone 中的所有服务器;
  • AvailabilityPredicate
    用于过滤出并发连接过多的服务。
  • RandomRule
    随机分配流量,即随机选择一个 Server。
  • RetryRule
    向现有负载均衡策略添加重试机制。
  • ResponseTimeWeightedRule
    该策略已过期,同见 WeightedResponseTimeRule。
  • WeightedResponseTimeRule
    根据响应时间为每个服务器动态分配权重(Weight)分,然后台加权循环的方式使用该策略。响应时间越长,权重越低,被选中可能性越低。
    以上是Ribbon 自带的负载策略,如果我们想改变策略怎么改?这时我们就可以自定义策略。

Ribbon修改请求策略

我们只需要定义一个bean,比如替换Ribbon自带的随机策略即可。如我们在原来的configuration中修改为随机:

@Configuration
public class EurekaRibbonConfig {

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

    @Bean
    public IRule ribbonRule() {
        //自定义成随机
        return new RandomRule();
    }
}

重启后多次请求发现地址随机的。

自定义请求策略

自定义负载请求的策略规则时新定义规则需要实现IRule并实现里面的方法,其中规则策略的实现是在choose(Object o)中。比如我们现在实现一个只取第一个注册到Eureka服务中的提供者实例。然后替代原Rule即可.

创建自定义规则类

@Slf4j
public class CustomRule implements IRule {

    private ILoadBalancer loadBalancer;

    @Override
    public Server choose(Object o) {
        List<Server> allServers = loadBalancer.getAllServers();
        //输出一遍提供者实例
        allServers.stream().forEach(server -> System.out.println(server.getHostPort()));
        if(CollectionUtils.isEmpty(allServers)){
            log.info("当前不存在负载调用的提供者实例");
            return null;
        }
        return allServers.get(0);
    }

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

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

再次修改Configuration,替换我们自定义的IRule:

@Configuration
public class EurekaRibbonConfig {

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

//    @Bean
//    public IRule ribbonRule() {
//        //自定义成随机
//        return new RandomRule();
//    }
    @Bean
    public IRule ribbonRule() {
        //自定义规则
        return new CustomRule();
    }
}

再次重启,请求发现只有第一个提供服务。

不再Eureka环境下运行

在无Eureka 环境下如何运行Spring Cloud RIbbon 提供的负载均衡?我们只需要修改一下application.yml配置

ribbon:
  eureka:
    enabled: false #禁用eurake
# 禁用 Eureka 后手动配置服务地址
ribbon-server: #服务名称
  ribbon:
    listOfServers: 127.0.0.1:8773,127.0.0.1:8774

这里需要将Eureka设置为false,就是不启用从Eureka获取服务实例,但是需要将已经启动的服务端实例的地址设置在调用服务的后面,规则如下

服务名称(spring.application.name):
    ribbon:
        listOfServers: ip:port,ip:port

再次重启,测试。

Spring Cloud对Ribbon 的封装已经可以满足我们的日常需要,并且提供了现成的API供我们使用。前面说过Ribbon 在整个分布式项目架构中占着举足轻重的作用,下一章的Feign是基于Ribbon 实现的工具。

更新时间:2020-04-12 15:17:51

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/springcloud02负载均衡之ribbon
最后更新:2020-04-12 15:17:51

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×