dubbo原理分析

一、dubbo核心机制

1.基础概念

1.为什么要用 Dubbo?

随着分布式服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务的架构体系(SOA),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架。

因此,为分布式系统的服务调用和治理框架就出现了,Dubbo 也就这样产生了。

2.Dubbo是什么?

Dubbo是一个分布式RPC框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。

3.Dubbo 的使用场景有哪些?

满足下面需求的RPC调用场景,则可推荐使用dubbo
1.透明化的远程方法调用:就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。
2.软负载均衡及容错机制:可在内网替代 F5 等硬件负载均衡器,降低成本,减少单点。
3.服务自动注册与发现:不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。

4.Dubbo 的核心功能有哪些?

1.远程通讯: 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
2. 集群容错: 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
3. 自动发现: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。
4. 服务接口监控与治理:会对接口的调用情况进行统计,如耗时,成功或失败等便于多个服务治理

2.dubbo应用实战

1. 核心配置

配置关系如下:

在这里插入图片描述
配置原则

在Provider上尽量多配置Consumer端属性,原因如下:

1.作为服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等

2.在Provider配置后,Consumer不配置则会使用Provider的配置值,即Provider配置可以作为Consumer的缺省值。否则,Consumer会使用Consumer端的全局设置,这对于Provider不可控的,并且往往是不合理的

在Provider可以配置的Consumer端属性有:

1.timeout,方法调用超时,默认是1000ms。

2.retries,失败重试次数,缺省是2(表示加上第一次调用,会调用3次)

3.loadbalance,负载均衡算法(有多个Provider时,如何挑选Provider调用),缺省是随机(random)。还可以有轮训(roundrobin)、最不活跃优先(leastactive,指从Consumer端并发调用最好的Provider,可以减少的反应慢的Provider的调用,因为反应更容易累积并发的调用)

配置属性:

dubbo:service/

dubbo:reference/:主要用于消费端的配置,注意id要和服务端的<dubbo:service ref的值一一对应
如:

<!-- 声明需要暴露的服务接口 -->  
    <dubbo:service interface="com.test.DemoService" ref="demoService" />  

  <!-- 生成远程服务代理,可以和本地bean一样使用demoService -->    
    <dubbo:reference id="demoService" interface="com.test.DemoService" />  

dubbo:protocol/

dubbo:registry/

服务提供者与消费者可以通过dubbo.registry.address来关联
如:<dubbo:registry protocol="zookeeper" address="${dubbo.registry.address}"

dubbo:application/

dubbo:provider/

dubbo:consumer/

dubbo:method/

在这里插入图片描述
group:当一个接口有多种实现时,可以用group区分,但在实际项目中更倾向于用于区分不同的业务部门。
version:
1.当一个接口的实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间无法调用。
2.也可以用于自己在调试时,相互约定一致的版本,这样就只会调用到你们约定的服务里了。同样的办法,也可以在消费方指定对应的IP,也可以访问到对应的服务。如:

<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />

1.3 Dubbo内置的服务容器

Dubbo内置了哪几种服务容器?

  • Spring Container
  • Jetty Container
  • Log4j Container

Dubbo 的服务容器只是一个简单的 Main 方法,并加载一个简单的 Spring 容器,用于暴露服务。

二、架构设计

1.技术选型

1.通信框架:默认也推荐使用netty框架,其他nio通信框架可以还有mina。
2.系列化框架:默认使用Hessian序列化,其他系列化框架可以有Protobuf,jdk自带的序列化框架等。
3.注册中心:推荐使用zookeeper注册中心,还有redis等不推荐。

2. dubbo架构设计

这里写图片描述

1.核心组件

  • Provider: 暴露服务的服务提供方。
  • Consumer: 调用远程服务的服务消费方。
  • Registry: 服务注册与发现的注册中心。
  • Monitor: 统计服务的调用次调和调用时间的监控中心。
  • Container: 服务运行容器。

这点我觉得非常好,角色分明,可以根据每个节点角色的状态来确定该服务是否正常。

2.调用流程说明

服务容器负责启动,加载,运行服务提供者。

  1. 服务提供者在启动时,向注册中心注册自己提供的服务。
  2. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  3. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  4. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  5. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

3. 组件整体设计

在这里插入图片描述
图例说明:

1.图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。

2.图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。

3.图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。

4.图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。

