什么是Ribbon
springcloud的Ribbon是基于Netflix Ribbon实现的客户端负载均衡的工具,它的主要功能是提供软件客户端的软件负载均衡算法和服务调用。Ribbon组件提供一系列完善的配置链如链接超时,重试等。简单来说就是在配置文件中罗列出Load Balancer后面的所有机器,Ribbon就会自动帮你去连接相应的机器。
负载均衡
负载均衡(Load Banlancer)是什么
简单来说就是将用户的请求平均的分摊到多个服务器上,进而使系统达到高可用的状态HA
Ribbon本地负载均衡客户端和Nginx服务端负载均衡的区别
1.nginx是服务端的负载均衡,客户端所有的请求都会交给nginx去实现请求转发,这个负载均衡实在服务端实现的
2.Ribbon是本地负载均衡,在调用微服务接口的时候会去注册中心上获取所有的服务列表缓存到jvm中,从而在本地实现rpc远程调用服务
轮询算法源码分析
Ribbon最重要的有个组件是IRule接口
实现类
默认使用RoundRobinRule轮询
这里最主要是关注RandomRule类
// An highlighted block
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
this.nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
this.setLoadBalancer(lb);
}
可以看到在使用默认的轮询规则的时候就会创建一个原子整形类来确保原子性
// An highlighted block
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;
}
}
}
在choose 方法里面首先就进行了判断如果传入的lb没有负载均衡,返回空。
在下面的循环中lb.getReachableServers();先获取注册众兴中状态为up的服务lb.getAllServers();获取所有的服务,并且调用size()方法得到他们的数量。
在都不为空0得情况下调用incrementAndGetModulo()方法并且传入所有服务的数量
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextServerCyclicCounter.get();
next = (current + 1) % modulo;
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));
return next;
}
拿到原子整型类的数 初始0,当服务启动时都会从0开始,客户端就会current就会加一次 。
next = (current + 1) % modulo;
这就是默认的轮询算法拿请求次数去加1然后取模服务数量。while里面采用的是cas算法比较并设置 ,就是说在我设置新值的时候会把我拿出来的原值和最初内存里的值进行比较,如果一直就说明没有被其他线程动过就设置进去,如果不一致就不设置!
在这里当没有设置成功也就是返回FALSE的时候取反他就会又会去循环,如果设置成功取反就直接返回下一个要调用的服务的下标。
再回到choose方法中 server = (Server)allServers.get(nextServerIndex);
这里就直接用下标去拿对应的服务。为了严谨官方还在下面进行了判断,就是说如果拿到的服务是空的话,如果不为空就要再次去判断服务是否是健康的也就是是否还活着并且还在工作,如果满足就返回服务。
手写轮询算法
第一步永远都是导入jar包
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>springcloud-commons</artifactId>
</dependency>
</dependencies>
编写yml文件
这里我用的是Eureka集群模式
// An highlighted block
server:
port: 81
spring:
application:
name: speingcloud-consumer-order
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/, http://eureka7002.com:7002/eureka/, http://eureka7003.com:7003/eureka/
在主启动类上标注@EnableEurekaClient
package com.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class Application81 {
public static void main(String[] args) {
SpringApplication.run(Application81.class,args);
}
}
前面的基础环境搭建完毕之后就来开始写轮询算法
先来创建接口
下面展示一些 内联代码片
。
public interface LoadBalancer {
ServiceInstance instance(List<ServiceInstance> allInstance);
};
再编写实现类
下面展示一些 内联代码片
。
也就是算法的实现过程(其实也是模仿的源码那一套)
@Component
public class MyLb implements LoadBalancer {
private AtomicInteger atomicInteger=new AtomicInteger(0);
@Override
public ServiceInstance instance(List<ServiceInstance> allInstance) {
int index= getIncrement() % allInstance.size();
return allInstance.get(index);
}
public final int getIncrement(){
int current;//当前访问的次数
int next;
do {
current=this.atomicInteger.get();
next=current>=Integer.MAX_VALUE ? 0 : current+1;
}while (!this.atomicInteger.compareAndSet(current,next));
return next;
}
}
在这里值得注意的是一定要加@Component注解,我最开始在启动的时候就一直提示ioc容器中找不到这个这个实例
首先都是创建原子整型类,然后调用getIncrement()方法 返回调用次数,
next=current>=Integer.MAX_VALUE ? 0 : current+1。这里的话就判断了如果请求次数达到了整形的最大值就返回0重新开始(在服务器重启之后也是从0开始的),否则就加一次。这里的取反操作跟源码是一样的。随后返回请求次数。
在上面还是进行取模操作得到下标拿到对应的服务!
下面就来进行测试
编写controller
@Slf4j
@RestController
public class ComsumerController {
public static final String PAYMENT_URL = "http://SPRINGCLOUD-PROVIDER-PAYMENT";
@Resource
private DiscoveryClient discoveryClient;
@Resource
private LoadBalancer loadBalancer;
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/lb")
public String getLb(){
List<ServiceInstance> instances = discoveryClient.getInstances("SPRINGCLOUD-PROVIDER-PAYMENT");
if (instances==null || instances.size()<=0){
return null;
}
ServiceInstance instance = loadBalancer.instance(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
}
public static final String PAYMENT_URL = “http://SPRINGCLOUD-PROVIDER-PAYMENT”;
这里是我远程提供服务的名称
@Resource
private LoadBalancer loadBalancer;
注入我们编写的轮询算法
接下来在getLb()方法中首先就去获取到所有的服务并且先判断获取到的服务是否为空或者说是否为0
再把获取到的服务传入我们编写的instance()方法中。得到相应的服务并且拿到他的uri地址。在使用rest模板远程调用.
远程方法
@GetMapping("/payment/lb")
public String getLb(){
return "调用的服务为"+port;
}
至此结束!