Dubbo调用过程

1,890 阅读12分钟

dubbo服务注册与发现

dubbo默认使用zk作为注册中心,也可以用redis等作为注册中心,下面分析一下怎么利用zk的?

zookeeper

先了解一下zk,zk其实类似于linux的文件目录结构,它的数据模型就是由多个ZNode组成的树节点,每个ZNode可存有data也可以有子节点。最重要的几个特性:

  1. 节点分为临时(Ephemeral)和永久(Persistent)的,临时的生命周期就是Session,当连接断开时,该Session内创建的所有ZNode都将自动删除。而永久的除非手动删除,否则一直存在;
  2. 节点可以顺序自动编号,分布式锁就是依靠这个特性实现的;
  3. 监听器模式Watcher,可以监听节点内容的变化和子节点数量的变化;

服务注册

dubbo在服务导出时,会将服务注册到zk上,其实就是在zk上建立一个ZNode,并且是临时的,这样做的目的是为了服务提供者下线后,该服务建立的的ZNode都将自动删除,从而实现失效自动踢出。dubbo用的zk客户端是CuratorZookeeperClient,下面展示一下dubbo在zk注册后,zk的结构图(假设有一个服务是DemoService,那服务提供者的节点路径就是DemoService/providers,data就是URL)

服务发现

dubbo服务发现的逻辑是在服务字典模块中的,白话讲就是服务字典存储了provider列表。
负载均衡前需要调用directory.list(invocation),而directory就是服务字典的对象,实现类有StaticDirectory 和 RegistryDirectory,就是RegistryDirectory实现了服务发现的功能。该类实现了NotifyListener接口,当注册中心的provider有变化时,将会回调这个接口通知consumer。 这个接口的具体实现逻辑:
1.RegistryDirectory缓存了一个MAP<method,invoker>;
2.notify方法就是写这个map(provider的上线下线)
3.list方法就是读这个map(list方法里还有服务路由的逻辑)

Invoker

Invoker 是由 ProxyFactory 创建而来,Dubbo 默认的 ProxyFactory 实现类是 JavassistProxyFactory.

调用链

  1. 当我们引用dubbo服务时(如使用@Reference注入),注入的是dubbo生成的代理类;具体过程如下:
  • 服务引用的入口方法为 ReferenceBean 的 getObject 方法,该方法定义在 Spring 的 FactoryBean 接口中,ReferenceBean 实现了这个方法
  • 该方法最重要的逻辑是createProxy,先构建Invoker实例,并生成代理类;代理类的方法调用其实是委托给Invoker。另外,filter链也是在此处初始化的;
  1. 当我们调用dubbo服务的某个方法时,过程如下:
    (1)调用代理类方法
    (2)代理类委托给InvokerInvocationHandler#invoke
    (3)经过AbstractClusterInvoker#invoke,也就是负载均衡,选择其中一个服务提供者
    (4)经过filter链处理
    (5)DubboInvoker#doInvoke
    (6)委托给Exchange层创建request并发起远程请求
    (7)Exchange层包装request,然后委托给Transport层发送请求
    (8)Transport层默认采用netty发起网络socket请求
proxy0#sayHello(String)
  —> InvokerInvocationHandler#invoke(Object, Method, Object[])  
    —> MockClusterInvoker#invoke(Invocation)  
      —> AbstractClusterInvoker#invoke(Invocation)  
        —> FailoverClusterInvoker#doInvoke(Invocation, List<Invoker<T>>, LoadBalance)  
         —> ListenerInvokerWrapper#invoke(Invocation)   
            —> Filter#invoke(Invoker, Invocation)    // 包含多个 Filter 调用
              —> AbstractInvoker#invoke(Invocation)   
                —> DubboInvoker#doInvoke(Invocation)  
                  —> ReferenceCountExchangeClient#request(Object, int)  
                    —> HeaderExchangeClient#request(Object, int)  
                      —> HeaderExchangeChannel#request(Object, int)  
                        —> AbstractPeer#send(Object)  
                          —> AbstractClient#send(Object, boolean)  
                            —> NettyChannel#send(Object, boolean)  
                              —> NioClientSocketChannel#write(Object)  

面试问题

1. dubbo的rpc支持哪几种协议实现?