1.各层核心功能

  • 接口服务层( Service):面向开发者,业务代码、接口、实现等

  • config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig
    为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类

  • proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy为中心,扩展接口为 ProxyFactory。
    对生产者和消费者、dubbo都会产生一个代理类封装调用细节包括编码,序列化,网络传输等,业务层对远程调用无感

  • registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory,Registry, RegistryService

  • cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance

  • monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory,Monitor, MonitorService。

  • protocol 远程调用协议层(dubbo-rpc模块下):封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol,Invoker, Exporter

  • exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer

  • transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec

  • serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool

2.模块核心功能

模块说明:

  • dubbo-common 公共逻辑模块:包括 Util 类和通用模型。
  • dubbo-remoting 远程通讯模块:相当于 Dubbo 协议的实现,如果 RPC 用 RMI协议则不需要使用此包。也就是说使用netty框架进行网络通信的原理实现也是在这个模块之中。
  • dubbo-rpc 远程调用模块:抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理。主要是抽象协议为主
  • dubbo-cluster 集群模块:将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。
  • dubbo-registry 注册中心模块:基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。以提供服务注册和发现接口
  • dubbo-monitor 监控模块:统计服务调用次数,调用时间的,调用链跟踪的服务。
  • dubbo-config 配置模块:是 Dubbo 对外的 API,用户通过 Config 使用Dubbo,隐藏 Dubbo 所有细节。
    以dubbo相关的配置文件有关。充分展示了面向对象的思想,使用建造者模式,可以参考mybaits使用建造者模式构建Configuration对象.
  • dubbo-container 容器模块:是一个 Standlone 的容器,以简单的 Main 加载 Spring 启动,因为服务通常不需要 Tomcat/JBoss 等 Web 容器的特性,没必要用 Web 容器去加载服务。但是在生产环境将会打成jar包或者web包部署到应用服务器上如Tomcat。
    作为容器它主要功能就是启动服务和加载配置文件,并将部分bean进行初始化。

3.设计模式

1.工厂模式
Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段:

private static final Protocol protocol =
ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi
on();

Dubbo 里有很多这种代码。这也是一种工厂模式,只是实现类的获取采用了 JDKSPI 的机制。这么实现
的优点是可扩展性强,想要扩展实现,只需要在 classpath下增加个文件就可以了,代码零侵入。另
外,像上面的 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态
代理,会造成代码调试比较麻烦,需要分析出实际调用的实现类。

4.内置拦截器

用户自定义 filter 默认在内置 filter 之后。

1.TimeoutFilter超时拦截器
  @Override
    public Result onResponse(Result result, Invoker<?> invoker, Invocation invocation) {
        String startAttach = invocation.getAttachment(TIMEOUT_FILTER_START_TIME);
        if (startAttach != null) {
            long elapsed = System.currentTimeMillis() - Long.valueOf(startAttach);
            if (invoker.getUrl() != null
                    && elapsed > invoker.getUrl().getMethodParameter(invocation.getMethodName(),
                    "timeout", Integer.MAX_VALUE)) {
                if (logger.isWarnEnabled()) {
                    logger.warn("invoke time out. method: " + invocation.getMethodName()
                            + " arguments: " + Arrays.toString(invocation.getArguments()) + " , url is "
                            + invoker.getUrl() + ", invoke elapsed " + elapsed + " ms.");
                }
            }
        }
        return result;
    }

当服务端处理超时后,将进入这个拦截器,可以再这个拦截器里面看到对应的dubbo接口以及对应的传参等。特别是排查消费端超时的问题

思考

1.按照分层结构进行分包的思想?

整体上按照分层结构进行分包,与分层的不同点在于:

container 为服务容器,用于部署运行服务,没有在层中画出。

protocol 层和 proxy 层都放在 rpc 模块中,这两层是 rpc的核心,在不需要集群也就是只有一个提供者时,可以只使用这两层完成 rpc 调用。

transport 层和 exchange 层都放在 remoting 模块中,为 rpc 调用的通讯基础。

serialize 层放在 common 模块中,以便更大程度复用。

2.Dubbo Monitor 实现原理-待整理?

Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然
后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有Monitorfilter。

  1. MonitorFilter 向 DubboMonitor 发送数据
  2. DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到
    ConcurrentMap<Statistics, AtomicReference> statisticsMap,然后使用一个含有 3 个线程(线
    程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历
    发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的
    AtomicReference
  3. SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000)
  4. SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)
    将 queue 中的数据写入文件(该线程以死循环的形式来写)
  5. SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程
    池每隔 5min 钟,将文件中的统计数据画成图表

