Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的。
Ribbon主要功能是提供客户端的软件负载均衡算法服务调用,Ribbon客户端组建提供一系列完善的配置项如连接超时,重试。简单的说就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动帮助你基于某种规则去连接这些机器。
LB负载均衡是什么?
简单的说就是将用户请求平摊到多个服务器上,从而达到高可用。常见的负载均衡软件有Nignx、LVS、硬件F5
Ribbon本地负载均衡客户端和Nginx服务端负载均衡区别?
Nginx是服务器端负载均衡,客户端所有请求给Nginx,然后由Nginx实现请求转发。即负载均衡由服务端实现。
Ribbon本地负载均衡,在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用的技术。
集中式LB
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,F5。也可以是软件,Nginx),由该设施负责把访问的请求通过某种策略转发至服务的提供方。
进程内的LB
将LB逻辑集成到消费方,消费方从服务注册中心获知有那些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方可以通过它来获取服务提供方的地址。
Ribbon在工作的时候分为两步
- 先选择EurekaServer,它优先选择在同一个区域内负载较少的Server
- 再根据用户指定的策略,在从Server取到注册列表中选择一个地址。
Ribbon提供的策略有多种 如 轮询、随机和根据响应时间增加权重等。
- RoundRobinRule 轮询
- RandomRule 随机
- RetryRule 先按照RoundRobinRule的策略获取服务,如果服务获取失败则在指定时间内进行重试
- BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态服务,然后选择一个并发量最小的服务
- WeightedResponseTimeRule 对RoundRobinRule的扩展,响应速度越快的实例权重越大,越容易被选择
- AvailabilityFilteringRule 先过滤故障实例,再选择并发量小的实例
- ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能和server的可用性选择服务器
接下来 我们看一下其中一个负载均衡是怎么实现的。
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);
}
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;
}
}
}
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextServerCyclicCounter.get();
//下次的下标 (当前值+1) / 服务提供者总数量
next = (current + 1) % modulo;
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));//使用自旋锁
return next;
}
public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
在我们前面的文章介绍的使用Eureka的实例中其实已经配合RestTemplate使用了Ribbon了。当前添加了一个@LoadBalanced。默认使用的是轮询,那么问题来了,既然上面说到的有这么多种方式,如果我不想使用默认的轮询,要怎么改成其他的负载均衡的方式呢?
官方文档上面说到,如果我们自定义配置类,那么这个配置类不能放到@CommponentScan所扫描的当前包下以及子包下。否则,我们自定义的配置类会被所有的Ribbon客户端所共享,达不到特殊定制化的目的了。所以新建一个myrule的目录
@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();
}
}
启动类上
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name="CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
@RibbonClient里面去指定Eureka中注册的服务提供者的名称,然后配置自定义的负载均衡的规则。
接下来我们启动项目就可以看到已经成功的修改了负载均衡的策略了。
上面我们查看了Rabbin的负载均衡的简单的实现,也看了一下里面轮询实现的源码,接下来我们自己来实现一个轮询的负载均衡的规则。
首先在写源码之前,我们大概回想一下轮询的大致实现的算法是什么
负载均衡轮询的算法 : rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置的下标。 每次服务器重启rest计数从1开始。
首先我们修改服务端的接口 增加两个测试的接口方法,用来返回对应的端口号
@GetMapping("/lb")
public String getPaymentLB(){
return serverport;
}
接着重点是修改客户端,毕竟Ribbon是客户端的负载均衡。 首先注释掉restTemplate的@LoadBalanced
接下来创建自定义的接口和实现类
public interface LoadBalancer {
ServiceInstance instances(List<ServiceInstance> serviceInstanceList);
}
@Component
public class MyLoadBalance implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement(){
int current, next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1;
}while (!this.atomicInteger.compareAndSet(current, next));
System.out.println("******************next:"+ next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstanceList) {
int index = getAndIncrement() % serviceInstanceList.size();
return serviceInstanceList.get(index);
}
}
接下来修改的是客户端的Controller
public class OrderController {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private LoadBalancer loadBalancer;
.....
@GetMapping("/lb")
public String getPaymentLB(){
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if(instances == null || instances.size() < 0){
return null;
}
ServiceInstance instance = loadBalancer.instances(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
}
接下来访问客户端的地址会看见端口号的轮询控制台也会打印对应的数据 代码地址