首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >你管这个叫Dubbo?

你管这个叫Dubbo?

作者头像
Java识堂
发布于 2022-04-06 02:33:37
发布于 2022-04-06 02:33:37
36000
代码可运行
举报
文章被收录于专栏:Java识堂Java识堂
运行总次数:0
代码可运行

RPC框架的实现

又到年初了,大家又要开始准备面试了。为了方便大家,我就写几篇面试相关的文章吧,这次是Dubbo

相信很多小伙伴已经看了很多Dubbo的八股文了。比如,Dubbo支持哪些序列化框架,支持哪些注册中心,支持哪些集群容错策略,支持服务降级吗?但是你知道Dubbo服务导出和服务引入的过程吗?服务降级是如何实现的?等等

本文就从源码的角度来分享一下Dubbo的整个调用过程(放心,图示为主,辅助一少部分源码)

「RPC框架的实现基本上都是如下架构」

一个RPC调用的过程如下

  1. 调用方发送请求后由代理类将调用的方法,参数组装成能进行网络传输的消息体
  2. 调用方代理类将消息体发送到提供方
  3. 提供方代理类将消息进行解码,得到调用的方法和参数
  4. 提供方代理类执行相应的方法,并将结果返回

「协议,编解码,序列化的部分不是本文的重点,我就不分析了,有兴趣的可以看我之前的文章。」

首先来手写一个极简版的RPC框架,以便你对上面的流程有一个更深的认识

手写一个简单的PRC框架

封装网络请求对象

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RpcRequest implements Serializable {

    private String interfaceName;
    private String methodName;
    private Class<?>[] paramTypes;
    private Object[] parameters;
}

根据interfaceName可以确定需要调用的接口,methodName和paramTypes则可以确定要调用接口的方法名,定位到具体的方法,传入参数即可调用方法

封装调用接口

封装接口到api模块,producer端写实现逻辑,consumer端写调用逻辑

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface HelloService {

    String sayHello(String content);
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface UpperCaseService {

    String toUpperCase(String content);
}

开发producer端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String content) {
        return "hello " + content;
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class UpperCaseServiceImpl implements UpperCaseService {

    @Override
    public String toUpperCase(String content) {
        return content.toUpperCase();
    }
}

ServiceMap保存了producer端接口名和接口实现类的映射关系,这样可以根据请求对象的接口名,找到对应的实现类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ServiceMap {

    // 接口名 -> 接口实现类
    private static Map<String, Object> serviceMap = new HashMap<>();

    public static void registerService(String serviceKey, Object service) {
        serviceMap.put(serviceKey, service);
    }

    public static Object lookupService(String serviceKey) {
        return serviceMap.get(serviceKey);
    }
}

为了提高服务端的并发度,我们将每一个请求的处理过程放到线程池中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Slf4j
public class RequestHandler implements Runnable {

    private Socket socket;

    public RequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try (ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
             ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())) {
            RpcRequest rpcRequest = (RpcRequest) inputStream.readObject();
            Object service = ServiceMap.lookupService(rpcRequest.getInterfaceName());
            Method method = service.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getParamTypes());
            Object result = method.invoke(service, rpcRequest.getParameters());
            outputStream.writeObject(result);
        } catch (Exception e) {
            log.error("invoke method error", e);
            throw new RuntimeException("invoke method error");
        }
    }

}

启动服务端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RpcProviderMain {

    private static final ExecutorService executorService = Executors.newCachedThreadPool();

    public static void main(String[] args) throws Exception {

        HelloService helloService = new HelloServiceImpl();
        UpperCaseService upperCaseService = new UpperCaseServiceImpl();
        // 将需要暴露的接口注册到serviceMap中
        ServiceMap.registerService(HelloService.class.getName(), helloService);
        ServiceMap.registerService(UpperCaseService.class.getName(), upperCaseService);

        ServerSocket serverSocket = new ServerSocket(8080);

        while (true) {
            // 获取一个套接字(阻塞)。所以为了并行,来一个请求,开一个线程处理
            // 为了复用线程,用了threadPool
            final Socket socket = serverSocket.accept();
            executorService.execute(new RequestHandler(socket));
        }
    }
}

