Ribbon核心源码解析(二)- ILoadBalancer组件

举报
码农参上 发表于 2022/04/29 10:11:10 2022/04/29
【摘要】 在上一篇文章中,我们介绍了Ribbon中的调用流程与负载均衡过程,本文我们再来详细看一看它的核心组件ILoadBalancer。 核心组件ILoadBalancer返回服务实例的调用过程大体已经了解了,但是我们刚才略过了一个内容,就是获取LoadBalancer的过程,回去看第一次调用的getServer方法:这里通过getLoadBalancer方法返回一个ILoadBalancer负载均...

在上一篇文章中,我们介绍了Ribbon中的调用流程与负载均衡过程,本文我们再来详细看一看它的核心组件ILoadBalancer。

核心组件ILoadBalancer

返回服务实例的调用过程大体已经了解了,但是我们刚才略过了一个内容,就是获取LoadBalancer的过程,回去看第一次调用的getServer方法:

这里通过getLoadBalancer方法返回一个ILoadBalancer负载均衡器,具体调用了Spring的BeanFactoryUtil,通过getBean方法从spring容器中获取类型匹配的bean实例:

回到前面getServer方法调用的那张图,你就会发现这时候已经返回了一个ZoneAwareLoadBalancer,并且其中已经保存好了服务列表。看一下ILoadBalancer 的接口定义:

public interface ILoadBalancer {
  //往该ILoadBalancer中添加服务
  public void addServers(List<Server> newServers);
  //选择一个可以调用的实例,keyb不是服务名称,而是zone的id
  public Server chooseServer(Object key);
  //标记下线服务
  public void markServerDown(Server server);
  @Deprecated
  public List<Server> getServerList(boolean availableOnly);
  //获取可用服务列表
  public List<Server> getReachableServers();
  //获取所有服务列表
  public List<Server> getAllServers();
}

该接口定义了Ribbon中核心的两项内容,服务获取服务选择,可以说,ILoadBalancer是Ribbon中最重要的一个组件,它起到了承上启下的作用,既要连接 Eureka获取服务地址,又要调用IRule利用负载均衡算法选择服务。下面分别介绍。

服务获取

Ribbon在选择之前需要获取服务列表,而Ribbon本身不具有服务发现的功能,所以需要借助Eureka来解决获取服务列表的问题。回到文章开头说到的配置类RibbonEurekaAutoConfiguration

@Configuration
@EnableConfigurationProperties
@ConditionalOnRibbonAndEurekaEnabled
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
public class RibbonEurekaAutoConfiguration {
}

其中定义了其默认配置类为EurekaRibbonClientConfiguration,在它的ribbonServerList方法中创建了服务发现组件DiscoveryEnabledNIWSServerList

DiscoveryEnabledNIWSServerList实现了ServerList接口,该接口用于初始化服务列表及更新服务列表。首先看一下ServerList的接口定义,其中两个方法分别用于初始化服务列表及更新服务列表:

public interface ServerList<T extends Server> {
    public List<T> getInitialListOfServers();
    public List<T> getUpdatedListOfServers();   
}

DiscoveryEnabledNIWSServerList中,初始化与更新两个方法其实调用了同一个方法来实现具体逻辑:

进入obtainServersViaDiscovery方法:

可以看到,这里先得到一个EurekaClient的实例,然后借助EurekaClient的服务发现功能,来获取服务的实例列表。在获取了实例信息后,判断服务的状态如果为UP,那么最终将它加入serverList中。

在获取得到serverList后,会进行缓存操作。首先进入DynamicServerListLoadBalancersetServerList方法,然后调用父类BaseLoadBalancersetServersList方法:

BaseLoadBalancer中,定义了两个缓存列表:

protected volatile List<Server> allServerList = Collections.synchronizedList(new ArrayList<Server>());
protected volatile List<Server> upServerList = Collections.synchronizedList(new ArrayList<Server>());

在父类的setServersList中,将拉取的serverList赋值给缓存列表allServerList

在Ribbon从Eureka中得到了服务列表,缓存在本地List后,存在一个问题,如何保证在调用服务的时候服务仍然处于可用状态,也就是说应该如何解决缓存列表脏读问题?

在默认负载均衡器ZoneAwareLoadBalancer的父类BaseLoadBalancer构造方法中,调用setupPingTask方法,并在其中创建了一个定时任务,使用ping的方式判断服务是否可用:

runPinger方法中,调用SerialPingStrategypingServers方法:

pingServers方法中,调用NIWSDiscoveryPingisAlive方法:

NIWSDiscoveryPing实现了IPing接口,在IPing 接口中,仅有一个isAlive方法用来判断服务是否可用:

public interface IPing {
    public boolean isAlive(Server server);
}

NIWSDiscoveryPingisAlive方法实现:

因为本地的serverList为缓存值,可能与eureka中不同,所以从eureka中去查询该实例的状态,如果eureka里面显示该实例状态为UP,就返回true,说明服务可用。

返回PingerrunPingger的方法调用处:

在获取到服务的状态列表后进行循环,如果状态改变,加入到changedServers中,并且把所有可用服务加入newUpList,最终更新upServerList中缓存值。但是在阅读源码中发现,创建了一个监听器用于监听changedServers这一列表,但是只是一个空壳方法,并没有实际代码对列表变动做出实际操作。

需要注意的是,在调试过程中当我下线一个服务后,results数组并没有按照预期的将其中一个服务的状态返回为false,而是results数组中的元素只剩下了一个,也就说明,除了使用ping的方式去检测服务是否在线外,Ribbon还使用了别的方式来更新服务列表。

我们在BaseLoadBalancersetServersList方法中添加一个断点:

等待程序运行,可以发现,在还没有进入执行IPing的定时任务前,已经将下线服务剔除,只剩下了一个可用服务。查看调用链,最终可以发现使用了定时调度线程池调用了PollingServerListUpdater类的start方法,来进行更新服务操作:

回到BaseLoadBalancersetServersList方法中:

在这里就用新的服务列表更新了旧服务列表,因此当执行IPing的线程再执行时,服务列表中只剩下了一个服务实例。

综上可以发现,Ribbon为了解决服务列表的脏读现象,采用了两种手段:

  • 更新列表
  • ping机制

在测试中发现,更新机制和ping机制功能基本重合,并且在ping的时候不能执行更新,在更新的时候不能运行ping,所以很难检测到ping失败的情况。

服务选取

服务选取的过程就是从服务列表中按照约定规则选取服务实例,与负载均衡算法相关。这里引入Ribbon对于负载均衡策略实现的接口IRule

public interface IRule{
    public Server choose(Object key);
    public void setLoadBalancer(ILoadBalancer lb);    
    public ILoadBalancer getLoadBalancer();    
}

其中choose为核心方法,用于实现具体的选择逻辑。

Ribbon中,下面7个类默认实现了IRule接口,为我们提供负载均衡算法:

在刚才调试过程中,可以知道Ribbon默认使用的是ZoneAvoidanceRule区域亲和负载均衡算法,优先调用一个zone区间中的服务,并使用轮询算法,具体实现过程前面已经介绍过不再赘述。

当然,也可以由我们自己实现IRule接口,重写其中的choose方法来实现自己的负载均衡算法,然后通过@Bean的方式注入到spring容器中。当然也可以将不同的服务应用不同的IRule策略,这里需要注意的是,Spring cloud的官方文档中提醒我们,如果多个微服务要调用不同的IRule,那么创建出IRule的配置类不能放在ComponentScan的目录下面,这样所有的微服务都会使用这一个策略。

需要在主程序运行的com包外另外创建一个config包用于专门存放配置类,然后在启动类上加上@RibbonClients注解,不同服务应用不同配置类:

@RibbonClients({@RibbonClient(name="eureka-hi",configuration = HiRuleConfig.class),
        @RibbonClient(name = "eureka-test",configuration = TestRuleConfig.class)})
public class ServiceFeignApplication {
……
}

总结

综上所述,在Ribbon的负载均衡中,大致可以分为以下几步:

  • 拦截请求,通过请求中的url地址,截取服务名称
  • 通过LoadBalancerClient获取ILoadBalancer
  • 使用Eureka获取服务列表
  • 通过IRule负载均衡策略选择具体服务
  • ILoadBalancer通过IPing及定时更新机制来维护服务列表
  • 重构该url地址,最终调用HttpURLConnection发起请求

了解了整个调用流程后,我们更容易明白为什么Ribbon叫做客户端的负载均衡。与nginx服务端负载均衡不同,nginx在使用反向代理具体服务的时候,调用端不知道都有哪些服务。而Ribbon在调用之前,已经知道有哪些服务可用,直接通过本地负载均衡策略调用即可。而在实际使用过程中,也可以根据需要,结合两种方式真正实现高可用。

最后

觉得对您有所帮助,小伙伴们可以点个赞啊,非常感谢~
公众号『码农参上』,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。欢迎来加我好友 DrHydra9,围观朋友圈,做个点赞之交。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。