dubbo、http、rmi、hession、webservice、redis、thrift等,默认使用dubbo协议。

  • Dubbo 协议
    采用单一长连接NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。当需要传输大数据,请使用rmi或者http。
    单一长连接:一个consumer和一个provider只建立一个连接,防止单个consumer把producer击垮,长连接是为了减少连接释放耗时,提高传输效率。
    NIO异步通讯:复用线程池,单线程监听多个socket(多路复用),防止 C10K 问题
  • hessian协议
    多连接,短连接,同步传输,底层使用http协议,适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
  • http协议 多连接,短连接,同步传输,使用表单序列化(json或xml),适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。
  • rmi协议 多连接,短连接,同步传输,jdk标准二进制序列化,适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。

2. dubbo负载均衡有哪几种?原理是什么?

Dubbo 提供了4种负载均衡实现,分别是基于权重随机算法的 RandomLoadBalance(缺省)、基于最少活跃调用数算法的 LeastActiveLoadBalance、基于 hash 一致性的 ConsistentHashLoadBalance,以及基于加权轮询算法的 RoundRobinLoadBalance。这四种算法都是实现了LoadBalance接口,用户也可以自定义负载均衡实现。

3. dubbo集群容错有哪几种方式?

  • Failover Cluster - 失败自动切换
    FailoverClusterInvoker 在调用失败时,会自动切换 Invoker 进行重试。默认配置下,Dubbo 会使用这个类作为缺省 Cluster Invoker
  • Failfast Cluster - 快速失败
    FailfastClusterInvoker 只会进行一次调用,失败后立即抛出异常。适用于幂等操作,比如新增记录。
  • Failsafe Cluster - 失败安全
    FailsafeClusterInvoker 是一种失败安全的 Cluster Invoker。所谓的失败安全是指,当调用过程中出现异常时,FailsafeClusterInvoker 仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。
  • Failback Cluster - 失败自动恢复
    FailbackClusterInvoker 会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重传,适合执行消息通知等操作
  • Forking Cluster - 并行调用多个服务提供者
    ForkingClusterInvoker 会在运行时通过线程池创建多个线程,并发调用多个服务提供者。只要有一个服务提供者成功返回了结果,doInvoke 方法就会立即结束运行。ForkingClusterInvoker 的应用场景是在一些对实时性要求比较高读操作(注意是读操作,并行写操作可能不安全)下使用,但这将会耗费更多的资源。

4. dubbo序列化默认用的是什么?为什么?

默认用的是hession2,支持的序列化框架还有kyro,fst,dubbo,jdk,protobuf以及jason序列化框架fastjson,gson;hession2相对于jdk序列化效率高很多,但是,即使不注册任何类,Kryo和FST的性能依然普遍优于hessian和dubbo序列化。 可以配置为Kryo和FST。dubbo 2.0之所以还默认是hession2,可能是为了兼容dubbo1.0,而且一般都是小数据量序列化影响不是特别大。 hession序列化的bug:1.会将Byte反序列化成Integer 2.当子类和父类拥有相同属性时,会导致反序列化时取不到改属性的值(实际取得是父类的值)

5. Dubbo的RpcContext是怎么传递的?

(1)隐式传参过程 RpcContext保存了当前调用的上下文信息,实际上是一个ThreadLocal变量

private static final InternalThreadLocal<RpcContext> LOCAL = new InternalThreadLocal<RpcContext>() {
        @Override
        protected RpcContext initialValue() {
            return new RpcContext();
        }
    };

参考上面的调用链,在其中的ConsumerContextFilter中会初始化RpcContext

RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getLocalHost(), 0)
                .setRemoteAddress(invoker.getUrl().getHost(),
                        invoker.getUrl().getPort());

然后在调用远程服务前,dubboInvoker会将RpcContext.getAttachments传给参数invocation,那么在服务提供端可以拿到invocation,拿到之后会在ContextFilter中将所有的参数重新赋值给服务端的RpcContext

Map<String, String> attachments = invocation.getAttachments();
        if (attachments != null) {
            attachments = new HashMap<String, String>(attachments);
            attachments.remove(Constants.PATH_KEY);
            attachments.remove(Constants.GROUP_KEY);
            attachments.remove(Constants.VERSION_KEY);
            attachments.remove(Constants.DUBBO_VERSION_KEY);
            attachments.remove(Constants.TOKEN_KEY);
            attachments.remove(Constants.TIMEOUT_KEY);
            attachments.remove(Constants.ASYNC_KEY);// Remove async property to avoid being passed to the following invoke chain.
        }
        RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
