堂妹问我:Dubbo的服务暴露过程
这周去苏州见大佬,没想到遇到一堆女粉丝,其中居然还有澡堂子堂妹,堂妹一遇到我就说敖丙哥哥我超级喜欢你写的dubbo系列,你能跟我好好讲一下他的服务暴露过程么?
我笑了笑:傻瓜,你想看怎么不早点说呢?
我今天来就带大家看看 Dubbo 服务暴露过程,这个过程在 Dubbo 中其实是很核心的过程之一,关乎到你的 Provider 如何能被 Consumer 得知并调用。
今天还是会进行源码解析,毕竟我们需要深入的去了解 Dubbo 是如何做的,只有深入它才能了解它。
不用担心源码问题,因为不仅仅有源码解析,敖丙也会通过画图和总结性的语言帮助大家理解,而且在面对面试官的时候,总结性的语言才是最重要的,因为不见得面试官也懂得或者记得具体的细节。
对了,源码是 2.6.5 版本。
URL
不过在进行服务暴露流程分析之前有必要先谈一谈 URL,有人说这 URL 和 Dubbo 啥关系?有关系,有很大的关系!
protocol://username:password@host:port/path?key=value&key=value
Dubbo 就是采用 URL 的方式来作为约定的参数类型,被称为公共契约,就是我们都通过 URL 来交互,来交流。
你想一下如果没有一个约束,没有指定一个都公共的契约那么不同的接口就会以不同的参数来传递信息,一会儿用 Map、一会儿用特定分隔的字符串,这就是导致整体很乱,并且解析不能统一。
而用了一个统一的契约之后,那么代码就更加的规范化、形成一种统一的格式,所有人对参数就一目了然,不用去揣测一些参数的格式等等。
而且用 URL 作为一个公共约束充分的利用了我们对已有概念的印象,通俗易懂并且容易扩展,我们知道 URL 要加参数只管往后面拼接就完事儿了。
因此 Dubbo 用 URL 作为配置总线,贯穿整个体系,源码中 URL 的身影无处不在。
URL 具体的参数如下:
-
protocol:指的是 dubbo 中的各种协议,如:dubbo thrift http -
username/password:用户名/密码 -
host/port:主机/端口 -
path:接口的名称 -
parameters:参数键值对
配置解析
一般常用 XML 或者注解来进行 Dubbo 的配置,我稍微说一下 XML 的,这块其实是属于 Spring 的内容,我不做过多的分析,就稍微讲一下大概的原理。
Dubbo 利用了 Spring 配置文件扩展了自定义的解析,像 dubbo.xsd 就是用来约束 XML 配置时候的标签和对应的属性用的,然后 Spring 在解析到自定义的标签的时候会查找 spring.schemas 和 spring.handlers。
spring.schemas 就是指明了约束文件的路径,而 spring.handlers 指明了利用该 handler 来解析标签,你看好的框架都是会预留扩展点的,讲白了就是去固定路径的固定文件名去找你扩展的东西,这样才能让用户灵活的使用。
我们再来看一下 DubboNamespaceHandler 都干了啥。
讲白了就是将标签对应的解析类关联起来,这样在解析到标签的时候就知道委托给对应的解析类解析,本质就是为了生成 Spring 的 BeanDefinition,然后利用 Spring 最终创建对应的对象。
服务暴露全流程
我们在深入源码之前来看下总的流程,有个大致的印象看起来比较不容易晕。
从代码的流程来看大致可以分为三个步骤(本文默认都需要暴露服务到注册中心)。
第一步是检测配置,如果有些配置空的话会默认创建,并且组装成 URL 。
第二步是暴露服务,包括暴露到本地的服务和远程的服务。
第三步是注册服务至注册中心。
从对象构建转换的角度看可以分为两个步骤。
第一步是将服务实现类转成 Invoker。
第二部是将 Invoker 通过具体的协议转换成 Exporter。
服务暴露源码分析
接下来我们进入源码分析阶段,从上面配置解析的截图标红了的地方可以看到 service 标签其实就是对应 ServiceBean,我们看下它的定义。
这里又涉及到 Spring 相关内容了,可以看到它实现了 ApplicationListener<ContextRefreshedEvent>
,这样就会在 Spring IOC 容器刷新完成后调用 onApplicationEvent
方法,而这个方法里面做的就是服务暴露,这就是服务暴露的启动点。
可以看到,如果不是延迟暴露、并且还没暴露过、并且支持暴露的话就执行 export 方法,而 export 最终会调用父类的 export 方法,我们来看看。
主要就是检查了一下配置,确认需要暴露的话就暴露服务, doExport 这个方法很长,不过都是一些检测配置的过程,虽说不可或缺不过不是我们关注的重点,我们重点关注里面的 doExportUrls 方法。
可以看到 Dubbo 支持多注册中心,并且支持多个协议,一个服务如果有多个协议那么就都需要暴露,比如同时支持 dubbo 协议和 hessian 协议,那么需要将这个服务用两种协议分别向多个注册中心(如果有多个的话)暴露注册。
loadRegistries 方法我就不做分析了,就是根据配置组装成注册中心相关的 URL ,我就给大家看下拼接成的 URL的样子。
registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&pid=7960&qos.port=22222®istry=zookeeper×tamp=1598624821286
我们接下来关注的重点在 doExportUrlsFor1Protocol 方法中,这个方法挺长的,我会截取大致的部分来展示核心的步骤。
此时构建出来的 URL 长这样,可以看到走得是 dubbo 协议。
然后就是要根据 URL 来进行服务暴露了,我们再来看下代码,这段代码我就直接截图了,因为需要断点的解释。
本地暴露
我们再来看一下 exportLocal 方法,这个方法是本地暴露,走的是 injvm 协议,可以看到它搞了个新的 URL 修改了协议。
我们来看一下这个 URL,可以看到协议已经变成了 injvm。
这里的 export 其实就涉及到上一篇文章讲的自适应扩展了。
Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
Protocol 的 export 方法是标注了 @ Adaptive 注解的,因此会生成代理类,然后代理类会根据 Invoker 里面的 URL 参数得知具体的协议,然后通过 Dubbo SPI 机制选择对应的实现类进行 export,而这个方法就会调用 InjvmProtocol#export 方法。
我们再来看看转换得到的 export 到底长什么样子。
从图中可以看到实际上就是具体实现类层层封装, invoker 其实是由 Javassist 创建的,具体创建过程 proxyFactory.getInvoker 就不做分析了,对 Javassist 有兴趣的同学自行去了解,之后可能会写一篇,至于 dubbo 为什么用 javassist 而不用 jdk 动态代理是因为 javassist 快。
为什么要封装成 invoker
至于为什么要封装成 invoker 其实就是想屏蔽调用的细节,统一暴露出一个可执行体,这样调用者简单的使用它,向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
为什么要搞个本地暴露呢
因为可能存在同一个 JVM 内部引用自身服务的情况,因此暴露的本地服务在内部调用的时候可以直接消费同一个 JVM 的服务避免了网络间的通信。
可以有些同学已经有点晕,没事我这里立马搞个图带大家过一遍。
对 exportLocal 再来一波时序图分析。
远程暴露
至此本地暴露已经好了,接下来就是远程暴露了,即下面这一部分代码
也和本地暴露一样,需要封装成 Invoker ,不过这里相对而言比较复杂一些,我们先来看下 registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()) 将 URL 拼接成什么样子。
registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo://192.168.1.17:20880/com.alibaba.dubbo.demo.DemoService....
因为很长,我就不截全了,可以看到走 registry 协议,然后参数里又有 export=dubbo://,这个走 dubbo 协议,所以我们可以得知会先通过 registry 协议找到 RegistryProtocol 进行 export,并且在此方法里面还会根据 export 字段得到值然后执行 DubboProtocol 的 export 方法。
大家要挺住,就快要完成整个流程的解析了!
现在我们把目光聚焦到 RegistryProtocol#export 方法上,我们先过一遍整体的流程,然后再进入 doLocalExport 的解析。
可以看到这一步主要是将上面的 export=dubbo://... 先转换成 exporter ,然后获取注册中心的相关配置,如果需要注册则向注册中心注册,并且在 ProviderConsumerRegTable 这个表格中记录服务提供者,其实就是往一个 ConcurrentHashMap 中将塞入 invoker,key 就是服务接口全限定名,value 是一个 set,set 里面会存包装过的 invoker 。
我们再把目光聚焦到 doLocalExport 方法内部。
这个方法没什么难度,主要就是根据URL上 Dubbo 协议暴露出 exporter,接下来就看下 DubboProtocol#export 方法。
可以看到这里的关键其实就是打开 Server ,RPC 肯定需要远程调用,这里我们用的是 NettyServer 来监听服务。
再下面我就不跟了,我总结一下 Dubbo 协议的 export 主要就是根据 URL 构建出 key(例如有分组、接口名端口等等),然后 key 和 invoker 关联,关联之后存储到 DubboProtocol 的 exporterMap 中,然后如果是服务初次暴露则会创建监听服务器,默认是 NettyServer,并且会初始化各种 Handler 比如心跳啊、编解码等等。
看起来好像流程结束了?并没有, Filter 到现在还没出现呢?有隐藏的措施,上一篇 Dubbo SPI 看的仔细的各位就知道在哪里触发的。
其实上面的 protocol 是个代理类,在内部会通过 SPI 机制找到具体的实现类。
这张图是上一篇文章的,可以看到 export 具体的实现。
复习下上一篇的要点,通过 Dubbo SPI 扫包会把 wrapper 结尾的类缓存起来,然后当加载具体实现类的时候会包装实现类,来实现 Dubbo 的 AOP,我们看到 DubboProtocol 有什么包装类。
可以看到有两个,分别是 ProtocolFilterWrapper 和 ProtocolListenerWrapper
对于所有的 Protocol 实现类来说就是这么个调用链。
而在 ProtocolFilterWrapper 的 export 里面就会把 invoker 组装上各种 Filter。
看看有 8 个在。
我们再来看下 zookeeper 里面现在是怎么样的,关注 dubbo 目录。
两个 service 占用了两个目录,分别有 configurators 和 providers 文件夹,文件夹里面记录的就是 URL 的那一串,值是服务提供者 ip。
至此服务流程暴露差不多完结了,可以看到还是有点内容在里面的,并且还需要掌握 Dubbo SPI,不然有些点例如自适应什么的还是很难理解的。最后我再来一张完整的流程图带大家再过一遍,具体还是有很多细节,不过不是主干我就不做分析了,不然文章就有点散。
然后再引用一下官网的时序图。
总结
还是建议大家自己打断点过一遍,这样能够更加的清晰,到时候面试官问起来一点都不虚,不过只要你认真看了这篇文章也差不多了,总的流程能说出来能证明你看过源码,一些细节记不住的,你想想看你自己写的代码过一两个月你记得住不?更别说别人写的了。
其实我可以不源码分析,我可以直接口述 + 画图,观赏性更佳,但是为什么我还是贴代码呢?
想带着大家从源码级别来过一遍流程,这样能让大家更有底气,毕竟你看图理解了是一回事,真正的看到源码,就会很直观的知道一些点,例如,缓存原来就是放一个 map 中,这过滤链原来是这样拼接的等等等等。
总的而言服务暴露的过程起始于 Spring IOC 容器刷新完成之时,具体的流程就是根据配置得到 URL,再利用 Dubbo SPI 机制根据 URL 的参数选择对应的实现类,实现扩展。
通过 javassist 动态封装 ref (你写的服务实现类),统一暴露出 Invoker 使得调用方便,屏蔽底层实现细节,然后封装成 exporter 存储起来,等待消费者的调用,并且会将 URL 注册到注册中心,使得消费者可以获取服务提供者的信息。
今天这个就差不多了,Dubbo 系列估计还有几篇,到时候再来个面试汇总,等着吧!
点个赞呗