1.概述
Ribbon是Netflix开源的一个客户端负载均衡库,也是Spring Cloud Netflix项目的核心组件之一。它主要用于在微服务架构中对服务进行负载均衡,以提高系统的可用性和性能。ribbon不是通信组件,而是服务调用者和通信组件之间的中间层,主要就是用来做负载均衡,选择出适合处理本次请求的服务节点。
现在整个spring cloud体系中的通信组件其实就是封装了负载均衡组件+HTTP通信组件,HTTP通信组件没什么好说的,就是用来发起HTTP请求的,市面上的HTTP通信组件也很多,诸如spring boot自带的RestTemplate,apache的Apache HttpClient等等。值得去探究一下的是负载均衡组件,其作为负载均很组件决定了请求的分发,可以说是整个通信组件的核心。
从Spring Cloud 2020.0版本开始,Spring Cloud官方已经将Ribbon标记为过时(deprecated),推荐使用Spring Cloud LoadBalancer作为替代方案。这样做是基于标准化的考虑,整个spring生态历来都是希望“包容万物”,所以社区自然不希望在负载均衡组件上直接就采用一个固定的实现,而是希望能让三方的解决方案可以平滑的接入、切换。
虽然ribbon以后不再是负载均衡组件的首选,但是作为最经典的负载均衡组件,其底层的一些思想仍然被后面的方案沿用,其实看明白ribbon的源码基本上也就明白负载均衡的原理了,万变不离其宗。
2.使用
2.1.引入
博主的上篇文章详细介绍了怎么使用eureka+通信组件来完成服务的注册、调用,其中也详细的介绍了ribbon的使用,可以移步:
详解Eureka服务注册和调用__BugMan的博客-CSDN博客
总的来说就是:
eureka集成了ribbon,导入eureka后不用单独导入ribbon,但是要注意的是一定要选对版本号,不要选到一个已经把ribbon移除的高版本,本文使用的依赖版本:
当然你也可以直接降级整个spring cloud的版本号,关于spring cloud版本号的问题,可以移步博主的另一篇文章,会有详细讲解:
详解Spring Cloud版本问题__BugMan的博客-CSDN博客
2.2.启用
引入依赖后在承载HTTP通信组件上开启负载均衡即可,此处以spring boot自带的RestTemplate为例,这样在每次客户端(消费者)发起http请求的时候都会在本端进行负载均衡运算后再进行服务访问。
2.3.切换负载均衡算法
负载均衡的核心接口Irule有多个实现类,每个实现类实现不同的负载均衡算法,
常用的有,轮询、随机、可获得、重试等几种:
RoundRobinRule,轮询
RandomRule,随机
AvailabilityFilteringRule,会过滤掉,跳闸,访问故障的服务,对剩下的进行轮询
RetryRule,会按照轮询获取服务,如果服务获取失败,则会在指定的时间内进行重试
需要切换的时候直接在@Configuration中注入@Bean即可:
3.负载均衡源码分析
3.1.接口
IRule接口:
- choose:选举出一个服务器
- get/setLoadBalancer:获取、修改均衡器
ILoadBalancer:
与服务器打交道,负责寻找、登记服务器
3.2.抽象类
AbstractLoadBalancer实现了Irule接口,重写了均衡器的get/set方法,只留下一个抽象方法——choose,待子类重写。
3.3.选择服务器
所有实现类都继承抽象类AbstractLoadBalancer。各自去重写choose方法,即各自实现不同的负载均衡规则(此处以ribbon默认的负载均衡规则,RoundRobinRule为例):
choose方法即负载均衡策略,是各负载均衡类的核心方法(此处以ribbon默认的负载均衡规则,RoundRobinRule为例):
均衡器会和注册中心交互,然后记录下当前整个系统中所有服务器的相关信息,包含服务器总数,可用总数等。
向均衡器所要服务器总数、服务器可用总数,然后根据这两个值进行运算,挑选出承载该此流量的服务器。
public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { log.warn("no load balancer"); return null; } else { Server server = null; int count = 0; while(true) { if (server == null && count++ < 10) { List<Server> reachableServers = lb.getReachableServers(); List<Server> allServers = lb.getAllServers(); int upCount = reachableServers.size(); int serverCount = allServers.size(); if (upCount != 0 && serverCount != 0) { int nextServerIndex = this.incrementAndGetModulo(serverCount); server = (Server)allServers.get(nextServerIndex); if (server == null) { Thread.yield(); } else { if (server.isAlive() && server.isReadyToServe()) { return server; } server = null; } continue; } log.warn("No up servers available from load balancer: " + lb); return null; } if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; } } }
整个过程中封装服务器相关信息的Server类,其中有服务器的详细参数:
3.4.原子性
挑选过程中,为了保证原子性,使用了自旋锁(CAS),保证每次处理的只有一个访问线程,其余线程处于自选等待状态:
compareAndSet(期望值,修改值)
期望值,即版本号。
修改值,即要将版本号更新为的值。
判断期望值是否改变(前后是否相同),如果期望值未改变,则将期望值更新为修改值。
返回true,否则返回false。
4.自定义负载均衡算法
存在顶级接口并且可以切换负载均衡算法,那自然可以自定义负载均衡算法,以下是一个根据服务器权重进行负载均衡的一个负载均衡算法:
public class CustomWeightedRandomRule extends AbstractLoadBalancerRule { private AtomicInteger totalWeight = new AtomicInteger(0); @Override public Server choose(Object key) { ILoadBalancer lb = getLoadBalancer(); if (lb == null) { return null; } List<Server> allServers = lb.getAllServers(); int serverCount = allServers.size(); if (serverCount == 0) { return null; } int randomWeight = ThreadLocalRandom.current().nextInt(totalWeight.get()); int currentWeight = 0; for (Server server : allServers) { currentWeight += getWeight(server); if (randomWeight < currentWeight) { return server; } } // Fallback to the default server if no server is selected return super.choose(key); } private int getWeight(Server server) { // Return the weight of the server (custom logic) // Example: return server.getMetadata().getWeight(); return 1; // Default weight is 1 } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { super.initWithNiwsConfig(clientConfig); // Calculate the total weight of all servers List<Server> allServers = getLoadBalancer().getAllServers(); totalWeight.set(allServers.stream().mapToInt(this::getWeight).sum()); } }