目录
一、SPI 是什么?
二、Dubbo SPI
三、dubbo负载均衡策略
四、详解dubbo负载均衡实现原理
4.1 环境搭建
4.2 代码执行流程分析
参考文章:
阿里面试真题:Dubbo的SPI机制_三太子敖丙博客-CSDN博客
dubbo(二)dubbo spi机制_dubbo spi-CSDN博客
核心技术概念-SPI (baidu.com)
Dubbo源码解析-——SPI机制_dubbo的spi-CSDN博客
Dubbo负载均衡的源码流程(2022.5.30)_dubbo3 负载均衡调用时机源码-CSDN博客
Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。所以说 Java SPI 无法按需加载实现类。
SPI 的应用:如不同厂商数据库驱动,日志框架。
因此 Dubbo 就自己实现了一个 SPI,让我们想一下按需加载的话首先你得给个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化即可。Dubbo 就是这样设计的,配置文件里面存放的是键值对,我截一个 Cluster 的配置。
并且 Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制。
我们先来看一下 Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。
META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。
META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。
META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。
官网详细介绍:负载均衡 | Apache Dubbo
Dubbo的负载均衡 | Apache Dubbo(很老的博文介绍)
上图中最后两种是不知道那个新版本加的。简记为:
以默认的 Random 负载均衡为例。
Dubbo学习笔记(二)——dubbo-admin的搭建&使用-CSDN博客
以上面我自己的这篇文章搭建的项目为例进行测试Random负载均衡。首先启动zookeeper注册中心,然后provider端以debug的方式依次启动两个服务提供方项目(端口分别为20880、20881),服务消费方代码如下:
然后在 org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance类中的上图代码处打上断点,并以debug的方式启动服务消费方项目。
启动上面的代码后,打开浏览器输入 localhost:8081/order/userId?4 回车后,进入断点,截图如下:
说明:上面的访问路径是简写形式,因为一个参数时可以简写。其实完整的应该是 localhost:8081/order/userId?userId=4 ,其中问号前是路径,问号后是参数拼接。
为方便后续分析,还是拷贝一下栈帧中的内容:
doSelect:56, RandomLoadBalance (org.apache.dubbo.rpc.cluster.loadbalance)
select:61, AbstractLoadBalance (org.apache.dubbo.rpc.cluster.loadbalance)
doSelect:196, AbstractClusterInvoker (org.apache.dubbo.rpc.cluster.support)
select:176, AbstractClusterInvoker (org.apache.dubbo.rpc.cluster.support)
doInvoke:76, FailoverClusterInvoker (org.apache.dubbo.rpc.cluster.support)
invoke:341, AbstractClusterInvoker (org.apache.dubbo.rpc.cluster.support)
invoke:46, RouterSnapshotFilter (org.apache.dubbo.rpc.cluster.router)
invoke:327, FilterChainBuilder$CopyOfFilterChainNode (org.apache.dubbo.rpc.cluster.filter)
invoke:100, MonitorFilter (org.apache.dubbo.monitor.support)
invoke:327, FilterChainBuilder$CopyOfFilterChainNode (org.apache.dubbo.rpc.cluster.filter)
invoke:52, FutureFilter (org.apache.dubbo.rpc.protocol.dubbo.filter)
invoke:327, FilterChainBuilder$CopyOfFilterChainNode (org.apache.dubbo.rpc.cluster.filter)
invoke:40, ConsumerClassLoaderFilter (org.apache.dubbo.rpc.cluster.filter.support)
invoke:327, FilterChainBuilder$CopyOfFilterChainNode (org.apache.dubbo.rpc.cluster.filter)
invoke:120, ConsumerContextFilter (org.apache.dubbo.rpc.cluster.filter.support)
invoke:327, FilterChainBuilder$CopyOfFilterChainNode (org.apache.dubbo.rpc.cluster.filter)
invoke:194, FilterChainBuilder$CallbackRegistrationInvoker (org.apache.dubbo.rpc.cluster.filter)
invoke:92, AbstractCluster$ClusterFilterInvoker (org.apache.dubbo.rpc.cluster.support.wrapper)
invoke:103, MockClusterInvoker (org.apache.dubbo.rpc.cluster.support.wrapper)
invoke:282, MigrationInvoker (org.apache.dubbo.registry.client.migration)
invoke:56, InvocationUtil (org.apache.dubbo.rpc.proxy)
invoke:75, InvokerInvocationHandler (org.apache.dubbo.rpc.proxy)
findAllAddrById:-1, UserServiceDubboProxy0 (org.wuya)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeJoinpointUsingReflection:344, AopUtils (org.springframework.aop.support)
invoke:208, JdkDynamicAopProxy (org.springframework.aop.framework)
findAllAddrById:-1, $Proxy69 (com.sun.proxy)
selectAddrByUserId:19, OrderServiceImpl (org.wuya.service.impl)
selectAddrByUserId:25, OrderController (org.wuya.controller)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
doInvoke:205, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:150, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle:117, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:895, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:808, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:1071, DispatcherServlet (org.springframework.web.servlet)
doService:964, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:645, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:750, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:177, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:541, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:360, CoyoteAdapter (org.apache.catalina.connector)
service:399, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:891, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1784, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
在 AbstractClusterInvoker 类中的下面这个方法中,进行了负载均衡初始化操作:
/**
* Init LoadBalance.
*
* if invokers is not empty, init from the first invoke's url and invocation
* if invokes is empty, init a default LoadBalance(RandomLoadBalance)
*
*
* @param invokers invokers
* @param invocation invocation
* @return LoadBalance instance. if not need init, return null.
*/
protected LoadBalance initLoadBalance(List> invokers, Invocation invocation) {
ApplicationModel applicationModel = ScopeModelUtil.getApplicationModel(invocation.getModuleModel());
if (CollectionUtils.isNotEmpty(invokers)) {
return applicationModel.getExtensionLoader(LoadBalance.class).getExtension(
invokers.get(0).getUrl().getMethodParameter(
RpcUtils.getMethodName(invocation), LOADBALANCE_KEY, DEFAULT_LOADBALANCE
)
);
} else {
return applicationModel.getExtensionLoader(LoadBalance.class).getExtension(DEFAULT_LOADBALANCE);
}
}
然后,代码到了栈帧中 doSelect:196, AbstractClusterInvoker (最上面第三行代码)时,该方法内部又调用了 Invoker
/**
* LoadBalance. (SPI, Singleton, ThreadSafe)
*
* Load-Balancing
*
* @see org.apache.dubbo.rpc.cluster.Cluster#join(Directory)
*/
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
/**
* select one invoker in list.
*
* @param invokers invokers.
* @param url refer url
* @param invocation invocation.
* @return selected invoker.
*/
@Adaptive("loadbalance")
Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException;
}
于是,下一步又进入了栈帧中的第二行 select:61, AbstractLoadBalance ,如下图:
再下一步,代码进入了栈帧中的第一行,执行 RandomLoadBalance 随机负载均衡策略实现类中的 doSelect 方法,是真正的随机策略的实现,就是可以看到是怎么个随机法。
问:上面图片中的64行执行AbstractLoadBalance中的doSelect抽象方法时,它明明有5个实现类,为什么选择执行的是RandomLoadBalance实现类中的重写方法呢?
答:AbstractLoadBalance抽象类实现了LoadBalance接口,于是这就得看该接口中的实现,涉及到了 @SPI(RandomLoadBalance.NAME) 和 @Adaptive("loadbalance") 注解的原理。
分析:当调用LoadBalance接口中的select方法时,由于该方法被@Adaptive("loadbalance")标注,所以会去解析参数url中的参数,并匹配是否有key为“loadbalance”的,如果有,便把它对应的value作为扩展类的名字,这里找到并匹配到了random,于是便去配置文件resources\META-INF\dubbo\internal\org.apache.dubbo.rpc.cluster.LoadBalance中再去匹配,即把random当作key去找具体的实现类,找到了RandomLoadBalance。如果不显示指定负载平衡策略时,url中就不会有“loadbalance”的key,这时根据"loadbalance"就找不到负载平衡策略了,于是就会去找@SPI(RandomLoadBalance.NAME)注解指定的默认的RandomLoadBalance.NAME,它的值为random,同样地,会去配置文件中找到特定的实现类RandomLoadBalance。
配置文件内容如下:
random=org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=org.apache.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
leastactive=org.apache.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
consistenthash=org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
shortestresponse=org.apache.dubbo.rpc.cluster.loadbalance.ShortestResponseLoadBalance
adaptive=org.apache.dubbo.rpc.cluster.loadbalance.AdaptiveLoadBalance
@DubboService //暴露dubbo服务
public class OrderServiceImpl implements OrderService {
@DubboReference(loadbalance = LoadbalanceRules.RANDOM) //显示指定负载均衡策略
private UserService userService;
@Override
public List selectAddrByUserId(String userId) {
return userService.findAllAddrById(userId);
}
}
在本次显式配置了随机负载均衡策略的debug调用中,在类的如下抽象方法中,有三个参数,具体参数内容复制粘贴如下:
protected abstract Invoker doSelect(List> invokers, URL url, Invocation invocation);
(1)url参数为:
url为服务消费方携带的数据去请求provider的资源,其中methods代表要访问的服务提供方的方法。
consumer://192.168.1.36/org.wuya.UserService?application=dubbo-spring-boot-demo-consumer&background=false&check=false&dubbo=2.0.2&interface=org.wuya.UserService&loadbalance=random&methods=findAllAddrById&pid=25768&qos.enable=true®ister.ip=192.168.1.36&release=3.1.5&side=consumer&sticky=false×tamp=1703051891201&unloadClusterRelated=false
(2)invocation参数为:
RpcInvocation [methodName=findAllAddrById, parameterTypes=[class java.lang.String]]
(3)invokers参数为:
invokers是服务提供方集群的集合,下面是集合中的一个元素,另一个元素的端口为20880。
interface org.wuya.UserService -> DefaultServiceInstance{serviceName='dubbo-spring-boot-demo-provider', host='192.168.1.36', port=20881, enabled=true, healthy=true, metadata={dubbo.endpoints=[{"port":20881,"protocol":"dubbo"}], dubbo.metadata-service.url-params={"connections":"1","version":"1.0.0","dubbo":"2.0.2","release":"3.1.5","side":"provider","port":"20881","protocol":"dubbo"}, dubbo.metadata.revision=8bed92d17b68e445b8fa31d7bd5e845a, dubbo.metadata.storage-type=local, timestamp=1703051857177}}, service{name='org.wuya.UserService',group='null',version='null',protocol='dubbo',port='20881',params={side=provider, application=dubbo-spring-boot-demo-provider, release=3.1.5, methods=findAllAddrById, background=false, deprecated=false, dubbo=2.0.2, dynamic=true, interface=org.wuya.UserService, service-name-mapping=true, generic=false, anyhost=true},}
执行完实现类 RandomLoadBalance 中的 doSelect 方法后,返回一个invoker(调用程序),即消费者本次调用的服务提供者。这就是负载均衡的完整过程。
以上内容中,@Adaptive和@SPI注解的内容参考的文章:(非常通俗易懂)
dubbo之@Adaptive注解分析_dubbo @adaptive-CSDN博客
dubbo SPI @Adaptive注解使用方法与原理解析 简单易懂-CSDN博客
摘抄部分如下
dubbo提供了SPI机制可以通过外部配置文件来动态加载扩展类,有时我们可能需要配置这些扩展类挨个执行来满足业务场景,进行一些数据的处理等,此时这些扩展类我们都是需要的,还有一些其他的场景,需要根据外部环境的不同(如某参数的值),来动态的选择使用哪个扩展类,针对这种需求,dubbo提供了@Adaptive注解来完成该功能,源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD}) // 在类和方法上使用
public @interface Adaptive {
/**
设置注入哪个扩展类。目标扩展类名称通过在URL中传递的参数值确定,而URL中
参数的key是什么就是通过该方法的返回值确定的,可以有多个,默认值是接口简单名称
转点分形式,如MyInterface就是my.interface。如果是在URL上没有设置目标扩展类名称,
则会读取在@SPI注解上配置的值。
比如这里配置的值是String[] {"key1", "key2"},首先通过key1从URL上寻找目标值
作为扩展类的名字,找不到则使用key2继续寻找,没有找到则使用默认值,即@SPI注解配置
的值(没有配置则转换为点分形式作为名称),如果是也没有则会抛出java.lang.IllegalStateException
*/
String[] value() default {};
}
至此,再来欣赏官方的博客文章:Dubbo可扩展机制实战 | Apache Dubbo 便更加通俗易懂了!这篇文章一定要看完哦,尤其是最后的总结:
上面这篇文章是旧的,这个是新的哦:Dubbo SPI 概述 | Apache Dubbo 好好看看!
扩展点开发指南 | Apache Dubbo