SpringCloud-负载均衡-ribbon

时间:2024-10-26 08:06:35

本篇是从基础方便讲解一些springcloud-负载均衡-ribbon中的一些理论性的内容;还会讲一些源码内容;后面的文章会将源码进行整理,并且将源码的github地址上传。

1.什么是负载均衡

核心概念:用户来访请求应该相对平均的分摊到每个节点;
不要将所有工作量都给一个节点。

LB策略的目的(负载均衡):海量的用户请求均匀的分配到集群中的每一台机器上

设计系统时需要有一定的余量(应对系统不同情况)

2.客户端负载与服务端负载均衡

客户端的负载均衡:服务机器的列表(客户端保存copy,且动态变化)
+LB策略(负载均衡策略)
在这里插入图片描述
服务端的负载均衡:nignx/F5等
大型应用通常是客户端+服务端负载均衡搭配使用
在这里插入图片描述
客户端负载均衡(红色)与服务端负载均衡的比较:
客户端,对开发团队更加友好,代码中可以决定使用那种负载均衡策略(配置文件)。
运维成本低(不需要额外的组件,所有的负载均衡策略在服务端就制定好了)
强依赖于注册中心,从注册中心拉到可用服务的列表(可动态更新)
微服务框架搭配使用,gateway,zookeeper,ribbon偏多。

服务端,开发团队很难去主动的发起修改,负载均衡都在网关层,由专业的网络人员来
操作,修改。网关层语言跟日常的java有很大的区别。
运维成本高(需要额外的组件,使用F5还要搭配硬件设备)
通常不依赖(SpringCloud架构zookeeper,gateway时存在依赖)
Tomcat,JBoss部署传统应用:nginx,F5偏多

3.Ribbon体系架构解析

Ribbon体系架构:
丰富的组件库;适配性好;(牛的Ribbon可以脱离
SpringCloud可以应用在一般项目中)
组件图:
在这里插入图片描述

一个HttpRequest发过来,先被转发到Eureka上。此时Eureka仍然通过服务发现获取了所有服务节点的物理地址,但问题是他不知道该调用哪一个,只好把请求转到了Ribbon手里。

IPing IPing是Ribbon的一套healthcheck机制,故名思议,就是要Ping一下目标机器看是否还在线,一般情况下IPing并不会主动向服务节点发起healthcheck请求,Ribbon后台通过静默处理返回true默认表示所有服务节点都处于存活状态(和Eureka集成的时候会检查服节点UP状态)。
IRule 这就是Ribbon的组件库了,各种负载均衡策略都继承自IRule接口。所有经过Ribbon的请求都会先请示IRule一把,找到负载均衡策略选定的目标机器,然后再把请求转发过去。

实现的demo:
在eureka-consumer中,

