Ribbon核心源码解析

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
全局流量管理 GTM,标准版 1个月
EMR Serverless StarRocks,5000CU*H 48000GB*H
简介: Ribbon核心源码解析

Spring cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡工具,简单的说,它能够使用负载均衡器基于某种规则或算法调用我们的微服务集群,并且我们也可以很容易地使用Ribbon实现自定义负载均衡算法。

在之前使用Eureka的过程中,需要导入对应的依赖,但是Ribbon有一点特殊,不需要引入依赖也可以使用。这是因为在Eureka-client中,已经默认为我们集成好了Ribbon,可以直接拿来使用。

image.png

根据Spring Boot自动配置原理,先从各个starter的spring.factories中寻找可能存在的相关配置类:

1、在spring-cloud-common中,存在自动配置类:LoadBalancerAutoConfiguration

2、在eureka-client中,存在配置类RibbonEurekaAutoConfiguration

3、在ribbon中,存在配置类RibbonAutoConfiguration

需要注意,RibbonEurekaAutoConfiguration中存在@AutoConfigureAfter注解,说明需要在加载RibbonAutoConfiguration配置类后再加载当前配置类。这三个类的配置将在后面结合具体代码调试中说明。

image.png

下面我们通过代码调试的方式来探究Ribbon的运行流程。

image.png

Ribbon的调用过程非常简单,使用RestTemplate加上@LoadBalanced注解就可以开启客户端的负载均衡,写一个简单的测试用例进行测试:

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
   return new RestTemplate();
}
@GetMapping("/test")
public String test(String service){
    String result=restTemplate.getForObject("http://eureka-hi/"+service,String.class);
    System.out.println(result);
    return result;
}

结果:

image.png

通过结果可以看出,RestTemplate基于服务名称,即可实现访问Eureka-client集群下的不同服务实例,实现负载均衡的调用方式。看一下@LoadBalanced注解的定义:

/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

注释说明了@LoadBalanced用于注解在RestTemplate上实现负载均衡,那么来看一下@LoadBalanced注解是如何生效的呢?回到前面提到的配置类LoadBalancerAutoConfiguration中:

image.png

在配置类中定义了一个LoadBalancerInterceptor拦截器,并且为restTemplate添加了这个拦截器。在restTemplate每次执行方法请求时,都会调用intercept方法执行拦截:

image.png

在上面的intercept拦截方法中,首先获取本次访问的url地址,从中获取本次要访问的服务名,然后调用RibbonLoadBalancerClient中的execute方法。

image.png

在这里通过服务名获取了该服务对应的负载均衡器ILoadBalancer的实例对象,然后调用该实例的chooseServer方法获取一个可用服务实例,关于ILoadBalancer会在后面具体介绍。

image.png

在execute方法调用apply方法的过程中,会调用LoadBalancerContext的reconstructURIWithServer方法重构将要访问的url地址:

image.png

在拼接完成URL后,调用AbstractClientHttpRequest类的execute方法发送请求。

image.png

调用executeInternal方法:

image.png

可以看到,最终RestTemplate底层调用了HttpURLConnection来发送请求。

总体的调用流程我们总结完了,那么负载均衡的过程究竟是如何实现的呢?我们来详细梳理一下。

image.png

在Ribbon中有个非常重要的组件LoadBalancerClient,它是负载均衡的一个客户端,我们从这入手写一个测试接口:

@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/choose")
public String loadBalance(String serviceId){
    ServiceInstance instance = loadBalancerClient.choose(serviceId);
    System.out.println(instance.getHost()+" "+instance.getPort());
    return "ok";
}

调用接口测试结果,可以看出是通过LoadBalancerClient 的choose方法,选择调用了不同端口上的服务实例,体现了负载均衡:

image.png

对代码进行调试,发现注入的LoadBalancerClient的实现类正是之前看见过的RibbonLoadBalancerClient,进入其choos方法中,先后调用两次getServer方法:

image.png