开发consumer端

前面说过,我们要通过动态代理对象解耦方法调用和网络调用,所以接下来我们就写一下动态代理对象的实现逻辑

生成一个代理对象的过程很简单

  1. 实现InvocationHandler接口,在invoke方法中增加代理逻辑
  2. 调用Proxy.newProxyInstance方法生成代理对象,3个参数分别是ClassLoader,代理对象需要实现的接口数组,InvocationHandler接口实现类
  3. 当执行代理执行实现的接口方法时,会调用到InvocationHandler#invoke,这个方法中增加了代理逻辑哈。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ConsumerProxy {

    public static <T> T getProxy(final Class<T> interfaceClass, final String host, final int port) {

        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass}, new ConsumerInvocationHandler(host, port));
    }
}

可以看到代理对象的主要功能就是组装请求参数,然后发起网络调用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Slf4j
public class ConsumerInvocationHandler implements InvocationHandler {

    private String host;
    private Integer port;

    public ConsumerInvocationHandler(String host, Integer port) {
        this.host = host;
        this.port = port;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        try (Socket socket = new Socket(host, port);
             ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
             ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())) {
            RpcRequest rpcRequest = RpcRequest.builder()
                    .interfaceName(method.getDeclaringClass().getName())
                    .methodName(method.getName())
                    .paramTypes(method.getParameterTypes())
                    .parameters(args).build();
            outputStream.writeObject(rpcRequest);
            Object result = inputStream.readObject();
            return result;
        } catch (Exception e) {
            log.error("consumer invoke error", e);
            throw new RuntimeException("consumer invoke error");
        }
    }
}

此时我们只需要通过ConsumerProxy#getProxy方法,就能很方便的获取到代理对象。通过代理对象调用远程方法和调用本地方法一样方便

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RpcConsumerMain {

    public static void main(String[] args)  {

        // 因为这是一个小demo,就不拆分多模块了
        // 这个HelloService是通过网络调用的HelloServiceImpl,而不是本地调用
        HelloService helloService = ConsumerProxy.getProxy(HelloService.class, "127.0.0.1", 8080);
        // hello world
        System.out.println(helloService.sayHello("world"));
        UpperCaseService upperCaseService = ConsumerProxy.getProxy(UpperCaseService.class, "127.0.0.1", 8080);
        // THIS IS CONTENT
        System.out.println(upperCaseService.toUpperCase("this is content"));
    }
}

至此我们已经把一个RPC框架最核心的功能就实现了,是不是很简单。「其实Dubbo的源码也很简单,只不过增加了很多扩展功能,所以大家有时候会认为比较难。」

所以我们就来分析一下核心的扩展功能。比如Filter,服务降级,集群容错等是如何实现的?其他的扩展功能,比如支持多种注册中心,支持多种序列化框架,支持多种协议,基本不会打交道,所以就不浪费时间了

从前面的图示我们知道,代理类在服务调用和响应过程中扮演着重要的角色。「在Dubbo中,代理类有个专有名词叫做Invoker,而Dubbo中就是通过对这个Invoker不断进行代理增加各种新功能的」

Dubbo服务导出

「当第三方框架想和Spring整合时,有哪些方式?」

  1. 实现BeanFactoryPostProcessor接口(对BeanFactory进行扩展)
  2. 实现BeanPostProcessor接口(对Bean的生成过程进行扩展)

Dubbo也不例外,当Dubbo和Spring整合时,会往容器中注入2个BeanPostProcessor,作用如下

ServiceAnnotationBeanPostProcessor,将@Service注解的类封装成ServiceBean注入容器 ReferenceAnnotationBeanPostProcessor,将@Reference注解的接口封装成ReferenceBean注入容器

所以服务导出和服务引入肯定和ServiceBean和ReferenceBean的生命周期有关。

「ServiceBean实现了ApplicationListener接口,当收到ContextRefreshedEvent事件时(即Spring容器启动完成)开始服务导出。」

服务导出比较重要的2个步骤就是

  1. 将服务注册到zk(我们后面的分析,注册中心都基于zk哈)
  2. 将服务对象包装成Invoker,并保存在一个map中,key为服务名,value为Invoker对象