//                .setAttachments(attachments)  // merged from dubbox
                .setLocalAddress(invoker.getUrl().getHost(),
                        invoker.getUrl().getPort());

        // mreged from dubbox
        // we may already added some attachments into RpcContext before this filter (e.g. in rest protocol)
        if (attachments != null) {
            if (RpcContext.getContext().getAttachments() != null) {
                RpcContext.getContext().getAttachments().putAll(attachments);
            } else {
                RpcContext.getContext().setAttachments(attachments);
            }
        }

(2)RpcContext是怎么在主线程和线程池之间传递的?
在服务提供端接受到请求后,准确的讲,nettyServer监听到channel写入数据时,首先会通过解码器对数据进行解码,解码后生成一个Request对象,该对象的数据主要为RpcInvocation(客户端传过来的),很多RpcContext的数据都存在RpcInvocation中,所以客户端的RpcContext是通过RpcInvocation传给服务端的,然后在服务端重新创建一个RpcContext。
解码后的对象传递给下一个入站处理器的指定方法。编解码过程我们先忽略,处理链的入口NettyHandler处理,接下去的处理链如下:

NettyHandler#messageReceived(ChannelHandlerContext, MessageEvent)
  —> AbstractPeer#received(Channel, Object)
    —> MultiMessageHandler#received(Channel, Object)
      —> HeartbeatHandler#received(Channel, Object)
        —> AllChannelHandler#received(Channel, Object)
          —> ExecutorService#execute(Runnable)    // 由线程池执行后续的调用逻辑

AllChannelHandler其实就是一个disPatcher委托的线程派发类,持有线程池ExecuterService

public class AllDispatcher implements Dispatcher {

    public static final String NAME = "all";

    @Override
    public ChannelHandler dispatch(ChannelHandler handler, URL url) {
        return new AllChannelHandler(handler, url);
    }

}

根据不同的派发策略有不同的ChannelHandler,包括AllChannelHandler、MessageOnlyChannelHandler、 ExecutionChannelHandler和ConnectionOrderedChannelHandler,Direct策略没有对应的ChannelHandler,因为是直接在IO线程上执行。


到这里主线程的工作基本完成了,这里并没有看到RpcContext的任何信息;只是将Message解码组装成Request对象并传递给ChannelHandler。接下来我们看默认派发器AllChannelHandler的逻辑:

ExecutorService cexecutor = getExecutorService();
        try {
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
        }

封装了ChannelEventRunnable对象,该对象也是委托给DecodeHandler处理,该处理器只做解码逻辑,解码操作默认是在线程池完成,这个与选择的线程派发器有关系。
接下来交给HeaderExchangeHandler处理,在这里做请求校验、创建响应对象并将处理交给下层

Object result = handler.reply(channel, msg);

到目前为止transport、exchange层的逻辑已经处理完毕,接下去就是Protocol层,我们看下DubboProtocol的处理逻辑:

private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {

        @Override
        public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
            if (message instanceof Invocation) {
                Invocation inv = (Invocation) message;
                Invoker<?> invoker = getInvoker(channel, inv);
                // need to consider backward-compatibility if it's a callback
                //回调逻辑,先忽略
                RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
                return invoker.invoke(inv);
            }
        }

    Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {
        boolean isCallBackServiceInvoke = false;
        boolean isStubServiceInvoke = false;
        int port = channel.getLocalAddress().getPort();
        String path = inv.getAttachments().get(Constants.PATH_KEY);
        // if it's callback service on client side
        //可忽略
        String serviceKey = serviceKey(port, path, inv.getAttachments().get(Constants.VERSION_KEY), inv.getAttachments().get(Constants.GROUP_KEY));

        DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);

        if (exporter == null)
            throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv);

        return exporter.getInvoker();
    }

注意在这里已经初始化RpcContext了,后续在filter链中会继续设置属性到RpcContext中(上文中已经提到的ContextFilter),Invoker是通过serviceKey到内存中获取对应的exporter,exporter是在服务导出的时候放到内存中的,服务导出是监听spring的刷新事件发生时中启动的。
后面的调用过程与消费过程类似,调用链如下:

ChannelEventRunnable#run()
  —> DecodeHandler#received(Channel, Object)
    —> HeaderExchangeHandler#received(Channel, Object)
      —> HeaderExchangeHandler#handleRequest(ExchangeChannel, Request)
        —> DubboProtocol.requestHandler#reply(ExchangeChannel, Object)
          —> Filter#invoke(Invoker, Invocation)
            —> AbstractProxyInvoker#invoke(Invocation)
              —> Wrapper0#invokeMethod(Object, String, Class[], Object[])
                —> DemoServiceImpl#sayHello(String)

完整调用图

综上所述:
消费者将RpcContext的数据通过Invocation传给提供者; 提供者将Invocation解码并封装到Request中; 提供者主线程将Request传给线程池; 线程池中将Request数据拿出来,新建一个RpcContext放进去;

6. dubbo的消费过程?

参考上文

7. dubbo的服务注册和服务发现是怎么实现的?

参考上文

8. dubbo的Spi怎么实现的?自适应扩展点怎么实现的?

参考上一篇文章 juejin.im/post/684490…

9. 了解spring cloud吗?与dubbo有什么区别?

Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智能路由,微代理,控制总线)。
简单来讲,spring cloud致力于让开发者可以快速的搭建起一个复杂的分布式系统,分布式系统里面所需要的一切功能或组件都可以由spring cloud提供,就是一站式解决方案,目前主要有以下重要特性:

  • 分布式/版本化配置(spring cloud config)
  • 服务注册和发现(eureka)
  • 路由(Zuul)
  • service - to - service调用(feign)
  • 负载均衡(ribbon)
  • 断路器(hystrix)
  • 分布式消息传递(spring cloud stream)

这里重点比较一下spring cloud的rpc组件feign和dubbo的区别:
| 核心要素| dubbo | spring cloud |
|--|-------|-----|-----|
|服务注册中心|zookeeper、redis|eukare| |远程调用协议|dubbo(默认),http,rmi|http| |服务网关|无|zuul| |断路器|不完善(简单的服务降级)|hystrix| |分布式追踪| 无|sleuth| |负载均衡|内部实现|ribbon|

1、dubbo由于是二进制的传输,占用带宽会更少

2、springCloud是http协议传输,带宽会比较多,同时使用http协议一般会使用JSON报文,消耗会更大

3、dubbo的开发难度较大,原因是dubbo的jar包依赖问题很多大型工程无法解决

4、springcloud的接口协议约定比较自由且松散,需要有强有力的行政措施来限制接口无序升级

5、dubbo的注册中心可以选择zk,redis等多种,springcloud的注册中心只能用eureka或者自研
原文链接:blog.csdn.net/xuri24/arti…

10.dubbo并发控制怎么实现的?

provider通过ExecuteLimitFilter实现该功能,具体是通过Semaphore,根据配置参数创建一个Semaphore(max),每当一个请求进来就tryacquire,请求返回之后就release。 dubbo默认的线程池是固定线程池,大小是200,超过直接抛异常。

dubbo消费者发送请求后线程处于什么状态?

waiting。

  1. 业务线程发出请求,DefaultFuture实例,继承于CompletableFuture
  2. 在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待take(),直到队列中被加入元素。
  3. 当业务数据返回后,生成一个 Runnable Task 并放入 ThreadlessExecutor 队列
  4. 业务线程将 Task 取出并在本线程中执行:反序列化业务数据并 set 到 Future。
  5. 业务线程拿到结果直接返回
    那是怎么将request和response绑定上的呢? 每个request都是有一个唯一id,在DefaultFuture类中有一个Map维护id->DefaultFuture的映射,其实就是id对应的request的映射,这样收到了一个response消息就能对应上request。
    那怎么知道收到的是request还是response?
    协议头里面有一bit专门表示

怎么实现超时逻辑的?

DefaultFuture有一个TimeOutTask,为每个请求定时
blog.csdn.net/leisurelen/…

配置优先级

1.精确优先(方法级>接口级>全局配置(消费者))

2.消费者设置优先(在生产者和消费者中同时设置超时时间(防止由于网络等原因造成的堵塞),消费者设置的超时时间生效)

泛华调用

www.jianshu.com/p/e3a42571e…