此时loadBalancer实例对象为ZoneAwareLoadBalancer,并且里面的allServerList列表已经缓存了所有的服务列表。调用chooseServer方法,由于此时我们只有一个zone,所以默认调用父类BaseLoadBalancer的chooseServer方法:

image.png

在父类的方法中,根据IRule实例定义的规则来确定返回哪一个具体的Server:

image.png

这里的IRule实现使用了默认的ZoneAvoidanceRule,为区域内亲和选择算法。关于IRule负载均衡算法在后面再做介绍。由于ZoneAvoidanceRule中没有实现choose方法,直接调用其父类PredicateBasedRule的choose方法:

image.png

调用AbstractServerPredicate的chooseRoundRobinAfterFiltering方法:

image.png

实现非常简单,通过轮询的方式选择下标:

image.png

返回choose方法中,可以看到已经获得了一个server实例:

image.png

image.png

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

image.png

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

image.png

回到前面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利用负载均衡算法选择服务。下面分别介绍。

image.png

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

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

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

image.png

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

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

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

image.png

进入obtainServersViaDiscovery方法:

image.png

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

在获取得到serverList后,会进行缓存操作。首先进入DynamicServerListLoadBalancer 的setServerList方法,然后调用父类BaseLoadBalancer的setServersList方法:

image.png

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

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

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

image.png

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

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

image.png

runPinger方法中,调用SerialPingStrategy的pingServers方法:

image.png

pingServers方法中,调用NIWSDiscoveryPing的isAlive方法:

image.png

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

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

NIWSDiscoveryPing的isAlive方法实现:

image.png

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

返回Pinger的runPingger的方法调用处:

image.png

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

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

我们在BaseLoadBalancer的setServersList方法中添加一个断点:

image.png

等待程序运行,可以发现,在还没有进入执行IPing的定时任务前,已经将下线服务剔除,只剩下了一个可用服务。

查看调用链,最终可以发现使用了定时调度线程池调用了PollingServerListUpdater类的start方法,来进行更新服务操作:

image.png

回到BaseLoadBalancer的setServersList方法中:

image.png

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

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

更新列表

ping机制

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

image.png

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

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

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

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

image.png

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

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

image.png

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

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

image.png

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

1、拦截请求,通过请求中的url地址,截取服务名称 ;

2、通过LoadBalancerClient获取ILoadBalancer;

3、使用Eureka获取服务列表;

4、通过IRule负载均衡策略选择具体服务;

5、ILoadBalancer通过IPing及定时更新机制来维护服务列表;

6、重构该url地址,最终调用HttpURLConnection发起请求。

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

相关文章
|
3天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
4天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
16天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
37 3
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
53 5
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
108 5
|
1月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
1月前
|
算法 Java 程序员
Map - TreeSet & TreeMap 源码解析
Map - TreeSet & TreeMap 源码解析
33 0
|
9天前
|
负载均衡 监控 网络协议
SpringCloud之Ribbon使用
通过以上步骤,就可以在Spring Cloud项目中有效地使用Ribbon来实现服务调用的负载均衡,提高系统的可靠性和性能。在实际应用中,根据具体的业务场景和需求选择合适的负载均衡策略,并进行相应的配置和优化,以确保系统的稳定运行。
36 15
|
9天前
|
负载均衡 算法 Java
除了 Ribbon,Spring Cloud 中还有哪些负载均衡组件?
这些负载均衡组件各有特点,在不同的场景和需求下,可以根据项目的具体情况选择合适的负载均衡组件来实现高效、稳定的服务调用。
24 5
|
2月前
|
负载均衡 Java Nacos
SpringCloud基础1——远程调用、Eureka,Nacos注册中心、Ribbon负载均衡
微服务介绍、SpringCloud、服务拆分和远程调用、Eureka注册中心、Ribbon负载均衡、Nacos注册中心
SpringCloud基础1——远程调用、Eureka,Nacos注册中心、Ribbon负载均衡

推荐镜像

更多