@SpringBootApplication
@EnableDiscoveryClient
public class EurekaConsumerApplication {
    @Bean
    public RestTemplate register(){
        return new RestTemplate();
    }
    public static void main(String[] args) {
        new SpringApplicationBuilder(EurekaConsumerApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

在eureka-consumer的controller中,实现的方式是

@RestController
public class Controller {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping
    public String sayHi() {
            return restTemplate.getForObject(
                    "http://eureka-client/sayHi",
                    String.class);
    }

}

在Ribbon-consumer的controller中如何实现(LB策略)下获取对应的节点:
这里直接将地址写上去(直接应用服务名)。返回值是String.class。
后台是如何找到对应的服务和对应的端口呢?
key就在@LoadBalanced

@Bean
@LoadBalanced
public RestTemplate template(){
return new RestTemplate();
}

4.懒加载与饥饿加载

INFO [main] com.netflix.loadbalancer.DynamicServerListLoadBalancer -
DynamicServerListLoadBalancer for client eureka-consumer initialized

这行log意思是,Ribbon客户端已经完成了LoadBalancer的初始化。初始化是没问题的,但问题是初始化发生的时间,为什么偏偏发生在首次超时之前呢?

这就要从Ribbon的懒加载说起了,原来Ribbon是在第一次方法调用的时候才去初始化LoadBalancer。这样看来,第一个方法请求不仅仅包含HTTP连接和方法的响应时间,还包括了LoadBalancer的创建耗时。假如你的方法本身就比较耗时的话,而且超时时间又设置的比较短,那么很大可能这第一次http调用就会失败。其实还有很多框架也实现了类似的懒加载功能,比如Hibernate的lazy-fetch,懒加载在大部分情况下可以节省系统资源开销,但某些情况下反而导致服务响应时间被延长。

之所以这类问题难以发现的原因,在于它无法稳定重现,比如只能通过重启来重现,对这种问题往往直接从生产环境的log入手去分析是比较有效的手段。

Ribbon–饥饿加载:
ribbon.eager-load.enabled=true
ribbon.eager-load.clients=ribbon-consumer

第一个参数开启了Ribbon的饥饿加载模式,第二个属性指定了需要应用饥饿加载的服务名称。完成上面配置并再次重启服务,就会发现LoadBalancer初始化日志在方法调用之前就打印出来了

5.负载均衡策略-七种策略
RandomRule:随性而为:随机挑选访问节点
RoundRobinRule:按部就班从一个节点一步一步向后选取节点,
不会跳过一个,不会原地踏步,每次只会向后移动一步;
在这里插入图片描述
假如在多线程环境下,两个请求同时访问这个Rule是否会读取到相同节点呢?不会,这靠的是RandomRobinRule底层的自旋锁+CAS的同步操作。CAS的全称是compare and swap,是一种借助操作系统函数来实现的同步操作。类似Eureka为了防止服务下线被重复调用,就使用AtomicBoolean的CAS方法做同步控制,CAS+自旋锁这套组合技是高并发下最廉价的线程安全手段,因为这套操作不需要锁定系统资源。有优点也有缺点,自旋锁如果迟迟不能释放,将会带来CPU资源的浪费,因为自旋本身并不会执行任何业务逻辑,而是单纯的使CPU“空转”。所以通常情况下会对自旋锁的旋转次数做一个限制,比如JDK中synchronize底层的锁升级策略,就对自旋次数做了动态调整。

// CAS+自旋锁获取系统资源的打开方式,真实应用中还要注意防止无休止自旋:
// 或者for (;;) 做自旋
while (true) { 
	// cas操作
	if (cas(expected, update)) {
		// 业务逻辑代码
		// break或退出return
	}  }

Netflix是特别喜欢用自旋+CAS,作为中间件来说性能是非常重要的。
RetryRule:卷土重来
一个类似装饰器模式的Rule。装饰器相当于一层层的俄罗斯套娃,每一层都会加上一层独特的buff。

RetryRule也是同样的道理,他的BUFF就是给其他负载均衡策略加上“重试”功能。而在RetryRule里还藏着一个subRule,这才是隐藏在下面的真正被执行的负载均衡策略,RetryRule正是要为它添加重试功能(如果初始化时没指定subRule,将默认使用RoundRibinRule)。

WeightedResponseTimeRule:能者多劳
继承自RoundRibbonRule,他会根据服务节点的响应时间计算权重,
响应时间越长权重就越低,响应越快则权重越高。权重的高低决定了
机器被选中概览的高低。就是说,响应时间越小的机器,被选中的概览越大。
在这里插入图片描述
由于服务器刚启动的时候,对各个服务节点采样不足,因此采用轮询策略,当积累
到一定的样本时,会切换到WeightedResponseTimeRule模式。

BestAvailableRule:让最闲的人来
在这里插入图片描述
有点智能。过滤掉故障服务以后,它会基于30分钟的统计结果选取当前并发量最小的
服务结点,也就最”闲”的节点作为目标地址。
如果无统计结果,则采用轮询的方式选定节点。
关键字:过滤故障服务;选取并发量最小的节点。

AvailabilityFilteringRule:我是有底线的(AFR)
底层依赖RandomRobinRule来选取节点,满足它的最小要求的节点才会被选中。
如果节点满足要求,无论响应时间或当前并发量是什么。都会被选中。
在这里插入图片描述
每次AvailabilityFilteringRule(简称AFR)都会请求RobinRule挑选一个节点,然后对这个节点做以下两步检查:

是否处于熔断状态(熔断是Hystrix中的知识点,后面章节会讲到,这里大家可以把熔断当做服务不可用)
节点当前的active请求连接数超过阈值,超过了则表示节点目前太忙,不适合接客
如果被选中的server不幸挂掉了检查,那么AFR会自动重试(次数最多10次),让RobinRule重新选择一个服务节点。

ZoneAvoidanceRule:我的地盘我做主
在这里插入图片描述

ZoneFilter:在Eureka注册中一个服务结点有Zone,Region和URL三个身份信息
其中Zone可以理解为机房大区(未指定则由Eureka给定默认值),
这里会对这个Zone的监控情况过滤器下面所有服务结点
可用性过滤:这里和AvailabilityFilteringRule的验证非常像,会过滤当前并发量
较大,或者处于熔断的服务节点。

5.配置负载均衡策略

Ribbon默认的策略是:RoundRobinRule。一个个轮询

配置负载均衡策略的方式:@Bean注入的方式。也可以放在启动类中

@Configuration
public class RibbonConfiguration {

    @Bean
    public IRule delaultLBStrategy() {
        return new RandomRule();
    }

}

针对特定的服务的id的负载均衡策略:
配置在application.yml

eureka-client:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

注解的方式:
(配置在public class RibbonConfiguration {)
//@RibbonClient(name = “eureka-client”, configuration = MyRule.class)

6.部分源码品读:

概念:
**真假随机数:**真随机数是通过物理过程(如放射性衰变、大气噪声等)生成的,其每个数字都是完全随机的,无法预测。而假随机数则是通过算法生成的,其每个数字都是基于前一个数字计算得出的,存在一定的规律性,可以被预测。因此,在需要高度安全性的场景中,应该使用真随机数来保证随机性。

RandomRule核心:

@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
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) {
            /*
             * No servers. End regardless of pass, because subsequent passes
             * only get more restrictive.
             */
            return null;
        }
        int index = chooseRandomInt(serverCount);
        server = upList.get(index);
        if (server == null) {
            /*
             * The only time this should happen is if the server list were
             * somehow trimmed. This is a transient condition. Retry after
             * yielding.
             */
            Thread.yield();
            continue;
        }
        if (server.isAlive()) {
            return (server);
        }
        // Shouldn't actually happen.. but must be transient or a bug.
        server = null;
        Thread.yield();
    }
    return server;
}

对于上面的分析:ILoadBalancer lb。
当server = = null. If(Thread.interrupted()),表示线程中断,则返回空。防御性编程
if (server == null) {
Thread.yield(); continue; }
Thread.yield()表示线程让渡,当前线程愿意让出CPU。
如果server.isAlive,则直接返回。
为空,则调用Thread.yield(),完成线程让步。
原因是没有其他退出线程的办法。

RoundRobinRule:在代码最后没有做线程让渡
因为有Count计数器,10次之后就自动退出了。

