😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本专栏《八股消消乐》旨在记录个人所背的八股文,包括Java/Go开发、Vue开发、系统架构、大模型开发、机器学习、深度学习、力扣算法
等相关知识点,期待与你一同探索、学习、进步,一起卷起来叭!
目录
- 题目
- 答案
- 问题1
- 问题2
- 问题3
- 问题4
- 问题5
- 问题6
- 问题7
- 问题8
- 问题9
- 问题10
- 问题11
- 问题12
- 问题13
- 问题14
- 问题15
- 问题16
- 问题17
- 问题18
- 问题19
- 问题20
- 问题21
- 问题22
- 问题23
- 问题24
- 问题25
- 复盘
题目
💬技术栈:Dubbo
🔍简历内容:熟悉Dubbo使用和基本原理,具备一定的项目实战经验。
🚩面试问:
(1)Dubbo 的主要节点角色有哪些?分别是干什么用的?
(2)Dubbo3.x 提供方注册有哪几种方式,怎么设置?消费方订阅又有哪几种方式,又怎么设置?
(3)Dubbo有哪些容错策略以及作用是什么?
(4)Dubbo 通过 RpcContext 开启了异步之后,是怎么衔接父子线程的上下文信息的?
(5)泛化调用编写代码的关键步骤是怎样的?
(6)点点直连有该怎么设置?
(7)Dubbo 的事件通知怎么设置?
(8)Dubbo 的参数验证是怎么设置的?
(9)Dubbo 怎么设置缓存?缓存有哪些类似可以设置?
(10)配置的加载顺序是怎样的?
(11)Dubbo 默认使用的是什么通信框架?
(12)Dubbo 的 <dubbo:application>
、<dubbo:reference>
等标签,是怎么被加载到 Spring 中的?
(13)Dubbo 源码分层模块是怎样的?
(14)Dubbo 如何扫描含有 @DubboService 这种注解的类?
(15)Dubbo SPI 解决了 JDK SPI 的什么问题?
(16)简要描述下 Dubbo SPI 与 Spring SPI 的加载原理?
(17)LinkedHashMap 可以设计成 LRU 么?
(18)利用 Dubbo 框架怎么来做分布式限流呢?
(19)Wrapper 是怎么降低调用开销的?
(20)使用 Javassist 编译的有哪些关键要素环节?
(21)使用 ASM 编译有哪些基本步骤?
(22)Dubbo 是怎么完成实例注入与切面拦截的?
(23)服务发布的流程是怎样的?
(24)服务订阅的流程是怎样的?
(25)你有研究过 Dubbo 的协议帧格式么?
💡建议暂停思考10s,你有答案了嘛?如果你有不同题解,欢迎评论区留言、打卡。
答案
问题1
Dubbo 的主要节点角色有五个。
- Container:服务运行容器,为服务的稳定运行提供运行环境。
- Provider:提供方,暴露接口提供服务。
- Consumer:消费方,调用已暴露的接口。
- Registry:注册中心,管理注册的服务与接口。
- Monitor:监控中心,统计服务调用次数和调用时间。
问题2
一般老项目升级至 Dubbo 新版本会用到这个。
(1)提供方有注册模式,一共三种方式,通过设置 dubbo.application.register-mode 属性来控制不同的注册模式:
- interface:只接口级注册。
- instance:只应用级注册。
- all:接口级注册、应用级注册都会存在,同时也是默认值。
(2)消费方有订阅模式,也有三种模式,通过设置 dubbo.application.service-discovery.migration 属性来兼容新老订阅方案:
- FORCE_INTERFACE:只订阅消费接口级信息。
- FORCE_APPLICATION:只订阅应用级信息。
- APPLICATION_FIRST:注册中心有应用级注册信息则订阅应用级信息,否则订阅接口级信息,起到了智能决策来兼容过渡方案。
问题3
问题4
RpcContext 通过调用 startAsync 方法开启异步模式
之后,然后在另外的线程中采用 asyncContext.signalContextSwitch 方法来同步父线程的上下文信息
,本质还是进行了ThreadLocal 传递。
因为 asyncContext 富含上下文信息,只需要把这个所谓的 asyncContext 对象传入到子线程中
,然后将 asyncContext 中的上下文信息充分拷贝到子线程的 ThreadLocal 中
,这样,子线程处理所需要的任何信息就不会因为开启了异步化处理而缺失。
问题5
泛化调用:在调用方没有服务方提供的API(SDK)的情况下,对服务方进行调用,并且可以拿到调用结果。
泛化调用三部曲:
(1)明确 4 个维度的参数,分别是:接口类名、接口方法名、接口方法参数类名、业务请求参数
。
(2)根据接口类名创建 ReferenceConfig 对象,设置 generic = true 属性,调用referenceConfig.get 拿到 genericService 泛化对象。
(3)将接口方法名、接口方法参数类名、业务请求参数,传入genericService.$invoke 方法中
,即可拿到响应对象。
示例代码:
问题6
场景:因为测试环境的快速联调,或者产线问题的快速修复,都会需要这种特殊的请求连接方式。一般通过设置 url 属性来进行点点直连,方式主要四种。
(1)设置在 <dubbo:reference>
标签中:
(2)设置在 @DubboReference 注解中:
(3)通过 -D 参数设置在 JVM 启动命名中:
(4)设置在外部配置文件中,比如设置在外部的 dubbo.properties 配置文件中:
问题7
场景:新增的技术属性如何通过事件通知的方式进行解耦。
通过在@DubboReference 注解或 <dubbo:reference/>
标签中设置属性,来实现事件通知机制。
三部曲:
(1)创建一个服务类,在该类中添加 onInvoke、onReturn、onThrow 三个方法。
(2)在三个方法中按照源码 FutureFilter 的规则定义好方法入参。
(3)@DubboReference 注解中或者 <dubbo:reference/>
标签中给需要关注事件的Dubbo接口添加配置即可。
示例代码:
事件通知的底层实现原理,是借助于 FutureFilter
过滤器来实现的:
(1)在 invoker.invoke(invocation) 方法之前,利用 fireInvokeCallback 方法反射调用了接口配置中指定服务中的 onInvoke
方法。
(2)在 onResponse 响应时,处理了正常返回和异常返回的逻辑,分别调用了接口配置中指定服务中的 onReturn、onThrow
方法。
(3)在 onError 框架异常后,调用了接口配置中指定服务中的 onThrow 方法。
问题8
Dubbo 源码中使用参数校验有两种方式。
(1)设置 validation 为 jvalidation、jvalidationNew 两种框架提供的值。
(2)设置 validation 为自定义校验器的类路径,并将自定义的类路径添加到 META-INF 文件夹下面的 org.apache.dubbo.validation.Validation 文件中。
示例代码:
问题9
场景:面对接口高频大量调用时,如何针对接口优雅添加缓存特性。
缓存设置: <dubbo:service/>
、<dubbo:method/>
、<dubbo:provider/>
、<dubbo:consumer/>
、@DubboReference
、@DubboService
等。
缓存的类型:lru、threadlocal、jcache、expiring。
- lru,使用的是 LruCacheFactory 工厂类,类注释上有提到使用 LruCache 缓存类来进行处理,实则背后使用的是
JVM 内存
。 - threadlocal,使用的是 ThreadLocalCacheFactory 工厂类,类名中 ThreadLocal 是本地线程的意思,而
ThreadLocal 最终还是使用的是 JVM 内存
。 - jcache,使用的是 JCacheFactory 工厂类,是提供 javax-spi 缓存实例的工厂类,既然是一种 spi 机制,可以接入很多自制的开源框架。
- expiring,使用的是 ExpiringCacheFactory 工厂类,内部的 ExpiringCache 中还是
使用的 Map 数据结构来存储数据
,仍然使用的是 JVM 内存
。
问题10
- System Properties,最高优先级,我们一般会在启动命令中通过 JVM 的 -D 参数进行指定,图中通过 -D 参数从指定的磁盘路径加载配置,也可以从公共的 NAS 路径加载配置。
- Externalized Configuration,优先级次之,外部化配置,我们可以直接从统一的配置中心加载配置,图中就是从 Nacos 配置中心加载配置。
- API / XML / 注解,优先级再次降低,这三种应该是我们开发人员最熟悉不过的配置方式了。
- Local File,优先级最低,一般是项目中默认的一份基础配置,当什么都不配置的时候会读取。
问题11
默认使用 Netty
作为 Dubbo 的网络通信框架。同时,Netty 也位于 Dubbo 十层模块中的第 9 层,Transport 层
。
问题12
(1)Spring 在启动的时候,不但会读取 Spring 默认的一些 schema
,还会读取第三方(比如 Dubbo)自定义的 schema
。
(2)Spring 的底层会回调 NamespaceHandler 接口的所有实现类
,调用每个实现类的 parse 方法
,然而 DubboNamespaceHandler 也就是在这个 parse 方法中完成了配置的解析,并转为 Spring 的 bean 对象
。
问题13
主要分为三大块:
(1)和 Business 紧密相关的 Service 层;
- Service,与业务逻辑关联紧密的一层称为服务层。
(2)和 RPC 紧密相关的 Config、Proxy、Registry、Cluster、Monitor 和 Protocol;
- Config,专门存储与读取配置打交道的层次称为配置层。
- Proxy,代理接口发起远程调用,或代理接收请求进行实例分发处理的层次,称为服务代理层。
- Registry,与注册中心打交道的层次,称为注册中心层。
- Cluster,封装多个提供者并承担路由过滤和负载均衡的层次,称为路由层。
- Monitor,同步调用结果的层次称为监控层。
- Protocol,封装调用过程的层次称为远程调用层。
(3)和Remoting 紧密相关的 Exchange、Transport、Serialize。
- Exchange,封装请求并根据同步异步模式获取响应结果的层次,称为信息交换层。
- Transport,将数据通过网络发送至对端服务的层次称为网络传输层。
- Serialize,把对象与二进制进行相互转换的正反序列化的层次称为数据序列化层。
问题14
场景:根据业务功能抽象插件,或者在系统中通过无侵入性进行技术改造。
(1)Dubbo 利用了一个 DubboClassPathBeanDefinitionScanner 类继承了 ClassPathBeanDefinitionScanner,充分利用 Spring 自身已有的扩展特性来扫描自己需要关注的三个注解类,org.apache.dubbo.config.annotation.DubboService、org.apache.dubbo.config.annotation.Service、com.alibaba.dubbo.config.annotation.Service,然后完成 BeanDefinition 对象的创建。
(2)在 BeanDefinition 对象的实例化完成后,在容器触发刷新的事件过程中,通过回调了 ServiceConfig 的 export 方法完成了服务导出,即完成 Proxy 代理对象的创建,最后在运行时就可以直接被拿来使用了。
问题15
(1)JDK SPI 使用一次,就会一次性实例化所有实现类。
为了弥 JDK SPI 的不足,Dubbo 定义出了自己的一套 SPI 机制逻辑,既要通过 O(1) 的时间复杂度来获取指定的实例对象
,还要控制缓存创建出来的对象,做到按需加载获取指定实现类
。
Dubbo SPI 在实现的过程中,采用了两种方式来优化。
- 方式一,增加缓存,来降低磁盘IO访问以及减少对象的生成。
- 方式二,使用Map的hash查找,来提升检索指定实现类的性能。
通过两种方式的优化后,在面对大量高频调用时,JDK SPI 可能会出现磁盘 IO 吞吐下降、大量对象产生和查询指定实现类的 O(n) 复杂度等问题,而 Dubbo SPI 采用缓存+Map的组合方式
更加友好地避免了这些情况,即使大量调用,也问题不大。
问题16
Dubbo SPI 加载了三个资源路径下的文件内容:
- META-INF/dubbo/internal/,存放的是 Dubbo 内置的一些扩展点
- META-INF/dubbo/,存放的是上层业务系统自身的一些定制 Dubbo 的相关扩展点。
- META-INF/services/,存放的是 Dubbo 自身的一些业务逻辑所需要的一些扩展点
Spring 中的 SPI :通过 org.springframework.core.io.support.SpringFactoriesLoader#loadFactories
方法读取所有 jar 包的“META-INF/spring.factories
”资源文件,并从文件中读取一堆的类似 EnableAutoConfiguration 标识的类路径
,把这些类创建对应的 Spring Bean 对象注入到容器中,就完成了 SpringBoot 的自动装配。
问题17
(1)通过继承 Map 并重写 removeEldestEntry 方法来灵活扩展为 LRU。
(2)Dubbo 框架中也进行了类似扩展,LRU2Cache 缓存类。通过继承LinkedHashMap 的类,然后重写了父类 LinkedHashMap 中的 removeEldestEntry 方法,当 LRU2Cache 存储的数据个数大于设置的容量后,会删除最先存储的数据,让最新的数据能够保存进来。
问题18
使用过滤器进行限流改造的方法流程:
(1)寻找请求流经的必经之路,并在必经之路上找到可扩展的接口
。
(2)找到该接口的众多实现类,研究在触发调用的入口可以拿到哪些数据
,再研究关于方法的入参数据、方法本身信息以及方法归属类的信息
可以通过哪些 API 拿到。
(3)根据限流的核心计算模块,逐渐横向扩展
,从单个方法到多个方法,从单个服务到多个服务,从单个节点到集群节点,尽可能周全地考虑通用处理方式,同时站在使用者的角度,做到简单易用的效果。
问题19
Wrapper 降低开销的主要有 2 个关键要素的原因:
(1)生成了代理类缓存起来,避免频繁创建对象。
(2)代理类中的逻辑,是通过 if…else 的普通代码进行了强转操作,转为原始对象后继续调用方法,而不是采用反射方式来调用方法的。
源码:
Wrapper 调用 getWrapper 方法来生成一个代理类。
- 以源对象的类属性为维度,与生成的代理类建立缓存映射关系,避免频繁创建代理类影响性能。
- 生成了一个继承 Wrapper 的动态类,并且暴露了一个公有 invokeMethod 方法来调用源对象的方法。
- 在invokeMethod 方法中,通过生成的 if…else 逻辑代码来识别调用源对象的不同方法。
问题20
利用 Javassist 生成代理对象流程:
(1)设计一个代码模板。
(2)使用 Javassist 的相关 API,通过 ClassPool.makeClass 得到一个操控类的 CtClass 对象,然后针对 CtClass 进行 addField 添加字段、addMethod 添加方法、addConstructor 添加构造方法等等。
(3)调用 CtClass.toClass 方法并编译得到一个类信息,有了类信息,就可以实例化对象处理业务逻辑了。
问题21
利用 ASM 来生成代理对象流程:
(1)设计一个代码模板。
(2)通过 IDEA 的协助得到代码模板的字节码指令内容。
(3)使用 Asm 的相关 API 依次将字节码指令翻译为 Asm 对应的语法,比如创建 ClassWriter 相当于创建了一个类,继续调用 ClassWriter.visitMethod 方法相当于创建了一个方法等等,对于生僻的字节码指令实在找不到对应的官方文档的话,可以通过“MethodVisitor + 字节码指令”来快速查找对应的 Asm API。
(4)调用 ClassWriter.toByteArray 得到字节码的字节数组,传递到 ClassLoader.defineClass 交给 JVM 虚拟机得出一个 Class 类信息。
问题22
创建扩展点对象的时候,不但会通过 setter 方法进行实例注入,而且还会通过包装类层层包裹:
Dubbo 完成实例注入,主要是当 ExtensionLoader 的 getExtension 方法被调用
时,才会酌情考虑是取缓存对
象,还是直接创建对象并进行实例注入
。
直接创建对象:
(1)通过反射创建出来一个婴儿对象
(2)经历前置初始化前置处理(postProcessBeforeInitialization)、注入扩展点(injectExtension)【该方法中会从根据对象的 setter 方法获取扩展点名称,然后直接从容器中找到对应的实例,完成实例注入。
】、初始化后置处理(postProcessAfterInitialization)三个阶段,经过三段处理的对象,我们暂且称为“原始对象”。
(3)这个原始对象,会根据 getExtension 传入的 wrap 变量,决定是否需要将原始对象再次进行包裹处理。
(4)若需要包裹,会将该 SPI 接口的所有包装类排序好,以套娃的形式,将原始对象层层包裹。而包装类上可以设置 @Wrapper 注解,结合注解,有 3 种情况决定是否需要包装。
- 无 @Wrapper 注解,则需要包装。
- 有 @Wrapper 注解,但是注解中的 matches 字段值为空,则需要包装。
- 有 @Wrapper 注解,但是注解中的 matches 字段值包含入参的扩展点名称,并且 mismatches 字段值不包含入参的扩展点名称,则需要包装。
问题23
服务发布的流程,主要可以从配置、导出、注册
三方面描述。
- 配置流程:通过扫描指定包路径下含有
@DubboService 注解的 Bean 定义
,把扫描出来的 Bean 定义属性,全部转移至新创建的 ServiceBean 类型的 Bean 定义中,为后续导出做准备。 - 导出流程:主要有两块,一块是
injvm 协议的本地导出
,一块是暴露协议的远程导出
,远程导出与本地导出有着实质性的区别,远程导出会使用协议端口,通过 Netty 绑定来提供端口服务。 - 注册流程:其实是远程导出的一个分支流程,会将提供方的服务接口信息,
通过 Curator 客户端,写到 Zookeeper 注册中心服务端去
。
问题24
(1)在程序上,往往会通过 @DubboReference 注解
来标识需要订阅哪些接口的服务,并使用这些服务进行调用。在源码跟踪上,可以通过该注解一路探索出背后的核心类 ReferenceConfig
。
(2)在ReferenceConfig 的 get 方法
会先后进行本地引用与远程引用的两大主干流程。
(3)在本地引用环节中使用的 invoker 对象是从 InjvmProtocol 中 exporterMap 获取到的
。而在远程引用环节中,创建 invoker 的核心逻辑是在 RegistryProtocol 的 doCreateInvoker
方法中完成的。
(4)在这段 doCreateInvoker 逻辑中,还进行了消费者注册和接口订阅逻辑,订阅逻辑的本质就是启动环节从注册中心拉取一遍接口的所有提供方信息,然后为这些接口添加监听操作
,以便在后续的环节中,提供方有任何变化,消费方这边也能通过监听策略,及时感知到提供方节点的变化。
问题25
Dubbo 的协议帧本质:“定长 + 变长”的整个报文格式。
- magic high:魔术高位,占用 8 bit,也就是 1 byte。该值固定为 0xda,是一种标识符。
- magic low:魔术低位,占用 8 bit,也就是 1 byte。该值固定为 0xbb,也是一种标识符。
魔术低位和魔术高位合并起来就是
0xdabb
,代表着 dubbo 数据协议报文的开始。如果从通信 socket 收到的报文不是以 0xdabb 开始的,可以认为是非法报文。
- request flag and serialization id:请求类型和序列化方式,占用 8 bit,也就是 1 byte。前面 4 bit 是请求类型,后面 4 bit 是序列化方式,合起来用 1 个 byte 来表示。
- response status:响应码,占用 8 bit,也是 1 byte。因为已经明确是响应码了,所以一个请求发送出去的时候,不用填充这个值,响应回来的时候,这里就有值了。但是这个响应码,并不是那些真实业务数据功能的响应码,而是 Dubbo 通信层面的错误码,比如通信响应成功码、消费方超时码、服务方超时码、请求格式错误码等等,都是一些 Dubbo 框架自己易于通信识别错误的码,并非那些真正上层业务功能的错误码。
- request id:请求唯一ID,占用 64 bit,也就是 8 byte。标识请求的唯一性,用来证明你收到的响应,就是你曾经发出去的请求返回来的数据。
- body length:报文体长度,占用 32 bit,也就是 4 byte。体现真正的业务报文数据到底有多长。因为真实的业务数据有大有小,如果报文里不告知业务数据的长度,服务方就不知道要读取多长的字节,所以,就需要知道业务报文数据到底有多长。当客户端发送数据时,把要发送的业务数据报文计算一下长度后,放到这个位置,服务方看到该长度后,就会读取指定长度的字节,读完就结束,也就收到了一个完整的报文数据。
- body content:报文体数据,占用的 bit 未知,占用的 byte 字节个数也未知。这里是我们真正业务数据的内容,至于真正的业务数据的长度有多长,完全由报文体长度决定。
复盘
🔨复盘:START 法则
- “S”—— situation,背景或环境。
- “T”—— task,制定任务。
- “A”—— action,实施步骤。
- “R”—— result,结果反响。
- “T”—— think,思考改进。
答题模板:
遇到什么样的背景问题,在分析问题的过程中,你给自己制定了怎样的任务目标或者期望,然后采取了怎样的实施步骤,得到了什么样的结果,效果如何,最后针对已知的结果,思考未来可以怎样改进,来得到更好的效果。