3.Dubbo 配置文件是如何加载到 Spring 中的?

Spring 容器在启动的时候,会读取到 Spring 默认的一些 schema 以及 Dubbo 自定义的schema,每个 schema 都会对应一个自己的 NamespaceHandler,NamespaceHandler 里面通过 BeanDefinitionParser 来解析配置信息并转化为需要加载的 bean 对象!

4 Dubbo网络传输架构设计

1.网络传输架构设计要点

在这里插入图片描述

IO模型:

1.BIO 同步阻塞
2.NIO 同步非阻塞
3.AIO 异步非阻塞

2.dubbo网络传输UML类图:

在这里插入图片描述

简单说就是服务端绑定端口,客户端建立连接。然后通过约定的dubbo协议进行报文传输。

3.Transporter组件的协作如图:

在这里插入图片描述

1.初始连接

时序图:引用服务|增加提供者==>获取连接===》是否获取共享连接==>创建连接客户端==》开启心跳检测状态检查定时任务===》开启连接状态检测

源码见:com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#getClients

2.心跳发送

在创建一个连接客户端同时也会创建一个心跳客户端,客户端默认基于60秒发送一次心跳来保持连接的存活,可通过 heartbeat 设置。

源码见:com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeClient#startHeatbeatTimer

startHeatbeatTimer的源码如下:

 private void startHeatbeatTimer() {
        stopHeartbeatTimer();
        if ( heartbeat > 0 ) {
            heatbeatTimer = scheduled.scheduleWithFixedDelay(
                    new HeartBeatTask( new HeartBeatTask.ChannelProvider() {
                        public Collection<Channel> getChannels() {
                            return Collections.<Channel>singletonList( HeaderExchangeClient.this );
                        }
                    }, heartbeat, heartbeatTimeout),
                    heartbeat, heartbeat, TimeUnit.MILLISECONDS );
        }
    }

其实就是通过一个调度线程池,每隔一段时间就创建一个线程去发送心跳检测。

scheduled属性如下:

 private static final ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(2, new NamedThreadFactory("dubbo-remoting-client-heartbeat", true));
3.断线重连

每创建一个客户端连接都会启动一个定时任务每两秒中检测一次当前连接状态,如果断线则自动重连。

源码见:com.alibaba.dubbo.remoting.transport.AbstractClient#initConnectStatusCheckCommand

/**
     * init reconnect thread
     */
    private synchronized void initConnectStatusCheckCommand(){
        //reconnect=false to close reconnect 
        int reconnect = getReconnectParam(getUrl());
        if(reconnect > 0 && (reconnectExecutorFuture == null || reconnectExecutorFuture.isCancelled())){
            Runnable connectStatusCheckCommand =  new Runnable() {
                public void run() {
                    try {
                    	//检测是否连接正常,不正常就进行重连
                        if (! isConnected()) {
                            connect();
                        } else {
                            lastConnectedTime = System.currentTimeMillis();
                        }
                    } catch (Throwable t) { 
                        String errorMsg = "client reconnect to "+getUrl().getAddress()+" find error . url: "+ getUrl();
                        // wait registry sync provider list
                        if (System.currentTimeMillis() - lastConnectedTime > shutdown_timeout){
                            if (!reconnect_error_log_flag.get()){
                                reconnect_error_log_flag.set(true);
                                logger.error(errorMsg, t);
                                return ;
                            }
                        }
                        if ( reconnect_count.getAndIncrement() % reconnect_warning_period == 0){
                            logger.warn(errorMsg, t);
                        }
                    }
                }
            };
            reconnectExecutorFuture = reconnectExecutorService.scheduleWithFixedDelay(connectStatusCheckCommand, reconnect, reconnect, TimeUnit.MILLISECONDS);
        }
    }

重连线程属性reconnectExecutorService配置如下:

private static final ScheduledThreadPoolExecutor reconnectExecutorService = new ScheduledThreadPoolExecutor(2, new NamedThreadFactory("DubboClientReconnectTimer", true));
4.连接销毁