   // Next.
    server = null;
}

if (count >= 10) {
    log.warn("No available alive servers after 10 tries from load balancer: "
            + lb);
}

在这里插入图片描述

for(;????:自旋锁的实现。
自旋锁+NSCC.CAS操作是一个非常高效的线程同步方式。

private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextServerCyclicCounter.get();
        int next = (current + 1) % modulo;
        if (nextServerCyclicCounter.compareAndSet(current, next))
            return next;
    }
}

全部代码:RoundRobinRule核心部分

public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        log.warn("no load balancer");
        return null;
    }

    Server server = null;
    int count = 0;
    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;
}

BestAvailableRule:
分析先判断loadBalancerStats ==null为空时,
直接调用super.choose(key).
它调用的是上层(ClientConfigEnabledRoundRobinRule).
里面的代码是:

@Override
public Server choose(Object key) {
    if (roundRobinRule != null) {
        return roundRobinRule.choose(key);
    } else {
        throw new IllegalArgumentException(
                "This class has not been initialized with the RoundRobinRule class");
    }
}

继续分析:这里代码是直接调用上层的roundRobinRule的choose方法。具体
RoundRobinRule核心choose方法上面已经进行了分析

获取所有的getAllServers.以及当前时间
遍历serverList,首先获取serverStats.
isCircuitBreakerTripped :一个布尔值,用于表示断路器是否触发。如果断路器已触发,则该值为 true;否则为 false。

对于isCircuitBreakerTripped
这段代码是一个名为 isCircuitBreakerTripped 的 Java 方法,它这段代码是一个名为 isCircuitBreakerTripped 的 Java 方法,它接受一个长整型参数 currentTime。该方法的作用是判断断路器是否触发。
首先,通过调用 getCircuitBreakerTimeout() 方法获取断路器超时时间,并将其赋值给变量 circuitBreakerTimeout。如果 circuitBreakerTimeout 小于等于 0,则表示断路器未设置超时时间,因此返回 false。
如果 circuitBreakerTimeout 大于 0,则比较当前时间 currentTime 和断路器超时时间 circuitBreakerTimeout。如果当前时间小于等于断路器超时时间,说明断路器还未触发,返回 false;否则,说明断路器已触发,返回 true。

BestAvailableRule的核心代码:

@Override
public Server choose(Object key) {
    if (loadBalancerStats == null) {
        return super.choose(key);
    }
    List<Server> serverList = getLoadBalancer().getAllServers();
    int minimalConcurrentConnections = Integer.MAX_VALUE;
    long currentTime = System.currentTimeMillis();
    Server chosen = null;
    for (Server server: serverList) {
        ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
        if (!serverStats.isCircuitBreakerTripped(currentTime)) {
            int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
            if (concurrentConnections < minimalConcurrentConnections) {
                minimalConcurrentConnections = concurrentConnections;
                chosen = server;
            }
        }
    }
    if (chosen == null) {
        return super.choose(key);
    } else {
        return chosen;
    }
}

RetryRule:给与其他Rule进行重试策略