「当收到请求时,根据服务名找到Invoker对象,Invoker对象根据方法名和参数反射执行方法,然后将结果返回。」

这里留个小问题,反射执行方式效率会很低,那么在Dubbo中还有哪些解决方案呢?

从图中可以看到AbstractProxyInvoker被其他Invoker进行代理了,而这些Invoker是用来执行Filter的,一个Invoker代理类执行一个Filter,层层进行代理

「如下图为Dubbo收到请求层层调用的过程」

Dubbo服务引入

前面我们已经推断出来服务导出和ReferenceBean有关。我们来看看具体在哪个阶段?ReferenceBean实现了FactoryBean接口,并重写了getObject方法,在这个方法中进行服务导出。因此我们推断服务导出的时机是ReferenceBean被其他对象注入时

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public Object getObject() {
    return get();
}

接下来就是从注册中心获取服务地址,构建Invoker对象,并基于Invoker对象构建动态代理类,赋值给接口。

最终能发起网络调用的是DubboInvoker,而这个Invoker被代理了很多层,用来实现各种扩展功能。

服务降级

第一个就是服务降级,什么是服务降级呢?

「当服务可不用时,我们不希望抛出异常,而是返回特定的值(友好的提示等),这时候我们就可以用到服务降级。」

dubbo中有很多服务降级策略,简单举几个例子

force: 代表强制使用 Mock 行为,在这种情况下不会走远程调用 fail: 只有当远程调用发生错误时才使用 Mock 行为

假如有如下一个controller,调用DemoService获取值,但是DemoService并没有启动

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RestController
public class DemoController {

    @Reference(check = false, mock = "force:return mock")
    private DemoService demoService;

    @RequestMapping("hello")
    public String hello(@RequestParam("msg") String msg) {
        return demoService.hello(msg);
    }

}

可以看到直接返回mock字符串(也并不会发生网络调用)

将@Reference的mock属性改为如下,再次调用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RestController
public class DemoController {

    @Reference(check = false, mock = "fail:return fail")
    private DemoService demoService;

    @RequestMapping("hello")
    public String hello(@RequestParam("msg") String msg) {
        return demoService.hello(msg);
    }

}

会发起网络调用,调用失败,然后返回fail。

「dubbo中的服务降级只用了MockClusterInvoker这一个类来实现,因此相对于Hystrix等功能很简单,实现也很简单,如下图。」

  1. 当Reference不配置mock属性或者属性为false时,表示不进行降级,直接调用代理对象即可
  2. 以属性以force开头时,表示直接进行降级,都不会发生网络调用
  3. 其他请求就是在进行网络失败后才进行降级

集群容错

过了服务降级这一层,接下来就到了集群容错了。

dubbo中有很多集群容错策略

容错策略

解释

代理类

AvailableCluster

找到一个可用的节点,直接发起调用

AbstractClusterInvoker匿名内部类

FailoverCluster

失败重试(默认)

FailoverClusterInvoker

FailfastCluster

快速失败

FailfastClusterInvoker

FailsafeCluster

安全失败

FailsafeClusterInvoker

FailbackCluster

失败自动恢复

FailbackClusterInvoker

ForkingCluster

并行调用

ForkingClusterInvoker

BroadcastCluster

广播调用

BroadcastClusterInvoker

Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。

Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。

Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。

「读操作建议使用 Failover 失败自动切换,默认重试两次其他服务器。写操作建议使用 Failfast 快速失败,发一次调用失败就立即报错。」

不知道你发现没?「换集群容错策略就是换DubboInvoker的代理类」

集群容错相关的代理类都有一个共同的属性RegistryDirectory,这个是一个很重要的组件,它用List保存了服务提供者对应的所有Invoker。

「更牛逼的是这个List是动态变化的,当服务提供者下线时,会触发相应的事件,调用方会监听这个事件,并把对应的Invoker删除,这样后续就不会调用到下线的服务了。当有新的服务提供者时,会触发生成新的Invoker。」

当一个服务的多个Invoker摆在我们面前时,该选择哪个来调用呢?这就不得不提到负载均衡策略了。

负载均衡策略实现类