基于注册中心通知,服务端断开后销毁
源码见:com.alibaba.dubbo.remoting.transport.AbstractClient#close()

 public void close() {
    	try {
    		if (executor != null) {
    			ExecutorUtil.shutdownNow(executor, 100);
    		}
    	} catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
        try {
            super.close();
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
        try {
        	disconnect();
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
        try {
            doClose();
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
    }

5.Dubbo线程协作设计

1.Dubbo 传输协作线程角色

1.客户端调度线程:用于发起远程方法调用的线程。

2.客户端结果Exchange线程:当远程方法返回response后由该线程填充至指定ResponseFuture,并叫醒等待的调度线程。

3.客户端IO线程:由传输框架实现,用于request 消息流发送、response 消息流读取与解码等操作。

4.服务端IO线程:由传输框架实现,用于request消息流读取与解码 和response编码与发送。

5.业务执行线程:服务端具体执行业务方法的线程,一般就是指执行业务逻辑的线程池。

2.客户端线程协作流程

在这里插入图片描述

1.调度线程

客户端调度线程可以理解为消费者执行远程调用的main线程。

1.调用远程方法

2.对request 进行协议编码

3.发送request 消息至IO线程

4.等待结果的获取,由exchange线程来进行唤醒

2.IO线程
1.读取response流

2.response 解码

3.提交Exchange任务到Exchange线程池

开启io线程的源码见:
com.alibaba.dubbo.remoting.transport.dispatcher.all.AllChannelHandler#received()

public void received(Channel channel, Object message) throws RemotingException {
        ExecutorService cexecutor = getExecutorService();
        try {
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
        } catch (Throwable t) {
            throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);
        }
    }
3.Exchange线程

Exchange线程主要就是用来填充response对象的属性值并唤醒调度线程

1.填写response值 至 ResponseFuture
2.唤醒调度线程,通知其获取结果

Exchange线程执行过程源码见:
com.alibaba.dubbo.remoting.transport.dispatche.ChannelEventRunnable#run()

public void run() {
        switch (state) {
            case CONNECTED:
                try{
                    handler.connected(channel);
                }catch (Exception e) {
                    logger.warn("ChannelEventRunnable handle " + state + " operation error, channel is " + channel, e);
                }
                break;
            case DISCONNECTED:
                try{
                    handler.disconnected(channel);
                }catch (Exception e) {
                    logger.warn("ChannelEventRunnable handle " + state + " operation error, channel is " + channel, e);
                }
                break;
            case SENT:
                try{
                    handler.sent(channel,message);
                }catch (Exception e) {
                    logger.warn("ChannelEventRunnable handle " + state + " operation error, channel is " + channel
                            + ", message is "+ message,e);
                }
                break;
            case RECEIVED:    //对返回结果进行包装处理
                try{
                    handler.received(channel, message);
                }catch (Exception e) {
                    logger.warn("ChannelEventRunnable handle " + state + " operation error, channel is " + channel
                            + ", message is "+ message,e);
                }
                break;
            case CAUGHT:
                try{
                    handler.caught(channel, exception);
                }catch (Exception e) {
                    logger.warn("ChannelEventRunnable handle " + state + " operation error, channel is "+ channel
                            + ", message is: " + message + ", exception is " + exception,e);
                }
                break;
            default:
                logger.warn("unknown state: " + state + ", message is " + message);
        }
    }

3.服务端线程协作

在这里插入图片描述

1.IO线程

1.request 流读取
2.request 解码
3.提交请求到业务线程池
4.将返回结果通过io通道传输到客户端

2.业务线程

1.业务方法执行
2.response 编码
3.回写结果至channel

思考

1.io线程和Exchange线程为何不设置成一个线程?

想法:io线程和Exchange线程其实我觉得设置成一个线程就够了,只要io线程去连接服务端并对结果解码和反序列化等过程,但dubbo为什么要开两种线程。

答:io线程有特定的角色定义,它的角色是处理网络传输。如果在保护其他的业务角色,将增加io线程的实现复杂度。Exchange主要是处理业务结果,并唤醒调度线程的,其包含的特定的业务功能,是属于业务领域。

6.Dubbo注册中心选型

Multicast 注册中心:Multicast 注册中心不需要任何中心节点,只要广播地址,就能进行服务注册和发现,基于网络中组播传输实现。
Zookeeper 注册中心:基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更。
Redis 注册中心:基于 Redis 实现,采用 key/map 存储,key 存储服务名和类型,map 中 key 存储服务 url,value 服务过期时间。基于 Redis 的发布/订阅模式通知数据变更。

Simple 注册中心。

推荐使用 Zookeeper 作为注册中心

7.dubbo通信协议

dubbo都支持什么协议,推荐用哪种?

dubbo://(推荐):
单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步 Hessian 序列化。Dubbo推荐使用dubbo协议。

http://
基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用。

webservice://

基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用

rmi://
采用 JDK 标准的 RMI 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使
用 Java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不
多,可传文件,传输协议 TCP。 多个短连接 TCP 协议传输,同步传输,适用常规的远程服务调用
和 RMI 互操作。在依赖低版本的 Common-Collections 包,Java 序列化存在安全漏洞。

hessian://
集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为
服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列
化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件。

memcached:// 基于 Memcache实现的 RPC 协议。
redis:// 基于 Redis 实现的RPC协议。

dubbo推荐用什么协议:默认使用dubbo协议。

dubbo协议
dubbo协议用来约定通信方式
接口com.alibaba.dubbo.rpc.Protocol源码如下:

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.extension.Adaptive;
import com.alibaba.dubbo.common.extension.SPI;

/**
 * Protocol. (API/SPI, Singleton, ThreadSafe)
 * 
 * @author william.liangf
 */
@SPI("dubbo")
public interface Protocol {
    
    /**
     * 获取缺省端口,当用户没有配置端口时使用。
     * 
     * @return 缺省端口
     */
    int getDefaultPort();

    /**
     * 暴露远程服务:<br>
     * 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
     * 2. export()必须是幂等的,也就是暴露同一个URL的Invoker两次,和暴露一次没有区别。<br>
     * 3. export()传入的Invoker由框架实现并传入,协议不需要关心。<br>
     * 
     * @param <T> 服务的类型
     * @param invoker 服务的执行体
     * @return exporter 暴露服务的引用,用于取消暴露
     * @throws RpcException 当暴露服务出错时抛出,比如端口已占用
     */
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    /**
     * 引用远程服务:<br>
     * 1. 当用户调用refer()所返回的Invoker对象的invoke()方法时,协议需相应执行同URL远端export()传入的Invoker对象的invoke()方法。<br>
     * 2. refer()返回的Invoker由协议实现,协议通常需要在此Invoker中发送远程请求。<br>
     * 3. 当url中有设置check=false时,连接失败不能抛出异常,并内部自动恢复。<br>
     * 
     * @param <T> 服务的类型
     * @param type 服务的类型
     * @param url 远程服务的URL地址
     * @return invoker 服务的本地代理
     * @throws RpcException 当连接服务提供方失败时抛出
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    /**
     * 释放协议:<br>
     * 1. 取消该协议所有已经暴露和引用的服务。<br>
     * 2. 释放协议所占用的所有资源,比如连接和端口。<br>
     * 3. 协议在释放后,依然能暴露和引用新的服务。<br>
     */
    void destroy();

}

8.dubbo SPI机制

DUBBO SPI:
1、对 Dubbo 进行扩展,不需要改动 Dubbo 的源码
2、延迟加载,可以一次只加载自己想要加载的扩展实现。
3、增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
4、Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。

DK SPI:
JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展很耗时,但也没用上,很浪费资源。所以只希望加载某个的实现,就不现实了

自己实现一个spi扩展

核心思路:
在src/main/resources目录下创建META-INF/dubbo文件夹,这样dubbo在接口被调用的时候,就好执行里面的文件。
在这里插入图片描述

三、高可用架构设计

1.服务提供者实现失效踢出原理?

服务失效踢出基于zookeeper的临时节点原理。

3.1 集群容错

Dubbo的几种集群容错方案
在这里插入图片描述

1.读写选型

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

原因:写操作不用Failover的原因是如果发生超时等失败可能会导致服务端重复写了多次。

思考:即使使用Failfast可以避免重复提交,但是做好的办法应该是服务端自己去控制幂等。
如设置数据库设置唯一索引或者使用分布式锁。

幂等:指同样的请求同样的参数多次请求后所产生的最终效果是一致的

2.Failover失败重试

dubbo 默认失败重试两次,消息端可以自己配置重试次数

设置方式支持如下两种方式设置,优先级由低至高:

<!-- 
Failover 失败自动切换 retries="1" 切换次数
Failfast 快速失败
Failsafe 勿略失败
Failback 失败重试,5秒后仅重试一次
Forking 并行调用 forks="2" 最大并行数
Broadcast 广播调用
-->
<dubbo:service interface="..." cluster="broadcast" />
<dubbo:reference interface="..." cluster="broadcast"/ >

源码解析

Failover的源码分析可见com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker类

 private static final Logger logger = LoggerFactory.getLogger(FailoverClusterInvoker.class);

    public FailoverClusterInvoker(Directory<T> directory) {
        super(directory);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    	List<Invoker<T>> copyinvokers = invokers;
    	checkInvokers(copyinvokers, invocation);
        int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
        if (len <= 0) {
            len = 1;
        }
        // retry loop.
        RpcException le = null; // last exception.
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);
        for (int i = 0; i < len; i++) {
        	//重试时,进行重新选择,避免重试时invoker列表已发生变化.
        	//注意:如果列表发生了变化,那么invoked判断会失效,因为invoker示例已经改变
        	if (i > 0) {
        		checkWhetherDestroyed();
        		copyinvokers = list(invocation);
        		//重新检查一下
        		checkInvokers(copyinvokers, invocation);
        	}
            Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
            invoked.add(invoker);
            RpcContext.getContext().setInvokers((List)invoked);
            try {
                Result result = invoker.invoke(invocation);
                if (le != null && logger.isWarnEnabled()) {
                    logger.warn("Although retry the method " + invocation.getMethodName()
                            + " in the service " + getInterface().getName()
                            + " was successful by the provider " + invoker.getUrl().getAddress()
                            + ", but there have been failed providers " + providers 
                            + " (" + providers.size() + "/" + copyinvokers.size()
                            + ") from the registry " + directory.getUrl().getAddress()
                            + " on the consumer " + NetUtils.getLocalHost()
                            + " using the dubbo version " + Version.getVersion() + ". Last error is: "
                            + le.getMessage(), le);
                }
                return result;
            } catch (RpcException e) {
                if (e.isBiz()) { // biz exception.
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
        throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method "
                + invocation.getMethodName() + " in the service " + getInterface().getName() 
                + ". Tried " + len + " times of the providers " + providers 
                + " (" + providers.size() + "/" + copyinvokers.size() 
                + ") from the registry " + directory.getUrl().getAddress()
                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
                + Version.getVersion() + ". Last error is: "
                + (le != null ? le.getMessage() : ""), le != null && le.getCause() != null ? le.getCause() : le);
    }

3.2 超时机制

1.在dubbo的provider和consumer的配置文件中,如果都配置了timeout的超时时间,dubbo默认以consumer中配置的时间为准

2.dubbo超时时间默认是1000ms

3.3 负载均衡

Dubbo有哪几种负载均衡策略,默认是哪种?
在这里插入图片描述

设置方式支持如下四种方式设置,优先级由低至高

<!-- 服务端级别-->
<dubbo:service interface="..." loadbalance="roundrobin" />
<!-- 客户端级别-->
<dubbo:reference interface="..." loadbalance="roundrobin" />
<!-- 服务端方法级别-->
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
<!-- 客户端方法级别-->
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>

源码分析:

负载均衡的源码在dubbo-cluster模块下的com.alibaba.dubbo.rpc.cluster.loadbalance包下面,主要看其抽象类AbstractLoadBalance:
com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance源码如下:

package com.alibaba.dubbo.rpc.cluster.loadbalance;

import java.util.List;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.cluster.LoadBalance;

/**
 * AbstractLoadBalance
 * 
 * @author william.liangf
 */
public abstract class AbstractLoadBalance implements LoadBalance {

    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        if (invokers == null || invokers.size() == 0)
            return null;
        if (invokers.size() == 1)
            return invokers.get(0);
        return doSelect(invokers, url, invocation);
    }

    protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);

    protected int getWeight(Invoker<?> invoker, Invocation invocation) {
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
        if (weight > 0) {
	        long timestamp = invoker.getUrl().getParameter(Constants.TIMESTAMP_KEY, 0L);
	    	if (timestamp > 0L) {
	    		int uptime = (int) (System.currentTimeMillis() - timestamp);
	    		int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
	    		if (uptime > 0 && uptime < warmup) {
	    			weight = calculateWarmupWeight(uptime, warmup, weight);
	    		}
	    	}
        }
    	return weight;
    }
    
    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
    	int ww = (int) ( (float) uptime / ( (float) warmup / (float) weight ) );
    	return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }

可见负载均衡还使用到模板设计模式,自己写代码时可用借鉴

3.4 异步调用

1.Dubbo服务之间的调用是阻塞的吗?

默认是同步等待结果阻塞的,支持异步调用。

异步主要体现在io上,待io获得结果后在进行通知唤醒等待结果的阻塞线程。

即消费端发起调用后,不用等待结果,其他线程还可以继续发起调用

Dubbo 是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象。

2.Dubbo 异步调用流程

异步调用是指发起远程调用之后获取结果的方式。

1.同步等待结果返回
2.异步等待结果返回
3.不需要返回结果

Dubbo 中关于异步等待结果返回的实现流程如下图:

在这里插入图片描述

实现原理主要可参看:wait和notify方式的线程的线程协作。

异步调用配置:

<dubbo:reference id="asyncDemoService"
interface="com.tuling.teach.service.async.AsyncDemoService">
<!-- 异步调async:true 异步调用 false 同步调用-->
<dubbo:method name="sayHello1" async="true"/>
<dubbo:method name="sayHello2" async="false"/>
<!-- return="false" 不需要返回结果,直接返回-->
<dubbo:method name="notReturn" return="false"/>
</dubbo:reference>

异步调用消费端使用

@Reference(version = "1.0.0")
private GreetingsService greetingsService;
 
public void asyncCallExample() {
    ResponseFuture future = greetingsService.sayHelloAsync("World");
    future.setCallback(new ResponseCallback() {
        @Override
        public void done(Object response) {
            System.out.println("Result: " + response);
        }
 
        @Override
        public void caught(Throwable exception) {
            exception.printStackTrace();
        }
    });
}

在上述代码中,sayHelloAsync 方法用于异步调用,ResponseCallback 用于处理异步调用的结果。当调用结果可用时,done 方法被调用,并将结果作为参数传递。如果调用过程中发生异常,caught 方法将被调用,并将异常作为参数传递。

请注意,你需要在Dubbo的配置中开启异步调用的支持,例如在 dubbo:consumer 标签中设置 async=“true”,或者在服务提供方和消费方通过API设置 ReferenceConfig.setAsync(true)。

3.5 服务降级

dubbo提供了一些服务降级措施,当服务提供端某一个非关键的服务出错时候,dubbo可以对消费端的调用进行降级,这样服务消费端就避免了在去调用出错的服务提供端,而是使用自定义的返回值直接在在本地返回。

dubbo服务的降级方式,有两种方式:

  • mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null
    值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响
  • mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null
    值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。

@see 官方文档http://dubbo.apache.org/zh-cn/docs/user/demos/service-downgrade.html

3.6 注册中心高可用

1. zookeeper宕机之后,服务之间是否还可以相互调用?

zookeeper注册中心宕机,还可以消费dubbo暴露的服务,因为启动dubbo时,消费者会从zk拉取注册的生产者的地址接口等数据,缓存在本地。

2.dubbo-zookeeper的容错方案

  • 注册中心宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能在对应的服务节点注册新服务
  • 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
  • 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯,但不能注册新服务
  • 服务提供者无状态,即为集群部署,任意一台宕掉后,不影响使用
  • 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复。

3 直连服务提供者

消费端服务配置直连提供者服务时,可以参考监控台如下:
在这里插入图片描述
这里的IP是服务提供者的IP而不是注册中心zk的IP。
那么消费端就可以做如下配置:

    <dubbo:reference interface="com.haixue.jjxt.api.statistics.StChanceDashboardService"
                     id="stChanceDashboardService" version="${jjxt.service.version}" group="jjxt" 
                     url="dubbo://192.168.1.12:20906"/>

zipkin实现分布式服务追踪
待续???

参考资料

1.dubbo官方文档:http://dubbo.apache.org/zh-cn/docs/user/preface/architecture.html
2.最近项目用到Dubbo框架,临时抱佛脚分享一下共探讨。https://www.cnblogs.com/Javame/p/3632473.html
3.Dubbo学习(九)https://www.cnblogs.com/aspirant/p/9003255.html
4.常见容错机制:failover、failfast、failback、failsafe https://blog.csdn.net/u011305680/article/details/79730646

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值