Spring Cloud Ribbon负载均衡策略详解

这里使用的ribbon的版本是:ribbon-loadbalancer-2.2.2.jar。


一,IRule接口


IRule接口定义了选择负载均衡策略的基本操作。通过调用choose()方法,就可以选择具体的负载均衡策略。

// 选择目标服务节点

Server choose(Object var1);

// 设置负载均衡策略

void setLoadBalancer(ILoadBalancer var1);

// 获取负载均衡策略

ILoadBalancer getLoadBalancer();


二,ILoadBalancer接口


ILoadBalancer接口定义了ribbon负载均衡的常用操作,有以下几个方法:


void addServers(List<Server> var1);


Server chooseServer(Object var1);


void markServerDown(Server var1);


List<Server> getReachableServers();


List<Server> getAllServers();



三,AbstractLoadBalancerRule抽象类


AbstractLoadBalancerRule实现了IRule接口和IClientConfigAware接口,主要对IRule接口的2个方法进行了简单封装。

private ILoadBalancer ib;

// 设置负载均衡策略

public void setLoadBalancer(ILoadBalancer ib){

    this.ib = ib;

}

// 获取负载均衡策略

public ILoadBalancer getLoadBalancer(){

    return this.ib;

}


AbstractLoadBalancerRule是每个负载均衡策略需要直接继承的类,Ribbon提供的几个负载均衡策略,都继承了这个抽象类。同理,我们如果需要自定义负载均衡策略,也要继承这个抽象类。


四,AbstractLoadBalancerRule的实现类


AbstractLoadBalancerRule的实现类就是ribbon的具体负载均衡策略,首先来看默认的轮询策略。


1,轮询策略(RoundRobinRule)

轮询策略理解起来比较简单,就是拿到所有的server集合,然后根据id进行遍历。这里的id是ip+端口,Server实体类中定义的id属性如下:

this.id = host + ":" + port

这里还有一点需要注意,轮询策略有一个上限,当轮询了10个服务端节点还没有找到可用服务的话,轮询结束。


2,随机策略(RandomRule)

随机策略:使用jdk自带的随机数生成工具,生成一个随机数,然后去可用服务列表中拉取服务节点Server。如果当前节点不可用,则进入下一轮随机策略,直到选到可用服务节点为止。

这里在while循环中,使用了Thread#interrupted()方法和Thread#yield()方法,使用的很巧妙,可以参考一下。

while(server == null) {

    if (Thread.interrupted()) {

        return null;

    }

……

    // 如果随机打到的节点为null,则再次循环随机

    if (server == null) {

        Thread.yield();

    } else {

        if (server.isAlive()) {

            return server;

        }

        // 如果节点不是存活状态,则再次循环随机

        server = null;

        Thread.yield();

    }

}


3,可用过滤策略(AvailabilityFilteringRule)

策略描述:过滤掉连接失败的服务节点,并且过滤掉高并发的服务节点,然后从健康的服务节点中,使用轮询策略选出一个节点返回。


AvailabilityFilteringRule#choose()方法实现如下:

// 记录轮询次数,最多轮询10次

int count = 0;

// 轮询10次

for(Server server = roundRobinRule.choose(key); count++ <= 10; server = roundRobinRule.choose(key)) {

    if (this.predicate.apply(new PredicateKey(server))) {

        return server;

    }

}

// 如果轮询10次还没有找到可用server,则执行父类中的筛选逻辑

return super.choose(key);


4,响应时间权重策略

(WeightedResponseTimeRule)

策略描述:根据响应时间,分配一个权重weight,响应时间越长,weight越小,被选中的可能性越低。

如何计算权重呢?代码逻辑位于WeightedResponseTimeRule$$ServerWeight#maintainWeights(),判断逻辑还是挺复杂的,暂时没时间看,先留着,以后有机会再研究。

注意,服务刚启动时,由于统计信息不足,先使用轮询策略。等到信息足够了,切换到WeightedResponseTimeRule策略。


5,轮询失败重试策略(RetryRule)


轮询失败重试策略(RetryRule)是这样工作的,首先使用轮询策略进行负载均衡,如果轮询失败,则再使用轮询策略进行一次重试,相当于重试下一个节点,看下一个节点是否可用,如果再失败,则直接返回失败。

这里还有一个点要注意,重试的时间间隔,默认是500毫秒,我们可以自定义这个重试时间间隔。

this.maxRetryMillis = 500L;


另外,失败重试策略的源码中,RetryRule#choose()方法很有参考价值,如果我们需要自己实现调用接口的失败重试功能的话,可以参考这个方法。这个方法中给我们展示了如何使用Thread#interrupted()和Thread#yield()。


6,并发量最小可用策略(BestAvailableRule)

策略描述:选择一个并发量最小的server返回。如何判断并发量最小呢?ServerStats有个属性activeRequestCount,这个属性记录的就是server的并发量。轮询所有的server,选择其中activeRequestCount最小的那个server,就是并发量最小的服务节点。

接下来我们看源码:

if (this.loadBalancerStats == null) {

    return super.choose(key);

} else {

    // 获取所有的server

    List<Server> serverList = this.getLoadBalancer().getAllServers();

    int minimalConcurrentConnections = 2147483647;

    long currentTime = System.currentTimeMillis();

    Server chosen = null;

    Iterator var7 = serverList.iterator();

   

    while(var7.hasNext()) {

        Server server = (Server)var7.next();

        ServerStats serverStats = this.loadBalancerStats.getSingleServerStats(server);

        // 判断断路器是否跳闸,如果没有跳闸,继续往下走。

        if (!serverStats.isCircuitBreakerTripped(currentTime)) {

            int concurrentConnections = serverStats.getActiveRequestCount(currentTime);

            if (concurrentConnections < minimalConcurrentConnections) {

                minimalConcurrentConnections = concurrentConnections;

                chosen = server;

            }

        }

    }


    // 如果没有找到并发量最小的服务节点,则使用父类的策略

    if (chosen == null) {

        return super.choose(key);

    } else {

      return chosen;

    }

}


并发量最小可用策略(BestAvailableRule)的优点是:可以充分考虑每台服务节点的负载,把请求打到负载压力最小的服务节点上。但是缺点是:因为需要轮询所有的服务节点,如果集群数量太大,那么就会比较耗时。当然一般来说,几十台,几百台的集群数量是不用考虑这个问题的。因此对于大部分的项目而言,是一个不错的选择。


7,ZoneAvoidanceRule

策略描述:复合判断server所在区域的性能和server的可用性,来选择server返回。


四,BaseLoadBalancer


BaseLoadBalancer是一个负载均衡器,是ribbon框架提供的负载均衡器。Spring Cloud对ribbon封装以后,直接调用ribbon的负载均衡器来实现微服务客户端的负载均衡。

这里需要注意,ribbon框架本身提供了几个负载均衡器,BaseLoadBalancer只是其中之一。

Spring Cloud是如何封装ribbon框架的呢?Spring Cloud提供了2个接口:ServiceInstanceChooser和LoadBalancerClient,这2个接口就是客户端负载均衡的定义。具体实现类是RibbonLoadBalancerClient。RibbonLoadBalancerClient#choose()方法根据微服务实例的serviceId,然后使用配置的负载均衡策略,打到对于的微服务实例节点上。

OK,到这里,我们简单梳理一下Spring Cloud集成ribbon后,负载均衡的执行逻辑。

1,Spring Cloud RibbonLoadBalancerClient#choose()调用ribbon框架的BaseLoadBalancer。

2,BaseLoadBalancer#chooseServer()选择具体的负载均衡策略(RoundRibonRule),然后执行。

但是,RibbonLoadBalancerClient#choose()是在哪里调用的呢?这里用到了拦截器,@RibbonClient注解自动化配置类LoadBalancerAutoConfiguration.class中有两个注解:

@ConditionalOnClass({RestTemplate.class})

@ConditionalOnClass({LoadBalancerClient.class})

也就是说,在RestTemplate.class和LoadBalancerClient.class存在的情况下,LoadBalancerInterceptor.class会拦截RestTemplate.class上的@LoadBalanced注解,然后将请求中的微服务实例名serviceId转化为具体的ip+端口,然后去请求目标服务节点。


OK,有点乱,我们再来梳理一下调用关系:

1,@LoadBalanced注解

2,org.springframework.web.client.RestTemplate

3,LoadBalancerAutoConfiguration.class

4,LoadBalancerInterceptor.class拦截org.springframework.web.client.RestTemplate的请求,注入客户端负载均衡功能,发送请求到目标服务节点。

这就是Spring Cloud 集成的ribbon客户端负载均衡。


五,如何自定义负载均衡策略


Ribbon不仅实现了几种负载均衡策略,也为开发者提供了自定义负载均衡策略的支持。

自定义负载均衡策略有3个关键点:

1,继承抽象类AbstractLoadBalancerRule

2,自定义的负载均衡策略类,不能放在@ComponentScan所扫描的当前包和子包下。

3,在主启动类上添加@RibbonClient注解,或者在配置文件中指定哪个微服务使用自定义负载均衡策略。

@RibbonClient注解的使用方法如下:

@RibbonClient(name="微服务名称", configuration="自定义配置类.class")

配置文件中配置项如下:

springboot.微服务名称.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

或者

springboot.微服务名称.NFLoadBalancerRuleClassName=com.xxx.xxx.xxx.自定义负载均衡策略实现类


这里需要注意,Spring Cloud微服务启动类上有个注解@SpringBootApplication,这个注解的源码上标注了@ScanComponent注解,也就是说,我们的微服务启动类其实间接引入了@ComponentScan注解。

@ComponentScan注解的作用就是扫描当前包及其子包下的标有指定注解的bean,然后把它们注入到IOC容器。

@Component会扫描哪些注解呢?有4个注解,分别是:

@Component

@Service

@Controller

@Repository

另外,所有间接引入了上面4个注解的注解,最终也会被@ComponentScan扫描,比如我们常用的@Configuration,也会被@ComponentScan扫描。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容