解释

RandomLoadBalance

随机策略(默认)

RoundRobinLoadBalance

轮询策略

LeastActiveLoadBalance

最少活跃调用数

ConsistentHashLoadBalance

一致性hash策略

「我们只需要通过合适的负载均衡策略来选择即可」

和服务端类似类似,最终能发送网络请求的Invoker还会被Filter对应的Invoker类所代理,一个Filter一个代理类,层层代理。

如下图为Dubbo发送请求时层层调用的过程

好了,Dubbo一些比较重要的扩展点就分享完了,整个请求响应的基本过程也串下来了!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-02-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java识堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Dubbo
说一说提供者启动流程? ServiceAnnotationBeanPostProcessor实现了BeanDefinitionRegistryPostProcessor接口,在它的register
spilledyear
2020/02/18
7750
Dubbo源码学习-服务引用(远程调用阶段)
Dubbo 支持同步和异步两种调用方式,其中异步调用还可细分为“有返回值”的异步调用和“无返回值”的异步调用。所谓“无返回值”异步调用是指服务消费方只管调用,但不关心调用结果,此时 Dubbo 会直接返回一个空的 RpcResult。若要使用异步特性,需要服务消费方手动进行配置。默认情况下,Dubbo 使用同步调用方式。
周同学
2020/03/20
1.2K0
Dubbo源码学习-服务引用(远程调用阶段)
Dubbo介绍、原理
Dubbo是一个分布式服务框架,以及SOA治理方案。其功能主要包括:高性能NIO通讯及多协议集成,服务动态寻址与路由,软负载均衡与容错,依赖分析与降级等。
DannyHoo
2022/04/02
2.4K0
Dubbo介绍、原理
关于Dubbo随便问八个问题
互联网公司的系统有成千上万个大大小小的服务组成,服务各自部署在不同的机器上,服务间的调用需要用到网络通信,服务消费方每调用一个服务都要写一坨网络通信相关的代码,不仅复杂而且极易出错。还要考虑新服务依赖老服务时如何调用老服务,别的服务依赖新服务的时候新服务如何发布方便他人调用。如何解决这个问题呢?业界一般采用RPC远程调用的方式来实现。
sowhat1412
2022/09/20
3280
关于Dubbo随便问八个问题
Dubbo服务消费者调用过程
首先通过ReferenceConfig类的private void init()方法会先检查初始化所有的配置信息后,调用private T createProxy(Map<String, String> map)创建代理,消费者最终得到的是服务的代理, 在createProxy接着调用Protocol接口实现的<T> Invoker<T> refer(Class<T> type, URL url)方法生成Invoker实例(如上图中的红色部分),这是服务消费的关键。接下来把Invoker通过ProxyFactory代理工厂转换为客户端需要的接口(如:HelloWorld),创建服务代理并返回。
Java
2018/09/13
1K0
Dubbo服务消费者调用过程
dubbo源码解析
dubbo里面主要用到了三种代理,代理设计模式,jdk代理,javassist代理。如JavassistProxyFactory,JdkProxyFactory类。
张伦聪zhangluncong
2022/10/26
3830
Dubbo2.7源码详解
    1)DubboConfigConfigurationRegistrar类的作用
