远程调用

TIP
1 负载均衡原理
2 NacosRule
我们知道微服务间远程调用都是有OpenFeign帮我们完成的,甚至帮我们实现了服务列表之间的负载均衡。但具体负载均衡的规则是什么呢?何时做的负载均衡呢?
接下来我们一起来分析一下。
1 负载均衡原理
在SpringCloud的早期版本中,负载均衡都是有Netflix公司开源的Ribbon组件来实现的,甚至Ribbon被直接集成到了Eureka-client和Nacos-Discovery中。
但是自SpringCloud2020版本开始,已经弃用Ribbon,改用Spring自己开源的Spring Cloud LoadBalancer了,我们使用的OpenFeign的也已经与其整合。
接下来我们就通过源码分析,来看看OpenFeign底层是如何实现负载均衡功能的。
1.1 源码跟踪
要弄清楚OpenFeign的负载均衡原理,最佳的办法肯定是从FeignClient的请求流程入手。
首先,我们在com.hmall.cart.service.impl.CartServiceImpl中的queryMyCarts方法中打一个断点。然后在swagger页面请求购物车列表接口。
进入断点后,观察ItemClient这个接口:

你会发现ItemClient是一个代理对象,而代理的处理器则是SentinelInvocationHandler。这是因为我们项目中引入了Sentinel导致。
我们进入SentinelInvocationHandler类中的invoke方法看看:

可以看到这里是先获取被代理的方法的处理器MethodHandler,接着,Sentinel就会开启对簇点资源的监控:

开启Sentinel的簇点资源监控后,就可以调用处理器了,我们尝试跟入,会发现有两种实现:

这其实就是OpenFeign远程调用的处理器了。继续跟入会进入SynchronousMethodHandler这个实现类:

在上述方法中,会循环尝试调用executeAndDecode()方法,直到成功或者是重试次数达到Retryer中配置的上限。
我们继续跟入executeAndDecode()方法:

executeAndDecode()方法最终会利用client去调用execute()方法,发起远程调用。
这里的client的类型是feign.Client接口,其下有很多实现类:

由于我们项目中整合了seata,所以这里client对象的类型是SeataFeignBlockingLoadBalancerClient,内部实现如下:

这里直接调用了其父类,也就是FeignBlockingLoadBalancerClient的execute方法,来看一下:

整段代码中核心的有4步:
- 从请求的
URI中找出serviceId - 利用
loadBalancerClient,根据serviceId做负载均衡,选出一个实例ServiceInstance - 用选中的
ServiceInstance的ip和port替代serviceId,重构URI - 向真正的URI发送请求
所以负载均衡的关键就是这里的loadBalancerClient,类型是org.springframework.cloud.client.loadbalancer.LoadBalancerClient,这是Spring-Cloud-Common模块中定义的接口,只有一个实现类:

而这里的org.springframework.cloud.client.loadbalancer.BlockingLoadBalancerClient正是Spring-Cloud-LoadBalancer模块下的一个类:

我们继续跟入其BlockingLoadBalancerClient#choose()方法:

图中代码的核心逻辑如下:
- 根据serviceId找到这个服务采用的负载均衡器(
ReactiveLoadBalancer),也就是说我们可以给每个服务配不同的负载均衡算法。 - 利用负载均衡器(
ReactiveLoadBalancer)中的负载均衡算法,选出一个服务实例
ReactiveLoadBalancer是Spring-Cloud-Common组件中定义的负载均衡器接口规范,而Spring-Cloud-Loadbalancer组件给出了两个实现:

默认的实现是RoundRobinLoadBalancer,即轮询负载均衡器。负载均衡器的核心逻辑如下:

核心流程就是两步:
- 利用
ServiceInstanceListSupplier#get()方法拉取服务的实例列表,这一步是采用响应式编程 - 利用本类,也就是
RoundRobinLoadBalancer的getInstanceResponse()方法挑选一个实例,这里采用了轮询算法来挑选。
这里的ServiceInstanceListSupplier有很多实现:

其中CachingServiceInstanceListSupplier采用了装饰模式,加了服务实例列表缓存,避免每次都要去注册中心拉取服务实例列表。而其内部是基于DiscoveryClientServiceInstanceListSupplier来实现的。
在这个类的构造函数中,就会异步的基于DiscoveryClient去拉取服务的实例列表:

1.2 流程梳理
根据之前的分析,我们会发现Spring在整合OpenFeign的时候,实现了org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient类,其中定义了OpenFeign发起远程调用的核心流程。也就是四步:
- 获取请求中的
serviceId - 根据
serviceId负载均衡,找出一个可用的服务实例 - 利用服务实例的
ip和port信息重构url - 向真正的url发起请求
而具体的负载均衡则是不是由OpenFeign组件负责。而是分成了负载均衡的接口规范,以及负载均衡的具体实现两部分。
负载均衡的接口规范是定义在Spring-Cloud-Common模块中,包含下面的接口:
LoadBalancerClient:负载均衡客户端,职责是根据serviceId最终负载均衡,选出一个服务实例ReactiveLoadBalancer:负载均衡器,负责具体的负载均衡算法
OpenFeign的负载均衡是基于Spring-Cloud-Common模块中的负载均衡规则接口,并没有写死具体实现。这就意味着以后还可以拓展其它各种负载均衡的实现。
不过目前SpringCloud中只有Spring-Cloud-Loadbalancer这一种实现。
Spring-Cloud-Loadbalancer模块中,实现了Spring-Cloud-Common模块的相关接口,具体如下:
BlockingLoadBalancerClient:实现了LoadBalancerClient,会根据serviceId选出负载均衡器并调用其算法实现负载均衡。RoundRobinLoadBalancer:基于轮询算法实现了ReactiveLoadBalancerRandomLoadBalancer:基于随机算法实现了ReactiveLoadBalancer,
这样一来,整体思路就非常清楚了,流程图如下:

2 NacosRule
之前分析源码的时候我们发现负载均衡的算法是有ReactiveLoadBalancer来定义的,我们发现它的实现类有三个:

其中RoundRobinLoadBalancer和RandomLoadBalancer是由Spring-Cloud-Loadbalancer模块提供的,而NacosLoadBalancer则是由Nacos-Discorvery模块提供的。
默认采用的负载均衡策略是RoundRobinLoadBalancer,那如果我们要切换负载均衡策略该怎么办?
2.1 修改负载均衡策
查看源码会发现,Spring-Cloud-Loadbalancer模块中有一个自动配置类:

其中定义了默认的负载均衡器:

这个Bean上添加了@ConditionalOnMissingBean注解,也就是说如果我们自定义了这个类型的bean,则负载均衡的策略就会被改变。
我们在hm-cart模块中的添加一个配置类:

代码如下:
package com.hmall.cart.config;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
public class OpenFeignConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment, NacosDiscoveryProperties properties,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new NacosLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, properties);
}
}
注意:
这个配置类千万不要加@Configuration注解,也不要被SpringBootApplication扫描到。
由于这个OpenFeignConfig没有加@Configuration注解,也就没有被Spring加载,因此是不会生效的。接下来,我们要在启动类上通过注解来声明这个配置。
有两种做法:
- 全局配置:对所有服务生效
@LoadBalancerClients(defaultConfiguration = OpenFeignConfig.class)
- 局部配置:只对某个服务生效
@LoadBalancerClients({
@LoadBalancerClient(value = "item-service", configuration = OpenFeignConfig.class)
})
我们选择全局配置:

DEBUG重启后测试,会发现负载均衡器的类型确实切换成功:

2.2 集群优先
RoundRobinLoadBalancer是轮询算法,RandomLoadBalancer是随机算法,那么NacosLoadBalancer是什么负载均衡算法呢?
我们通过源码来分析一下,先看第一部分:

这部分代码的大概流程如下:
- 通过
ServiceInstanceListSupplier获取服务实例列表 - 获取
NacosDiscoveryProperties中的clusterName,也就是yml文件中的配置,代表当前服务实例所在集群信息(参考2.2小节,分级模型) - 然后利用stream的filter过滤找到被调用的服务实例中与当前服务实例
clusterName一致的。简单来说就是服务调用者与服务提供者要在一个集群
为什么?
假如我现在有两个机房,都部署有item-service和cart-service服务:

假如这些服务实例全部都注册到了同一个Nacos。现在,杭州机房的cart-service要调用item-service,会拉取到所有机房的item-service的实例。调用时会出现两种情况:
- 直接调用当前机房的
item-service - 调用其它机房的
item-service
本机房调用几乎没有网络延迟,速度比较快。而跨机房调用,如果两个机房相距很远,会存在较大的网络延迟。因此,我们应该尽可能避免跨机房调用,优先本地集群调用:

现在的情况是这样的:
cart-service所在集群是defaultitem-service的8081、8083所在集群的defaultitem-service的8084所在集群是BJ
cart-service访问item-service时,应该优先访问8081和8082,我们重启cart-service,测试一下:

可以看到原本是3个实例,经过筛选后还剩下2个实例。
查看Debug控制台:

同集群的实例还剩下两个,接下来就需要做负载均衡了,具体用的是什么算法呢?
2.3 权重配置
我们继续跟踪NacosLoadBalancer源码:

那么问题来了, 这个权重是怎么配的呢?
我们打开nacos控制台,进入item-service的服务详情页,可以看到每个实例后面都有一个编辑按钮:

点击,可以看到一个编辑表单:

我们将这里的权重修改为5:

访问10次购物车接口,可以发现大多数请求都访问到了8083这个实例。