Ribbon 内置的负载均衡规则
在 com.netflix.loadbalancer
包下有一个接口 IRule
,它可以根据特定的算法从服务列表中选取一个要访问的服务,默认使用的是「轮询机制」
- RoundRobinRule:轮询
- RandomRule:随机
- RetryRule:先按照
RoundRobinRule
的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务 - WeightedResponseTimeRule:对
RoundRobinRule
的扩展,响应速度越快的实例选择权重越大,越容易被选择 - BestAvailableRule:会过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
- AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
- ZoneAvoidanceRule:默认规则,复合判断 server 所在区域的性能和 server 的可用性选择服务器
负载规则的替换
如果不想使用 Ribbon 默认使用的规则,我们可以通过自定义配置类的方式,手动指定使用哪一种。
需要注意的是,自定义配置类不能放在 @ComponentScan
所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了。
因此我们需要在 Spring Boot 启动类所在包的外面新建一个包存放自定义配置类
@Configuration
public class MyselfRule {
@Bean
public IRule rule(){
//随机
return new RandomRule();
}
}
然后在启动类上添加如下注解,指定服务名及自定义配置类
@RibbonClient(value = "CLOUD-PAYMENT-SERVICE", configuration = MyselfRule.class)
Ribbon 默认负载轮询算法的原理
算法概述
rest 接口第几次请求数 % 服务器集群总个数 = 实际调用服务器位置下标,服务每次重启后 rest 请求数变为1
源码
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
//循环获取服务,最多获取10次
while (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)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
//计算下一个服务的下标
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
//通过此方法获取服务的下标,使用了 CAS 和自旋锁
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
手写轮询算法
在服务提供者写一个方法,返回端口号看效果就行
@GetMapping("/payment/lb")
public String roundLb(){
return this.serverPort;
}
负载均衡接口
public interface LoadBalancer {
/**
* 获取服务实例
*/
ServiceInstance getInstance(List<ServiceInstance>serviceInstances);
}
算法实现类
@Component
public class MyLb implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 使用「自旋锁」和「CAS」增加请求次数
*/
public final int incrementAndGet() {
int current;
int next;
do {
current = atomicInteger.get();
//防溢出
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
} while (!atomicInteger.compareAndSet(current, next));
return next;
}
@Override
public ServiceInstance getInstance(List<ServiceInstance> serviceInstances) {
// 实际调用服务器位置下标 = rest 接口第几次请求数 % 服务器集群总个数
int index = incrementAndGet() % serviceInstances.size();
return serviceInstances.get(index);
}
}
编写服务消费者方法,记得注释 @LoadBalanced
注解,否则不生效
@GetMapping("/consumer/payment/lb")
public String roundLb(){
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null || instances.size() <= 0){
return null;
}
ServiceInstance instance = loadBalancer.getInstance(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}