忧愁的chafry
2022/10/30
7470
Dubbo2.7源码详解
dubbo消费方服务调用过程源码分析
dubbo PRC服务调用过程很复杂,这里准备通过分析一个典型rpc方法调用的调用栈来说明调用过程。说它典型,是因为本次分析的调用场景很典型简单 先定义一个接口 public interface DemoService { public String sayHello(String name); } 然后一个服务实现类 public class DemoServiceImpl implements DemoService { public String sayHello(S
技术蓝海
2018/04/26
5.5K4
Dubbo服务调用原理
从dubbo的架构设计中,我们可以看出服务启动时,除了本地暴露服务之外会把服务注册到注册中心,那么作为消费端,在服务启动的时候则会向注册中心订阅需要调用的服务,然后在调用的时候通过注册中心拿到的地址做负载后选择合适的服务,然后建立连接并实现调用。
叔牙
2022/03/28
1.4K3
Dubbo服务调用原理
dubbo的应用场景与高级特性之高级用法篇
在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直连方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。
Tom弹架构
2023/11/27
6170
dubbo的应用场景与高级特性之高级用法篇
手撕RPC实现基于TCP的仿Dubbo简单实现
文章目录 手撕RPC实现基于TCP的仿Dubbo实现 方法调用效果实现 分模块 写接口 通过代理写好了客户端 写服务端,并联调rpc 代码实现 myRpc rpc-client rpc-interface rpc-server 源码 手撕RPC实现基于TCP的仿Dubbo实现 还记得第一次听到这词是在别人的面试视频里,简单了解了一下只知道是远程调用。 万万没想到我的第一次面试的第一个问题就是与此相关,希望认真准备每一次面试,及时查漏补缺,谨以此文,代表诚意~奥利给! 思路: my-rpc通过cl
瑞新
2020/07/07
7560
微服务架构之服务冶理Dubbo-服务引用
注:公众号关于dubbo解读文章均基于apache-dubbo-incubating-2.7.1版本,发版于5月26号,此版本注册中心(多数是zookeeper)在某些特殊场景下会出现重复URL地址数据无法删除,导致消费方拿到的是失效地址,从而导致调用失败的问题。如果你也在使用此版本进行源码学习,在网络漂移(下班回家再调试源码)的情况下需要手动删除zookeeper的dubbo节点路径
公众号_松华说
2019/07/16
8820
Dubbo 源码分析 - 服务调用过程
注: 本系列文章已捐赠给 Dubbo 社区,你也可以在 Dubbo 官方文档中阅读本系列文章。
田小波
2019/02/15
1K0
Dubbo服务提供者发布过程
首先ServiceConfig类拿到对外提供服务的实际类ref(如:HelloServiceImpl),然后通过ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到Invoker的转化。接下来就是Invoker转换到Exporter的过程。
搜云库技术团队
2019/10/18
5190
Netty在Dubbo中的使用过程源码分析
最近项目中使用了netty服务,空余时间差了下dubbo中是如何使用netty做底层服务的,找了相关资料记录一下:
小勇DW3
2020/04/26
8240
Dubbo系列-扬帆起航
接下来一段时间敖丙将带大家开启紧张刺激的 Dubbo 之旅!是的要开始写 Dubbo 系列的文章了,之前我已经写过一篇架构演进的文章,也说明了微服务的普及化以及重要性,服务化场景下随之而来的就是服务之间的通信问题,那服务间的通信脑海中想到的就是 RPC,说到 RPC 就离不开咱们的 Dubbo。
敖丙
2020/08/19
3850
Dubbo 源码分析 - 服务引用
在上一篇文章中,我详细的分析了服务导出的原理。本篇文章我们趁热打铁,继续分析服务引用的原理。在 Dubbo 中,我们可以通过两种方式引用远程服务。第一种是使用服务直联的方式引用服务,第二种方式是基于注册中心进行引用。服务直联的方式仅适合在调试或测试服务的场景下使用,不适合在线上环境使用。因此,本文我将重点分析通过注册中心引用服务的过程。从注册中心中获取服务配置只是服务引用过程中的一环,除此之外,服务消费者还需要经历 Invoker 创建、代理类创建等步骤。这些步骤,我将在后续章节中一一进行分析。
田小波
2018/12/13
8670
Dubbo——服务引用
上一篇我们分析了服务发布的原理,可以看到默认是创建了一个Netty server,并通过Invoker调用服务,同样,在客户端也会创建一个Inovker对象,下面就一起来看看这个引用创建过程。
夜勿语
2020/09/07
4490
Dubbo整体架构总结
​ 在校期间大家都写过不少程序,比如写个hello world服务类,然后本地调用下,如下所示。这些程序的特点是服务消费方和服务提供方是本地调用关系。
用户5325874
2020/01/16
6520
Dubbo整体架构总结
【DUBBO】 服务引用RegistryDirectory
实现了FactoryBean接口,所以在获取实例的时候,实际上是调用getObject方法返回实例。这里面的get方法继承自ReferenceConfig类
spilledyear
2018/12/24
8520
相关推荐
Dubbo
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验