本文将总结Dubbo早大厂面试中的问题和解答。
一、Dubbo设计模式面试问题 1.1 知道什么是SPI嘛?SPI 是 Service Provider Interface,主要用于框架中,框架定义好接口,不同的使用者有不同的需求,因此需要有不同的实现,而 SPI 就通过定义一个特定的位置,Java SPI 约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。所以就可以通过接口找到对应的文件,获取具体的实现类然后加载即可,做到了灵活的替换具体的实现类。
1.2 为什么Dubbo不用JDK的SPI而是要自己实现?答:因为 Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。
因此 Dubbo 就自己实现了一个 SPI,给每个实现类配了个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化,按需加载。
1.3 Dubbo为什么默认用 Javassist?ASM 比 Javassist 更快,但是没有快一个数量级,而Javassist 只需用字符串拼接就可以生成字节码,而 ASM 需要手工生成,成本较高,比较麻烦。
二、Dubbo服务注册发现面试问题 2.1 看过源码,服务暴露的流程?- 服务的暴露起始于 Spring IOC 容器刷新完毕之后,会根据配置参数组装成 URL, 然后根据 URL 的参数来进行本地或者远程调用。
- 会通过
proxyFactory.getInvoker
,利用 javassist 来进行动态代理,封装真的实现类,然后再通过 URL 参数选择对应的协议来进行 protocol.export,默认是 Dubbo 协议。 - 在第一次暴露的时候会调用 createServer 来创建 Server,默认是 NettyServer。
- 然后将 export 得到的 exporter 存入一个 Map 中,供之后的远程调用查找,然后会向注册中心注册提供者的信息。基本上就是这么个流程,说了这些差不多了,太细的谁都记住不。
服务的引入时机有两种,第一种是饿汉式,第二种是懒汉式。
饿汉式就是加载完毕就会引入,懒汉式是只有当这个服务被注入到其他类中时启动引入流程,默认是懒汉式。
会先根据配置参数组装成 URL ,一般而言我们都会配置的注册中心,所以会构建 RegistryDirectory 向注册中心注册消费者的信息,并且订阅提供者、配置、路由等节点。
得知提供者的信息之后会进入 Dubbo 协议的引入,会创建 Invoker ,期间会包含 NettyClient,来进行远程通信,最后通过 Cluster 来包装 Invoker,默认是 FailoverCluster,最终返回代理类。
2.3 看过源码,那说下服务调用的流程?- 调用某个接口的方法会调用之前生成的代理类,然后会从 cluster 中经过路由的过滤、负载均衡机制选择一个 invoker 发起远程调用,此时会记录此请求和请求的 ID 等待服务端的响应。
- 服务端接受请求之后会通过参数找到之前暴露存储的 map,得到相应的 exporter ,然后最终调用真正的实现类,再组装好结果返回,这个响应会带上之前请求的 ID。
- 消费者收到这个响应之后会通过 ID 去找之前记录的请求,然后找到请求之后将响应塞到对应的 Future 中,唤醒等待的线程,最后消费者得到响应,一个流程完毕。
- 关键的就是 cluster、路由、负载均衡,然后 Dubbo 默认是异步的,所以请求和响应是如何对应上的。
至于为什么要封装成 invoker 其实就是想屏蔽调用的细节,统一暴露出一个可执行体,这样调用者简单的使用它,向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
2.6 为什么要搞个本地暴露呢?因为可能存在同一个 JVM 内部引用自身服务的情况,因此暴露的本地服务在内部调用的时候可以直接消费同一个 JVM 的服务避免了网络间的通信。
- 如何确定客户端和服务端之间的通信协议?
- 如何更高效地进行网络通信?
- 服务端提供的服务如何暴露给客户端?
- 客户端如何发现这些暴露的服务?
- 如何更高效地对请求对象和响应结果进行序列化和反序列化操作?
- 需要有非常高效的网络通信,比如一般选择Netty作为网络通信框架;
- 需要有比较高效的序列化框架,比如谷歌的Protobuf序列化框架;
- 可靠的寻址方式(主要是提供服务的发现),比如可以使用Zookeeper来注册服务等等;
- 如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能;
1、动态代理:生成Client Stub(客户端存根)和Server Stub(服务端存根)的时候需要用到Java动态代理技术,可以使用JDK提供的原生的动态代理机制,也可以使用开源的:CGLib代理,Javassist字节码生成技术。
2、序列化和反序列化:在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。
- 序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。
- 反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。
3、NIO通信:出于并发性能的考虑,传统的阻塞式 IO 显然不太合适,因此我们需要异步的 IO,即 NIO。Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。可以选择Netty或者MINA来解决NIO数据传输的问题。
4、服务注册中心:可选:Redis、Zookeeper、Consul 、Etcd。一般使用ZooKeeper提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。
3.4 RPC和Http有什么区别- 性能:RPC和Http,主要差别在序列化和反序列化。RPC通过thrift二进制传输,http json序列化更消耗性能。
- 传输协议:RPC基于tcp也可以基于http,http只能是http。
- 负载均衡:RPC自带负载均衡的。http 需要自己搞。比如nginx等等
- 传输效率:可以自定义tcp协议报文相对较小。http有很多无用的东西(很多头部信息,keepalivetime reffer)
- 通知:RPC自动通知,http事先通知,自行修改nginx配置或者其他负载均衡的配置
因dubbo协议采用单一长连接,假设网络为千兆网卡(1024Mbit=128MByte),根据测试经验数据每条连接最多只能压满7MByte(不同的环境可能不一样,供参考),理论上1个服务提供者需要20个服务消费者才能压满网卡
3.6 为什么不能传大包?因dubbo协议采用单一长连接,如果每次请求的数据包大小为500KByte,假设网络为千兆网卡(1024Mbit=128MByte),每条连接最大7MByte(不同的环境可能不一样,供参考),单个服务提供者的TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。单个消费者调用单个服务提供者的TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。如果能接受,可以考虑使用,否则网络将成为瓶颈。
3.7 为什么采用异步单一长连接?因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如Morgan的提供者只有6台提供者,却有上百台消费者,每天有1.5亿次调用,如果采用常规的hessian服务,服务提供者很容易就被压跨,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步IO,复用线程池,防止C10K问题。
接口增加方法,对客户端无影响,如果该方法不是客户端需要的,客户端不需要重新部署; 输入参数和结果集中增加属性,对客户端无影响,如果客户端并不需要新属性,不用重新 部署;
输入参数和结果集属性名变化,对客户端序列化无影响,但是如果客户端不重新部署,不管输入还是输出,属性名变化的属性值是获取不到的。
总结:服务器端 和 客户端 对 领域对象 并不需要完全一致,而是按照最大匹配原则。
如果不是集成Spring,单独配置如下:dubbo.service.protocol=dubbo
3.8 dubbo 默认使用什么序列化框架,你知道的还有哪些?dubbo 有多种协议,不同的协议默认使用不同的序列化框架。比如:dubbo 协议 默认使用 Hessian2 序列化。(说明:Hessian2 是阿里在 Hessian 基础上进行的二次开发,起名为Hessian2 )。rmi协议 默认为 java 原生序列化,http 协议 默认为 为 json 。
此外补充,hessian 协议,默认是 hessian 序列化;webservice 协议,默认是 soap 文本序列化 。
3.9 什么是本地暴露和远程暴露,他们的区别下面来看本地暴露于远程暴露的区别:本地暴露是暴露在本机JVM中,调用本地服务不需要网络通信.远程暴露是将ip,端口等信息暴露给远程客户端,调用远程服务时需要网络通信.
3.10 Dubbo支持的协议主要有:dubbo:Dubbo 缺省协议是dubbo协议,采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。
RMI:RMI协议采用阻塞式(同步)短连接和 JDK 标准序列化方式。适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
hessian:Hessian底层采用Http通讯(同步),采用Servlet暴露服务。适用于传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
3.11 Dubbo相关配置配置应用信息:
配置注册中心相关信息:
配置服务协议:
配置所有暴露服务缺省值:
配置暴露服务:
配置所有引用服务缺省值:
配置引用服务:
备注:
a. 其中reference的check默认=true,启动时会检查引用的服务是否已存在,不存在时报错
b. 的配置是所有的缺省配置, 的配置会覆盖的配置。同理是所有的缺省配置。
3.12 既然有 HTTP 请求,为什么还要用 RPC 调用?服务A调用服务B的过程是应用间的内部过程,牺牲可读性提升效率、易用性是可取的。基于这种思路,RPC产生了。通常,RPC要求在调用方中放置被调用的方法的接口。调用方只要调用了这些接口,就相当于调用了被调用方的实际方法,十分易用。于是,调用方可以像调用内部接口一样调用远程的方法,而不用封装参数名和参数值等操作。
HTTP协议,以其中的Restful规范为代表,其优势很大。它可读性好,且可以得到防火墙的支持、跨语言的支持。而且,在去年的报告中,Restful大有超过RPC的趋势。但是HTTP也有其缺点,这是与其优点相对应的。首先是有用信息占比少,毕竟HTTP工作在第七层,包含了大量的HTTP头等信息。其次是效率低,还是因为第七层的缘故。还有,其可读性似乎没有必要,因为我们可以引入网关增加可读性。此外,使用HTTP协议调用远程方法比较复杂,要封装各种参数名和参数值。
3.13 RPC 与 REST两种风格的API区别,总结一下其实非常简单:
- RPC面向过程,只发送 GET 和 POST 请求。GET用来查询信息,其他情况下一律用POST。
- RESTful面向资源,使用 POST、DELETE、PUT、GET 请求,分别对应增、删、改、查操作。
在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用的本地缓存进行通讯,这只是dubbo健壮性的一种体现。
dubbo的健壮性表现:
- 监控中心宕掉不影响使用,只是丢失部分采样数据
- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
- 服务提供者无状态,任意一台宕掉后,不影响使用
- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
java序列化:用ObjectInputStream 和ObjectOutputStream进行简单的序列化和反序列化。jdk进行序列化的时候要记录对象所在的类的信息,比如元数据,数据类型等等,这样反序列化才能进行映射。
protobuf 序列化:使用protobuf前需要写.proto文件,然后用proto命令生成对应的java文件。而protobuf会根据proto文件中字段的tag顺序。
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
getSerializedSize();
if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeInt32(1, id_);
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeInt32(2, age_);
}
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeBytes(3, getUsernameBytes());
}
if (((bitField0_ & 0x00000008) == 0x00000008)) {
output.writeBytes(4, getAddressBytes());
}
getUnknownFields().writeTo(output);
}
可以看到,protobuf会安装我们定义的顺序来一个一个写入到OutPutStream中。
XML/JSon/protobuf对比
4.2 Protobuf支持多语言的序列化和反序列化的原理?各种语言使用Protobuf 时候,将在使用protobuf前需要写.proto文件,然后有各种语言生成对应的文件进行的序列化和反序列操作。

如果存在海量定时任务,并且这些任务的开始时间跨度非常长,例如,有的是 1 分钟之后执行,有的是 1 小时之后执行,有的是 1 年之后执行,那你该如何对时间轮进行扩展,处理这些定时任务呢?
博文参考设计原则 | Apache Dubbo
《Dubbo系列》-Dubbo常见面试题 - 掘金