深入探讨Dubbo面试题与知识点的全面总结:2025年最新版,助你轻松应对面试挑战
大家好,今天我将为大家提供一份关于Dubbo知识点与面试题的详细总结指南!
RPC 基础知识
RPC的定义
RPC(Remote Procedure Call),即远程过程调用,顾名思义,RPC的核心关注点在于实现远程调用,而非本地调用。
RPC的必要性:因为不同服务器上的服务在各自的内存空间中,必须通过网络编程来传递方法调用所需参数,同时获取调用结果也需要网络传输。如果手动实现网络编程来完成这套流程,工作量会非常庞大,我们需要考虑底层的传输方式(TCP或UDP)、序列化方式等多个方面。
RPC的优势:简而言之,RPC使得调用远程计算机的服务方法变得如同调用本地方法一样简单,并且用户无需关注底层网络编程的具体细节。
例如,服务A和B分别部署在两台不同的机器上,若服务A需要调用服务B的某个方法,可以通过RPC轻松实现。
简而言之:RPC的设计初衷是让远程方法调用变得像本地方法调用一样简单。
RPC的工作原理
为了帮助大家理解RPC的工作原理,可以将整个RPC过程视为以下六个核心部分的实现:
- 客户端(服务消费端):负责发起远程方法调用。
- 客户端Stub(桩):作为代理类,将调用方法、类及方法参数等信息传递给服务端。
- 网络传输:将调用方法的信息(如参数)传递到服务端,服务端执行完毕后,将返回结果通过网络发送回客户端。网络传输的实现方式多种多样,例如基本的Socket或更高效的Netty。
- 服务端Stub(桩):接收客户端请求并将其反序列化为Java对象,随后根据请求信息调用相应的方法并返回结果。
- 服务端(服务提供端):负责提供远程方法的实现。
具体的原理图如下所示,后续内容将为大家详细阐述RPC的完整过程。
RPC原理图
- 客户端以本地调用的方式请求远程服务;
- 客户端Stub接收到调用后,将方法及参数组装成可以进行网络传输的消息体(序列化为
RpcRequest
); - 客户端Stub找到远程服务的地址,并将消息发送到服务提供端;
- 服务端Stub接收到消息后将其反序列化为Java对象
RpcRequest
; - 服务端Stub根据
RpcRequest
中的信息调用相应的方法; - 服务端Stub获取方法执行结果后,组装成网络传输的消息体(序列化为
RpcResponse
)发送回客户端; - 客户端Stub接收到消息并将其反序列化为Java对象
RpcResponse
,最终获得结果。
相信大家在阅读完以上内容后,对RPC的原理有了基本的了解。
虽然内容不多,但基本涵盖了RPC框架核心原理!后续章节中我将进一步介绍相关技术细节。
希望大家不仅能够理解RPC的原理,还能自己描绘出过程,并能够向他人讲解。在面试时,面试官通常会询问RPC相关内容。
Dubbo基础知识
Dubbo是什么?
Apache Dubbo (incubating) 是一款高性能、轻量级的开源Java RPC框架。
根据Dubbo官方文档介绍,Dubbo提供了六大核心能力:
- 面向接口代理的高性能RPC调用。
- 智能容错及负载均衡。
- 服务的自动注册和发现。
- 高度的可扩展性。
- 运行时流量调度。
- 可视化的服务治理与运维。
Dubbo提供的六大核心能力
简而言之,Dubbo不仅能帮助我们调用远程服务,还提供了智能负载均衡等开箱即用的其他功能。
目前,Dubbo已经获得了接近34.4k的Star。
在 2020年度OSC中国开源项目评选活动中,Dubbo位列开发框架和基础组件类项目第七名。与几年前相比,其热度和排名有所下降。
Dubbo最初由阿里开源,后来加入Apache。正因为Dubbo的出现,越来越多的公司开始接受和使用分布式架构。
为什么选择Dubbo?
随着互联网的发展,网站规模的扩大和用户数量的增加,单一应用架构和垂直应用架构已经无法满足需求,分布式服务架构因此应运而生。
在分布式服务架构下,系统被拆分成不同的服务模块,例如短信服务和安全服务,每个服务独立提供系统的某个核心功能。
我们可以使用Java RMI(Java远程方法调用)、Hessian等框架简单暴露和引用远程服务。然而,随着服务数量的增加,服务调用关系愈发复杂,尤其是在高并发情况下,对负载均衡和服务监控的需求愈加迫切。虽然可以利用F5等硬件进行负载均衡,但这样会增加成本,并存在单点故障的风险。
Dubbo的出现有效解决了上述问题。Dubbo为我们解决了哪些问题呢?
- 负载均衡:当同一服务在不同机器上部署时,如何选择调用哪台机器上的服务。
- 服务调用链路生成:随着系统的发展,服务数量不断增加,服务间的依赖关系愈加复杂,甚至无法明确某个应用需在另一个应用之前启动。Dubbo可以帮助我们理解服务之间的调用关系。
- 服务访问压力及时长统计、资源调度和治理:根据访问压力实时管理集群容量,提高集群的利用率。
- ......
此外,除了在分布式系统中应用,Dubbo还可用于当前较为流行的微服务系统。不过,由于Spring Cloud在微服务中应用更为广泛,因此通常提到Dubbo的场景多是在分布式系统中。
接下来,我们将进一步探讨分布式的概念及其必要性。
分布式基础知识
分布式的定义
分布式系统,或称为SOA,核心在于面向服务。简单来说,分布式系统是将整个系统拆分为不同的服务,并将这些服务部署在不同的服务器上,以减轻单体服务的压力,提高并发量和性能。例如,一个电商系统可以拆分为订单系统、商品系统、登录系统等,拆分后的每个服务可以独立部署在多台服务器上,以应对高流量访问。
分布式事务示意图
为什么选择分布式?
从开发角度看,单体应用的代码集中在一起,而分布式系统的代码则根据业务拆分。这样,各个团队可以独立负责一个服务的开发,从而提升开发效率。此外,业务拆分后的代码更易于维护和扩展。
我认为,将系统拆分为分布式系统不仅便利扩展和维护,同时也能显著提高系统性能。想象一下,整个系统被拆分为不同的服务/系统,并分别部署在各自的服务器上,这样会大幅提升系统性能。
Dubbo架构
Dubbo架构中的核心角色
官方文档中关于框架设计的章节已经进行了详细介绍,这里我再强调一些关键点。
Dubbo关系图
上述节点及其关系简要介绍:
- Container: 服务运行容器,负责加载和运行服务提供者,必不可少。
- Provider: 提供服务的服务方,需向注册中心注册自己提供的服务,必不可少。
- Consumer: 调用远程服务的服务方,会向注册中心订阅所需服务,必不可少。
- Registry: 服务注册与发现的中心。注册中心会返回服务提供者地址列表给消费者,非必不可少。
- Monitor: 负责统计服务调用次数和调用时长的监控中心。服务消费方和提供方会定期向监控中心发送统计数据,非必不可少。
你了解Dubbo中的Invoker概念吗?
Invoker
在Dubbo的领域模型中是一个非常重要的概念。如果你阅读过Dubbo源码,应该会频繁遇到这个术语。举例来说,在负载均衡的源码中,Invoker
的身影随处可见。
简单来说,Invoker
是Dubbo对远程调用的抽象。
dubbo_rpc_invoke.jpg
根据Dubbo官方的解释,Invoker
可分为:
- 服务提供
Invoker
- 服务消费
Invoker
当我们需要调用远程方法时,需要通过动态代理来隐藏远程调用的细节,这部分细节是通过相应的Invoker
实现来完成的。
了解Dubbo的工作原理吗?
下图展示了Dubbo的整体设计,分为十层,各层之间均为单向依赖。
左侧的淡蓝色背景表示服务消费方使用的接口,右侧的淡绿色背景表示服务提供方使用的接口,中间的轴线则是双向使用的接口。
dubbo-framework
- config配置层:存放Dubbo相关配置,支持代码配置、也支持基于Spring的配置,以
ServiceConfig
和ReferenceConfig
为中心。 - proxy服务代理层:使得远程方法调用如本地调用般简单的关键层,依赖代理类,以
ServiceProxy
为核心。 - registry注册中心层:封装服务地址的注册与发现。
- cluster路由层:处理多个服务提供者的路由及负载均衡,并桥接注册中心,以
Invoker
为中心。 - monitor监控层:对RPC调用次数和时长进行监控,以
Statistics
为中心。 - protocol远程调用层:封装RPC调用,以
Invocation
和Result
为中心。 - exchange信息交换层:封装请求响应模式,支持同步与异步,以
Request
和Response
为中心。 - transport网络传输层:将mina和netty统一接入,以
Message
为中心。 - serialize数据序列化层:对需要在网络上传输的数据进行序列化。
了解Dubbo的SPI机制吗?如何扩展Dubbo中的默认实现?
SPI(Service Provider Interface)机制在众多开源项目中得到了广泛应用,它能帮助我们动态查找服务或功能的实现(如负载均衡策略)。
SPI的基本原理是将接口的实现类放在配置文件中,程序在运行时读取配置文件,通过反射加载实现类。这样,可以在运行时动态替换接口实现,类似于IoC(控制反转)的解耦思路。
Java本身提供了SPI机制的实现,但Dubbo没有直接使用,而是对Java原生SPI机制进行了增强,以更好地满足自身需求。
那么,如何扩展Dubbo中的默认实现呢?
以实现自定义负载均衡策略为例,我们可以创建一个实现类XxxLoadBalance
,并实现LoadBalance
接口或继承AbstractLoadBalance
类。
package com.xxx;
import org.apache.dubbo.rpc.cluster.LoadBalance;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.RpcException;
public class XxxLoadBalance implements LoadBalance {
public <T> Invoker<T> select(List<Invoker<T>> invokers, Invocation invocation) throws RpcException {
// 具体实现...
}
}
将这个实现类的路径写入resources
目录下的META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance
文件中即可。
src
|-main
|-java
|-com
|-xxx
|-XxxLoadBalance.java (实现LoadBalance接口)
|-resources
|-META-INF
|-dubbo
|-org.apache.dubbo.rpc.cluster.LoadBalance (纯文本文件,内容为:xxx=com.xxx.XxxLoadBalance)
org.apache.dubbo.rpc.cluster.LoadBalance
xxx=com.xxx.XxxLoadBalance
还有许多其他可供扩展的选项,具体可参考官方文档中的SPI扩展实现部分。
你了解Dubbo的微内核架构吗?
Dubbo采用微内核(Microkernel)+插件(Plugin)模式,简单来说就是微内核架构。微内核负责插件的组装。
什么是微内核架构? 《软件架构模式》一书中提到:
微内核架构模式(有时被称为插件架构模式)是实现基于产品应用程序的自然模式。基于产品的应用程序是已打包并拥有不同版本的,可以作为第三方插件下载。因此,许多公司也在开发、发布自己内部的商业应用,如有版本号、说明及可加载的插件式应用软件(这也是这种模式的特征)。微内核系统允许用户添加额外的插件到核心应用,从而提供可扩展性和功能分离。
微内核架构包含两种组件:核心系统(core system)和插件模块(plug-in modules)。
核心系统提供系统所需核心能力,而插件模块则可以扩展系统功能。因此,基于微内核架构的系统非常易于扩展功能。
我们常用的一些IDE(集成开发环境),如IDEA和VSCode,都是基于微内核架构设计的,大多数IDE都提供插件以丰富其功能。
正因如此,Dubbo基于微内核架构,我们可以随意替换Dubbo的功能点。例如,若Dubbo的序列化模块的实现不满足需求,我们可以自行实现一个序列化模块。
通常情况下,微内核会采用Factory、IoC、OSGi等方式管理插件的生命周期。Dubbo选择了一种最简单的Factory方式管理插件,即JDK标准的SPI扩展机制(java.util.ServiceLoader
)。
关于Dubbo架构的一些自测小问题
注册中心的作用是什么?
注册中心负责服务地址的注册与查找,相当于目录服务。服务提供方和消费方只在启动时与注册中心交互。
服务提供者宕机后,注册中心会做什么?
注册中心会立即向消费者推送事件通知。
监控中心的作用是什么?
监控中心负责统计各服务的调用次数与调用时长。
注册中心和监控中心同时宕机,服务会停止运行吗?
不会。即使两者同时宕机,已运行的服务提供者和消费者不会受到影响,消费者本地缓存了提供者列表。注册中心和监控中心都是可选的,消费者可以直连服务提供者。
Dubbo的负载均衡策略
什么是负载均衡?
我们先来看一下负载均衡的官方定义:
负载均衡改善了跨多个计算资源(如计算机、计算机集群、网络连接、中央处理单元或磁盘驱动)的工作负载分配。负载均衡旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。使用具有负载均衡的多个组件(而非单个组件)可以通过冗余提高可靠性和可用性。负载均衡通常涉及专用软件或硬件。
为便于理解,换个说法:
当系统中某个服务的访问量极大时,我们将该服务部署在多台服务器上。当客户端发起请求时,多个服务器均可处理该请求。因此,如何正确选择处理该请求的服务器变得至关重要。如果始终只用一台服务器处理该服务的请求,部署在多台服务器的意义将不复存在。负载均衡的目的正是为了避免单个服务器对同一请求的过载,以减少服务器宕机和崩溃的风险。
Dubbo提供的负载均衡策略有哪些?
在集群负载均衡中,Dubbo提供了多种负载均衡策略,默认为random
随机调用。我们还可以自行扩展负载均衡策略(参考Dubbo的SPI机制)。
在Dubbo中,所有负载均衡的实现类均继承自AbstractLoadBalance
,该类实现了LoadBalance
接口,并封装了一些公共逻辑。
public abstract class AbstractLoadBalance implements LoadBalance {
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
}
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
}
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);
int getWeight(Invoker<?> invoker, Invocation invocation) {
}
}
AbstractLoadBalance
的实现类包括:
官方文档对负载均衡部分的介绍相当详细,推荐大家查阅,地址:Dubbo负载均衡文档。
RandomLoadBalance
根据权重随机选择(对加权随机算法的实现)。这是Dubbo默认采用的负载均衡策略。
RandomLoadBalance
的实现原理十分简单,假设有两个提供相同服务的服务器S1和S2,S1的权重为7,S2的权重为3。
我们将这些权重值分布在坐标区间会得到:S1->[0, 7),S2->(7, 10]。我们生成一个[0, 10)之间的随机数,若随机数落在指定区间,则选择对应的服务器来处理请求。
RandomLoadBalance
RandomLoadBalance
的源码相当简单,花几分钟时间了解一下即可。
以下源码来自Dubbo主分支的最新版本2.7.9。
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size();
boolean sameWeight = true;
int[] weights = new int[length];
int totalWeight = 0;
// 下面这个for循环的主要作用是计算所有该服务的提供者的权重之和
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight;
weights[i] = totalWeight;
if (sameWeight && totalWeight != weight * (i + 1)) {
sameWeight = false;
}
}
if (totalWeight > 0 && !sameWeight) {
// 随机生成一个[0, totalWeight)区间内的数字
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 判断会落在哪个服务提供者的区间
for (int i = 0; i < length; i++) {
if (offset < weights[i]) {
return invokers.get(i);
}
}
}
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
}
LeastActiveLoadBalance
LeastActiveLoadBalance
,直译为最小活跃数负载均衡。
这个名字看似不直观,若不仔细看官方对活跃数的定义,可能无法理解其含义。
初始状态下,所有服务提供者的活跃数均为0(每个服务提供者中特定方法都有一个活跃数)。当收到请求后,对应的服务提供者活跃数+1,请求处理完成后,活跃数-1。
因此,Dubbo会认为活跃数越少的服务提供者,处理速度越快,性能也越好,从而优先将请求交给活跃数较少的服务提供者处理。
如果多个服务提供者活跃数相同怎么办?
很简单,使用RandomLoadBalance
进行随机选择。
public class LeastActiveLoadBalance extends AbstractLoadBalance {
public static final String NAME = "leastactive";
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size();
int leastActive = -1;
int leastCount = 0;
int[] leastIndexes = new int[length];
int[] weights = new int[length];
int totalWeight = 0;
int firstWeight = 0;
boolean sameWeight = true;
// 这个for循环遍历invokers列表,找出活跃数最小的Invoker
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
// 获取invoker对应的活跃数
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
int afterWarmup = getWeight(invoker, invocation);
weights[i] = afterWarmup;
if (leastActive == -1 || active < leastActive) {
leastActive = active;
leastCount = 1;
leastIndexes[0] = i;
totalWeight = afterWarmup;
firstWeight = afterWarmup;
sameWeight = true;
} else if (active == leastActive) {
leastIndexes[leastCount++] = i;
totalWeight += afterWarmup;
if (sameWeight && afterWarmup != firstWeight) {
sameWeight = false;
}
}
}
// 如果只有一个Invoker具有最小的活跃数,直接返回该Invoker
if (leastCount == 1) {
return invokers.get(leastIndexes[0]);
}
// 如果有多个Invoker具有相同的最小活跃数,且它们之间的权重不同
if (!sameWeight && totalWeight > 0) {
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexes[i];
offsetWeight -= weights[leastIndex];
if (offsetWeight < 0) {
return invokers.get(leastIndex);
}
}
}
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
}
活跃数通过RpcStatus
中的ConcurrentMap
保存,依据URL及服务提供者被调用的方法名,我们即可获取到对应的活跃数。换句话说,服务提供者中的每个方法的活跃数都是独立的。
public class RpcStatus {
private static final ConcurrentMap<String, ConcurrentMap<String, RpcStatus>> METHOD_STATISTICS =
new ConcurrentHashMap<String, ConcurrentMap<String, RpcStatus>>();
public static RpcStatus getStatus(URL url, String methodName) {
String uri = url.toIdentityString();
ConcurrentMap<String, RpcStatus> map = METHOD_STATISTICS.computeIfAbsent(uri, k -> new ConcurrentHashMap<>());
return map.computeIfAbsent(methodName, k -> new RpcStatus());
}
public int getActive() {
return active.get();
}
}
ConsistentHashLoadBalance
ConsistentHashLoadBalance
是大家较为熟悉的负载均衡策略,广泛应用于分库分表等多种场景。
ConsistentHashLoadBalance
策略中并不涉及权重,具体由请求参数决定哪个服务提供者处理请求。相同参数的请求总是指向同一服务提供者。
此外,为了避免数据倾斜(节点不够分散,大量请求涌向同一节点),Dubbo还引入了虚拟节点的概念。通过虚拟节点,可以使请求量更加均匀分散。
官方提供了详细的源码分析,地址:Dubbo一致性哈希负载均衡文档。相关的PR#5440也修复了老版本中的一些Bug,感兴趣的小伙伴可多花时间研究。
RoundRobinLoadBalance
加权轮询负载均衡。
轮询即将请求依次分配给每个服务提供者,加权轮询则是在轮询的基础上让更多请求落到权重更大的服务提供者上。例如,若有两台提供相同服务的服务器S1和S2,S1的权重为7,S2的权重为3。
在10次请求中,S1将处理7次,S2将处理3次。
而如果使用RandomLoadBalance
,可能存在10次请求中9次都由S1处理的概率问题。
Dubbo中的RoundRobinLoadBalance
实现经过多次重构,Dubbo-2.6.5版本的RoundRobinLoadBalance
则实现了平滑加权轮询算法。
Dubbo序列化协议
Dubbo支持哪些序列化方式?
Dubbo支持多种序列化方式,包括Java自带的序列化、Hessian2、JSON、Kryo、FST、Protostuff、ProtoBuf等。
Dubbo默认使用的序列化方式为Hessian2。
你对这些序列化协议的看法是什么?
一般情况下,我们不会直接使用Java自带的序列化方式,主要有以下两个原因:
- 不支持跨语言调用:若要调用其他语言开发的服务将无法使用。
- 性能较差:相较于其他序列化框架,性能明显低下,主要是因为序列化后字节数组体积更大,导致传输成本增加。
由于性能原因,我们通常不会考虑使用JSON序列化。
像Protostuff、ProtoBuf、Hessian2等序列化方式都是跨语言的,若有跨语言需求可考虑使用。
Kryo和FST作为后期引入的序列化方式,性能极佳。但这两者都是专门针对Java语言的。根据Dubbo官网的一篇文章,推荐在生产环境中使用Kryo作为序列化方式。(文章地址:Dubbo序列化推荐)
Dubbo官方文档中还有关于这些序列化协议的性能对比图供参考。