SpringCloud客户端负载均衡Netflix Ribbon如何实现优化?

摘要:1. 什么是负载均衡? 负载均衡是一种基础的网络服务,它的核心原理是按照指定的负载均衡算法,将请求分配到后端服务集群上,从而为系统提供并行处理和高可用的能力。提到负载均衡,你可能想到nginx。对于负载均衡,一般分为服务端负载均衡和客户端负
1. 什么是负载均衡? 负载均衡是一种基础的网络服务,它的核心原理是按照指定的负载均衡算法,将请求分配到后端服务集群上,从而为系统提供并行处理和高可用的能力。提到负载均衡,你可能想到nginx。对于负载均衡,一般分为服务端负载均衡和客户端负载均衡 服务端负载均衡:在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件的负载均衡器,比如 F5,也有软件,比如 Nginx。 客户端负载均衡:所谓客户端负载均衡,就是客户端根据自己的请求情况做负载,本文介绍的Netflix Ribbon就是客户端负载均衡的组件 2. 什么是Netflix Ribbon? 在上一章的学习中,我们知道了微服务的基本概念,知道怎么基于Ribbon+restTemplate的方式实现服务调用,接着上篇博客,我们再比较详细学习客户端负载均衡Netflix Ribbon,学习本博客之前请先学习上篇博客,然后再学习本篇博客 Ribbon 是由 Netflix 发布的负载均衡器,它有助于控制 HTTP 和 TCP 的客户端的行为。Ribbon 属于客户端负载均衡。 3. Netflix Ribbon实验环境准备 环境准备: JDK 1.8 SpringBoot2.2.3 SpringCloud(Hoxton.SR6) Maven 3.2+ 开发工具 IntelliJ IDEA smartGit 创建一个SpringBoot Initialize项目,详情可以参考我之前博客:SpringBoot系列之快速创建项目教程 可以引入Eureka Discovery Client,也可以单独添加Ribbon Spring Cloud Hoxton.SR6版本不需要引入spring-cloud-starter-netflix-ribbon,已经默认集成 也可以单独添加Ribbon依赖: 本博客的是基于spring-cloud-starter-netflix-eureka-client进行试验,试验前要运行eureka服务端,eureka服务提供者,代码请参考上一章博客 补充:IDEA中多实例运行方法 step1:如图,不要加上勾选 step2:指定不同的server端口和实例id,如图: 启动成功后,是可以看到多个实例的 4. Netflix Ribbon API使用 使用LoadBalancerClient : @Autowired LoadBalancerClient loadBalancerClient; @Test void contextLoads() { ServiceInstance serviceInstance = loadBalancerClient.choose("EUREKA-SERVICE-PROVIDER"); URI uri = URI.create(String.format("http://%s:%s", serviceInstance.getHost() , serviceInstance.getPort())); System.out.println(uri.toString()); } 构建BaseLoadBalancer 实例例子: @Test void testLoadBalancer(){ // 服务列表 List<Server> serverList = Arrays.asList(new Server("localhost", 8083), new Server("localhost", 8084)); // 构建负载实例 BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList); loadBalancer.setRule(new RandomRule()); for (int i = 0; i < 5; i++) { String result = LoadBalancerCommand.<String>builder().withLoadBalancer(loadBalancer).build() .submit(new ServerOperation<String>() { public Observable<String> call(Server server) { try { String address = "http://" + server.getHost() + ":" + server.getPort()+"/EUREKA-SERVICE-PROVIDER/api/users/mojombo"; System.out.println("调用地址:" + address); return Observable.just(""); } catch (Exception e) { return Observable.error(e); } } }).toBlocking().first(); System.out.println("result:" + result); } } 5. 负载均衡@LoadBalanced Ribbon负载均衡实现,RestTemplate 要加上@LoadBalanced package com.example.springcloud.ribbon.configuration; 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; /** * <pre> * RestConfiguration * </pre> * * <pre> * @author mazq * 修改记录 * 修改后版本: 修改人: 修改日期: 2020/07/31 09:43 修改内容: * </pre> */ @Configuration public class RestConfiguration { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } } yaml配置: server: port: 8082 spring: application: name: eureka-service-consumer eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ fetch-registry: true register-with-eureka: false healthcheck: enabled: false instance: status-page-url-path: http://localhost:8761/actuator/info health-check-url-path: http://localhost:8761/actuator//health prefer-ip-address: true instance-id: eureka-service-consumer8082 关键点,使用SpringCloud的@LoadBalanced,才能调http://EUREKA-SERVICE-PROVIDER/api/users/? 接口的数据,浏览器是不能直接调的 import com.example.springcloud.ribbon.bean.User; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import java.net.URI; @SpringBootApplication @EnableEurekaClient @RestController @Slf4j public class SpringcloudRibbonApplication { @Autowired RestTemplate restTemplate; public static void main(String[] args) { SpringApplication.run(SpringcloudRibbonApplication.class, args); } @GetMapping("/findUser/{username}") public User index(@PathVariable("username")String username){ return restTemplate.getForObject("http://EUREKA-SERVICE-PROVIDER/api/users/"+username,User.class); } } 6. 定制Netflix Ribbon client 具体怎么定制?可以参考官网,@RibbonClient指定定制的配置类既可 package com.example.springcloud.ribbon.configuration; import com.example.springcloud.ribbon.component.MyRule; import com.netflix.loadbalancer.IPing; import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.PingUrl; import org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * <pre> * Ribbon Clients configuration * </pre> * * <pre> * @author mazq * 修改记录 * 修改后版本: 修改人: 修改日期: 2020/07/29 14:22 修改内容: * </pre> */ //@Configuration(proxyBeanMethods = false) //@IgnoreComponentScan public class RibbonClientConfiguration { // @Autowired // IClientConfig config; @Bean public IRule roundRobinRule() { return new MyRule(); } @Bean public ZonePreferenceServerListFilter serverListFilter() { ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter(); filter.setZone("myTestZone"); return filter; } @Bean public IPing ribbonPing() { return new PingUrl(); } } 在Application类加上@RibbonClient,name是为服务名称,跟bootstrap.yml配置的一样既可 @RibbonClient(name = "eureka-service-provider",configuration = RibbonClientConfiguration.class) 特别注意:官网这里特意提醒,这里的意思是说@RibbonClient指定的配置类必须加@Configuration(不过在Hoxton.SR6版本经过我的验证,其实是可以不加的,加了反而可能报错),@ComponentScan扫描要排除自定义的配置类,否则,它由所有@RibbonClients共享。如果你使用@ComponentScan(或@SpringBootApplication) 其实就是想让我们排除这个配置的全局扫描,所以我们可以进行编码,写个注解类@IgnoreComponentScan ,作用于类,指定@Target(ElementType.TYPE) package com.example.springcloud.ribbon.configuration; import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface IgnoreComponentScan { } 加上自定义的注解类 任何在Application加上代码,避免全局扫描: @ComponentScan(excludeFilters={@ComponentScan.Filter(type= FilterType.ANNOTATION,value= IgnoreComponentScan.class)}) 7. Netflix Ribbon常用组件 ps:介绍Netflix Ribbon的负载策略之前,先介绍Netflix Ribbon常用组件及其作用: 组件 作用 ILoadBalancer 定义一系列的操作接口,比如选择服务实例。 IRule 负载算法策略,内置算法策略来为服务实例的选择提供服务。 ServerList 负责服务实例信息的获取(可以获取配置文件中的,也可以从注册中心获取。) ServerListFilter 过滤掉某些不想要的服务实例信息。 ServerListUpdater 更新本地缓存的服务实例信息。 IPing 对已有的服务实例进行可用性检查,保证选择的服务都是可用的。 8. 定制Netflix Ribbon策略 因为服务提供者是多实例的,所以再写个接口测试,调用了哪个实例,来看看Netflix Ribbon的负载策略 @Autowired LoadBalancerClient loadBalancerClient; @GetMapping(value = {"/test"}) public String test(){ ServiceInstance serviceInstance = loadBalancerClient.choose("EUREKA-SERVICE-PROVIDER"); URI uri = URI.create(String.format("http://%s:%s", serviceInstance.getHost() , serviceInstance.getPort())); System.out.println(uri.toString()); return uri.toString(); } 部署成功,多次调用,可以看到每次调用的服务实例都不一样?其实Netflix Ribbon默认是按照轮询的方式调用的 要定制Netflix Ribbon的负载均衡策略,需要实现AbstractLoadBalancerRule抽象类,下面给出类图: Netflix Ribbon内置了如下的负载均衡策略,引用https://juejin.im/post/6854573215587500045的归纳: ok,接着我们可以在配置类,修改规则 @Bean public IRule roundRobinRule() { return new BestAvailableRule(); } 测试,基本都是调8083这个实例,因为这个实例性能比较好 显然,也可以自己写个策略类,代码参考com.netflix.loadbalancer.RandomRule,网上也有很多例子,思路是修改RandomRule原来的策略,之前随机调服务实例一次,现在改成每调5次后,再调其它的服务实例 package com.example.springcloud.ribbon.component; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ThreadLocalRandom; public class MyRule extends AbstractLoadBalancerRule { // 总共被调用的次数,目前要求每台被调用5次 private int total = 0; // 当前提供服务的机器号 private int index = 0; 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) { // 没有获取到服务 return null; } //int index = chooseRandomInt(serverCount); //server = upList.get(index); if(total < 5) { server = upList.get(index); total++; }else { total = 0; index++; if(index >= upList.size()) { index = 0; } } if (server == null) { // 释放线程 Thread.yield(); continue; } if (server.isAlive()) { return (server); } server = null; Thread.yield(); } return server; } protected int chooseRandomInt(int serverCount) { return ThreadLocalRandom.current().nextInt(serverCount); } @Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { } } 修改IRule ,返回MyRule @Bean public IRule roundRobinRule() { return new MyRule(); } 附录: ok,本博客参考官方教程进行实践,仅仅作为入门的学习参考资料,详情可以参考Spring Cloud官方文档https://docs.spring.io/spring-cloud-netflix/docs/2.2.x-SNAPSHOT/reference/html/#customizing-the-ribbon-client 代码例子下载:code download 优质学习资料参考: Ribbon负载均衡 -> 源码剖析:Ribbon负载均衡 -> 源码剖析 方志鹏大佬系列Spring Cloud博客:https://www.fangzhipeng.com/spring-cloud.html 使用Spring Cloud与Docker实战微服务:https://eacdy.gitbooks.io/spring-cloud-book/content/ 程序员DD大佬系列Spring Cloud博客:http://blog.didispace.com/spring-cloud-learning/