Ribbon 原理解析
1 初始化 Ribbon 配置信息
- 在 nacos 作为注册中心的项目中,需要引入服务发现的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
其中会引入 spring-cloud-commons 依赖,而该依赖又会引入 loadbancer 的东西,而这个就是 ribbon 的核心。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
- 通过案例分析,restTemplate 需要使用 @LoadBalanced 实现负载均衡,那么首先看下 @LoadBalanced,注释中翻译下就是这个注解用来标记 restTemplate 和 webClient 的 bean作为配置使用 LoadBalancerClient, 而 LoadBalancerClient 则是一个接口,定义了执行请求的方法。
@LoadBalanced
@Bean("xxxTemplate")// 自定义 bean name,方便后续区分。
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* Annotation to mark a RestTemplate or WebClient 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 {
}
- 在 loaderbanlancer 包下,查看下所有类,发现
LoadBalancerAutoConfiguration
自动配置类,那么很显然这个类就是实现负载均衡的配置加载类了,其中该类注解上添加了 LoadBalancerClient.class 和 RestTemplate.class,表示需要这两个 bean 都存在才会执行使用该类,而 LoadBalancerClient 该接口,只有一个实现,那么就是 RibbonLoadBalancerClient,也就是负载均衡客户端,如下图。
- 然后继续分析,看到了
restTemplates
将所有的 restTemplate 进行自动注入,这里可以看出是要将每一个 restTemplate 装配功能,实现负载均衡。这里可以尝试运行 springboot 项目,看下要进行什么操作。 - 初始化 LoadBalancerInterceptor, 然后将 LoadBalancerInterceptor 拦截器配置到 restTemplate 中。
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
// 将拦截器封装到 restTemplate
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
- LoadBalancerInterceptor 作用
在拦截器中获取到 originalUri,serviceName,后续猜测就是通过 serviceName,然后向注册中心中,获取到对应的注册的服务ip,最后将最开始的 url,进行替换。测试代码如下:已经在 nacos 上注册了两个 nacos-component-provider 服务,端口号分别是 8080 和 8081,url=http://nacos-component-provider/hystrixI/1,然后一步一步进行 debug,观察 restTemplate 的执行情况。
@GetMapping(value = "/hystrixII/{id}")
public String testRestTemplate(@PathVariable Integer id){
return restTemplate.getForObject("http://nacos-component-provider/hystrixI/" + id, String.class);
}
2 restTemplate 负载均衡执行过程
- restTemplate 执行,首先设置返回的信息格式,因为还没有具体的执行。
- 添加 URI 的扩展属性,例如参数等,然后调用RestTemplate#execute(String, HttpMethod, RequestCallback, ResponseExtractor, java.lang.Object…)执行请求
- 创建 request 请求,然后执行请求方法
- 判断该请求是否已经执行过,如果是,那么就会抛出异常,否则继续执行。
- 接着执行 InterceptingClientHttpRequest#executeInternal,主要是执行拦截器中的方法,执行完后,最后才会执行最终发送请求url的方法。
- 由于这里 restTemplate 请求中的拦截器列表只有一个,那么就直接就可以走到 LoadBalancerInterceptor#intercept 的方法,这里就是发送请求的核心方法。
- 这个 loadBalancer 就是 RibbonLoadBalancerClient,执行负载均衡的功能
- 接着调用 RibbonLoadBalancerClient#execute(String, LoadBalancerRequest, Object) 方法,
getLoadBalancer(serviceId)
方法,这里使用 serviceId,这里主要是为了做一个缓存,当再次访问该负载均衡器时,不用再去创建一个。
- 调用
RibbonLoadBalancerClient#getLoadBalancer
方法,获取该服务的负载均衡器,如果这里第一次获取时会使用IOC的原理,由于 ILoadBalancer.class 类型的实例没有,那么就会去创建一个ZoneAwareLoadBalancer 对象,代码如下,ZoneAwareLoadBalancer 负载均衡器:具备区域意识、动态服务列表的负载均衡器,同时它继承 DynamicServerListLoadBalancer,也就是说会同步 nacos上注册的服务信息节点。
public ILoadBalancer getLoadBalancer(String name) {
return getInstance(name, ILoadBalancer.class);
}
// 当没有 ILoadBalancer 类型的对象时,则会去创建一个,执行下面的方法
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
// 该ZoneAwareLoadBalancer负载均衡器:具备区域意识、动态服务列表的负载均衡器
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
- 负载均衡器就是 ZoneAwareLoadBalancer(动态服务列表负载均衡器),主要负责获取从 nacos 上已经注册好的服务,或者是移除已经下线的服务。每一个服务都有对应的负载均衡器。
getServer(loadBalancer, hint)
方法,这里就可以猜想出来,对应的负载均衡器,就会将我们请求的 url 根据某种规则,轮询、随机等规则,选择一个服务进行使用,默认是采用轮询的方式去获取实例。
- 执行
PredicateBasedRule#choose
方法中的chooseRoundRobinAfterFiltering
方法:从nacos上读取到的该 serviceName 对应的所有 serverList,过滤出可用的服务,然后进行轮询。下面是轮询的规则的具体实现
// 轮询的规则的具体实现
//modulo:可用服务列表的大小
private int incrementAndGetModulo(int modulo) {
for (;;) {
// 该类维护一个自增器,然后每次访问都+1,对可用列表进行取模,实现轮询的效果
int current = nextIndex.get();
int next = (current + 1) % modulo;
if (nextIndex.compareAndSet(current, next) && current < modulo)
return current;
}
}
- 根据服务名和负载均衡器,获取到一个服务信息,包括 ip:port 等信息,用于后续替换 url 请求。
- request.apply(serviceInstance) 这是一个函数接口,具体的执行的方法是 this.requestFactory.createRequest(request, body, execution),这个方法就是原来httpRequest 进行封装为 ServiceRequestWrapper serviceRequest,然后使用该 serviceRequest 去执行请求。
- 接着又回到了
InterceptingClientHttpRequest#execute(HttpRequest, byte[])
方法,由于目前已经执行将所有的拦截器都执行完了,那么现在会执行 else 中的部分,真正的去发送请求,下面代码就是构建后新的 URI 请求。
//这个就是 serviceRequest.getURI(),将原来的http://nacos-component-provider/hystrixI/1,替换为 ip:port形式的请求
public URI getURI() {
// 执行替换的方法
URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
return uri;
}
16. 最后构造一个委托类 delegate, 执行修改后的 URI,后续的代码就不需要再 debug 了,因为都是 http 请求相关的,处理响应后的内容等。