public Server choose(ILoadBalancer lb, Object key) {
   long requestTime = System.currentTimeMillis();
   long deadline = requestTime + maxRetryMillis;

   Server answer = null;

   answer = subRule.choose(key);

   if (((answer == null) || (!answer.isAlive()))
         && (System.currentTimeMillis() < deadline)) {

      InterruptTask task = new InterruptTask(deadline
            - System.currentTimeMillis());

      while (!Thread.interrupted()) {
         answer = subRule.choose(key);

         if (((answer == null) || (!answer.isAlive()))
               && (System.currentTimeMillis() < deadline)) {
            /* pause and retry hoping it's transient */
            Thread.yield();
         } else {
            break;
         }
      }
      task.cancel();
   }
   if ((answer == null) || (!answer.isAlive())) {
      return null;
   } else {
      return answer;
   }
}

7.负载均衡器LoadBalancer原理解析

在这里插入图片描述

@LoadBalanced 这个注解一头挂在RestTemplate上,另一头挂在LoadBalancerAutoConfiguration这个类上。它就像连接两个世界的传送门,将所有顶着「LoadBalanced」注解的RestTemplate类,都传入到LoadBalancerAutoConfiguration中。如果要深挖底层的作用机制,大家可以发现这个注解的定义上还有一个@Qualifier注解,@Qualifier注解搭配@Autowired注解做自动装配,可以通过name属性,将指定的Bean装载到指定位置(即使有两个同样类型的Bean,也可以通过Qualifier定义时声明的name做区分)。这里「LoadBalanced」也是借助Qualifier实现了一个给RestTemplate打标签的功能,凡是被打标的RestTemplate都会被传送到AutoConfig中做进一步改造。
LBAutoConfig 从前一步中传送过来的RestTemplate,会经过LBAutoConfig的装配,将一系列的Interceptor(拦截器)添加到RestTemplate中。拦截器是类似职责链编程模型的结构,我们常见的ServletFilter,权限控制器等,都是类似的模式。Ribbon拦截器会拦截每个网络请求做一番处理,在这个过程中拦截器会找到对应的LoadBalancer对HTTP请求进行接管,接着LoadBalancer就会找到默认或指定的负载均衡策略来对HTTP请求进行转发。
总结Ribbon的作用机制就是,由LoadBalanced在RestTemplate上打标,Ribbon将带有负载均衡能力的拦截器注入标记好的RestTemplate中,以此实现了负载均衡。

IPing机制
在这里插入图片描述

DummyPing,默认返回true,即认为所有节点都可用,这也是单独使用Ribbon时的默认模式
NIWSDiscoveryPing,借助Eureka服务发现机制获取节点状态,假如节点状态是UP则认为是可用状态
PingUrl,它会主动向服务节点发起一次http调用,如果对方有响应则认为节点可用
大家可以看出第三种主动出击的模式比较生猛。大家可以想象,假如我们的服务节点搭载的是随便一个微服务都有大几千台服务器,在服务本身就被超高访问量调用的情况下,那这种主动出击的IPing策略必然会大大增加服务节点的访问压力。

既然Eureka已经有了服务发现机制,可以获取节点的当前状态,拿来就用岂不更好?因此,除非特殊指定,在和Eureka搭配使用的时候,采用的是第二种,也就是过滤非UP状态的节点(其实这个功能直接放Eureka里也能做)

8.IPing机制解析

public interface IPing接口只有一个方法public boolean isAlive(Server server);
5个实现类
在这里插入图片描述
对于DummyPing,isAlive方法,直接返回true
NoOpPing同上。

NIWSDiscoveryPing代码:

DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;	            
InstanceInfo instanceInfo = dServer.getInstanceInfo();

发现Eureka的服务列表,然后通过是否是up状态来判断对应的
Server是否处于alive状态

public boolean isAlive(Server server) {
    boolean isAlive = true;
    if (server!=null && server instanceof DiscoveryEnabledServer){
           DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;                
           InstanceInfo instanceInfo = dServer.getInstanceInfo();
           if (instanceInfo!=null){                    
               InstanceStatus status = instanceInfo.getStatus();
               if (status!=null){
                   isAlive = status.equals(InstanceStatus.UP);
               }
           }
       }
    return isAlive;
}

对于PingUrl类:拼接url,向服务节点发起调用

public boolean isAlive(Server server) {
    String urlStr = "";
    if (this.isSecure) {
        urlStr = "https://";
    } else {
        urlStr = "http://";
    }

    urlStr = urlStr + server.getId();
    urlStr = urlStr + this.getPingAppendString();
    boolean isAlive = false;
    HttpClient httpClient = new DefaultHttpClient();
    HttpUriRequest getRequest = new HttpGet(urlStr);
    String content = null;

    try {
        HttpResponse response = httpClient.execute(getRequest);
        content = EntityUtils.toString(response.getEntity());
        isAlive = response.getStatusLine().getStatusCode() == 200;
        if (this.getExpectedContent() != null) {
            LOGGER.debug("content:" + content);
            if (content == null) {
                isAlive = false;
            } else if (content.equals(this.getExpectedContent())) {
                isAlive = true;
            }