Java分布式框架之Dubbo

分布式与微服务

 1、传统架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。

存在的问题:

  • 代码耦合,开发维护困难
  • 无法对不同模块进行针对性优化
  • 无法水平扩展
  • 单点容错率低,并发能力差

2、系统拆分

当系统不断的迭代,访问量逐渐增大单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分:

优点:

  • 系统拆分实现了流量分担,解决了并发问题
  • 可以针对不同模块进行优化
  • 方便水平扩展,负载均衡,容错率提高

缺点:

  • 系统间相互独立,会有很多重复开发工作,影响开发效率

3、分布式项目的诞生

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。

优点:

  • 将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率

缺点:

  • 系统间调用关系错综复杂,难以维护

4、服务治理(SOA)和微服务

服务治理:

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。

以前出现了什么问题?

  • 服务越来越多,需要管理每个服务的地址
  • 调用关系错综复杂,难以理清依赖关系
  • 服务过多,服务状态难以管理,无法根据服务情况动态管理

服务治理要做什么?

  • 服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
  • 服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
  • 动态监控服务状态监控报告,人为控制服务状态

缺点:

  • 服务间会有依赖关系,一旦某个环节出错会影响较大
  • 服务关系复杂,运维、测试部署困难,不符合DevOps思想

微服务:

在SOA思想的基础上,再把项目细分,将其‘微小独立化’就成了微服务项目。微服务(或微服务架构)是一种云原生架构方法,其中单个应用程序由许多松散耦合且可独立部署的较小组件或服务组成。

微服务的特点:

  • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责。
  • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
  • 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
  • 自治:自治是说服务间互相独立,互不干扰。
  • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
  • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉。
  • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动端开发不同接口。
  • 数据库分离:每个服务都使用自己的数据源部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护。

微服务和分布式项目

  • 分布式系统:是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。
  • 微服务(Microservices) :是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模组化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通讯。

(个人理解所谓“分布式系统”主要是偏向系统部署的一种方式,从以前把整个项目部署到一个Tomcat转变为不同的模块部署分别部署到其他服务,在水平方向对项目进行优化;而“微服务”主要指项目架构的垂直拆分,将原本整体的架构尽可能的进行模块拆分,分别管理。)

RPC

本文主角-Dubbo是在服务治理(SOA)思想下的一种RPC架构,而什么是RPC呢?

什么是RPC

RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。简言之,RPC使得程序能够像访问本地系统资源一样,去访问远端系统资源。比较关键的一些方面包括:通讯协议、序列化、资源(接口)描述、服务框架、性能、语言支持等。

RPC架构

1、客户端(Client):服务调用方(服务消费者)
2、客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端
3、服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理
4、服务端(Server):服务的真正提供者

RPC调用顺序

  1. 服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务;
  2. 客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体;
  3. 客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端;
  4. 服务端存根(server stub)收到消息后进行解码(反序列化操作);
  5. 服务端存根(server stub)根据解码结果调用本地的服务进行相关处理;
  6. 本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub);
  7. 服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方;
  8. 客户端存根(client stub)接收到消息,并进行解码(反序列化);
  9. 服务消费方得到最终结果;

而RPC框架的实现目标则是将上面的第2-8步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务。

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、Nacos、Consul 、Etcd。一般使用ZooKeeper提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。

Dubbo

什么是Dubbo

官网:https://cn.dubbo.apache.org/zh-cn/index.html

Apache Dubbo 最初在 2008 年由 Alibaba 捐献开源,很快成为了国内开源服务框架选型的事实标准框架 ,得到了各行各业的广泛应用。在 2017 年,Dubbo 正式捐献到 Apache 软件基金会并成为 Apache 顶级项目。

Dubbo的优势

1、开箱即用
易用性高,如 Java 版本的面向接口代理特性能实现本地透明调用。
功能丰富,基于原生库或轻量扩展即可实现绝大多数的微服务治理能力。

2、面向超大规模微服务集群设计
极致性能,高性能的 RPC 通信协议设计与实现。
横向可扩展,轻松支持百万规模集群实例的地址发现与流量治理。

3、高度可扩展
调用过程中对流量及协议的拦截扩展,如 Filter、Router、LB 等。
微服务治理组件扩展,如 Registry、Config Center、Metadata Center 等。

4、企业级微服务治理能力
国内公有云厂商支持的事实标准服务框架。
多年企业实践经验考验,参考用户实践案例。

Dubbo架构

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

Dubbo核心配置

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

协议:

协议是RPC的核心,它规范了数据在网络中的传输内容和格式。除必须的请求、响应数据外,通常还会包含额外控制数据,如单次请求的序列化方式、超时时间、压缩方式和鉴权信息等。

协议的内容包含三部分:

  1. 数据交换格式: 定义 RPC 的请求和响应对象在网络传输中的字节流内容,也叫作序列化方式
  2. 协议结构: 定义包含字段列表和各字段语义以及不同字段的排列方式
  3. 协议通过定义规则、格式和语义来约定数据如何在网络间传输。一次成功的 RPC 需要通信的两端都能够按照协议约定进行网络字节流的读写和对象转换。如果两端对使用的协议不能达成一致,就会出现鸡同鸭讲,无法满足远程通信的需求。

Dubbo支持有多少种

默认协议(Dubbo协议)

Dubbo 缺省协议采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。

Q1:为什么要消费者比提供者个数多?

因 dubbo 协议采用单一长连接,假设网络为千兆网卡 1024Mbit=128MByte,根据测试经验数据每条连接最多只能压满 7MByte(不同的环境可能不一样,供参考),理论上 1 个服务提供者需要 20 个服务消费者才能压满网卡。

Q2:为什么不能传大包?

因 dubbo 协议采用单一长连接,如果每次请求的数据包大小为 500KByte,假设网络为千兆网卡 1024Mbit=128MByte,每条连接最大 7MByte (不同的环境可能不一样),单个服务提供者的 TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。单个消费者调用单个服务提供者的 TPS (每秒处理事务数)最大为:7MByte / 500KByte = 14。如果能接受,可以考虑使用,否则网络将成为瓶颈。

Q3:为什么采用异步单一长连接?

因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如 Morgan 的提供者只有 6 台提供者,却有上百台消费者,每天有 1.5 亿次调用,如果采用常规的 hessian 服务,服务提供者很容易就被压跨,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步 IO,复用线程池,防止 C10K 问题。

Dubbo支持的序列化方式

  1. Dubbo 序列化:阿里尚未开发成熟的高效 Java 序列化实现,阿里不建议在生产环境使用它。
  2. Hessian2 序列化:Hessian 是一种跨语言的高效二进制序列化方式。但这里实际不是原生的 Hessian2 序列化,而是阿里修改过的Hessian Lite,它是 Dubbo RPC 默认启用的序列化方式。
  3. Json 序列化:目前有两种实现,一种是采用的阿里的 Fastjson 库,另一种是采用 Dubbo中自己实现的简单 Json库,但其实现都不是特别成熟,而且 Json 这种文本序列化性能一般不如上面两种二进制序列化。
  4. Java 序列化:主要是采用 JDK 自带的 Java 序列化实现,性能很不理想。

Dubbo有哪几种负载均衡策略,默认是哪种

  1. random loadbalance:按权重设置随机概率(默认);
  2. roundrobin loadbalance:轮寻,按照公约后权重设置轮训比例;
  3. lastactive loadbalance:最少活跃调用数,若相同则随机;
  4. consistenthash loadbalance:一致性hash,相同参数的请求总是发送到同一提供者。

Dubbo有哪几种集群容错方案,默认是哪种

(1)Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。(默认)
(2)Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
(3)Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
(4)Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操 作。
(5)Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
(6)Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。

Dubbo SPI和Java SPI

Java SPI:SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。Meta-InF/services 文件夹下创建一个接口全类名的文件,在文件中配置该接口的实现类,然后通过关键类“ServiceLoader”就可以调用:ServiceLoader.load()。

Dubbo SPI是Java SPI的一种改良版本。也是在META-INF文件夹下面创建指定接口问题,然后在文件里以key-value形式配置实现类,然后通过程序中指定key实现灵活调用不同实现类,关键类:ExtensionLoader

注册中心挂了可以继续通信吗

可以的!启动dubbo时,消费者会从zk拉取注册的生产者的地址接口等数据,缓存在本地。

  • 注册中心对等集群,任意一台宕掉后,会自动切换到另一台
  • 服务提供者无状态,任一台 宕机后,不影响使用
  • 服务提供者全部宕机,服务消费者会无法使用,并无限次重连等待服务者恢复

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

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

开启异步:在配置文件中开启:

<dubbo:reference id="helloService" interface="com.demo.service.HelloService">

<!--添加异步调用方式,注解方式不支持-->
<dubbo:method name="sayHello" async="true" />

</dubbo:reference>

Dubbo怎么实现异步变同步的

调用服务的时候会有一个关键类:AsyncToSyncInvoker,然后在AsyncRpcResult类的get(long timeout, TimeUnit unit)方法调用CompletableFuture的timedGet()获取Response,如果Response为空则循环一直等待直到获取返回结果。

AsyncToSyncInvoker

public Result invoke(Invocation invocation) throws RpcException {
        Result asyncResult = this.invoker.invoke(invocation);
        try {
            if (InvokeMode.SYNC == ((RpcInvocation)invocation).getInvokeMode()) {
                asyncResult.get(2147483647L, TimeUnit.MILLISECONDS);
            }
            return asyncResult;
        } catch (InterruptedException var5) {
            throw new RpcException("Interrupted unexpectedly while waiting for remote result to return!  method: " + invocation.getMethodName() + ", provider: " + this.getUrl() + ", cause: " + var5.getMessage(), var5);
        } catch (ExecutionException var6) {
            Throwable t = var6.getCause();
            if (t instanceof TimeoutException) {
                throw new RpcException(2, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + this.getUrl() + ", cause: " + var6.getMessage(), var6);
            } else if (t instanceof RemotingException) {
                throw new RpcException(1, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + this.getUrl() + ", cause: " + var6.getMessage(), var6);
            } else {
                throw new RpcException(0, "Fail to invoke remote method: " + invocation.getMethodName() + ", provider: " + this.getUrl() + ", cause: " + var6.getMessage(), var6);
            }
        } catch (Throwable var7) {
            throw new RpcException(var7.getMessage(), var7);
        }
    }

AsyncRpcResult

    public Result get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        if (this.executor != null && this.executor instanceof ThreadlessExecutor) {
            ThreadlessExecutor threadlessExecutor = (ThreadlessExecutor)this.executor;
            threadlessExecutor.waitAndDrain();
        }
        return (Result)this.responseFuture.get(timeout, unit);
    }

CompletableFuture

    public T get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        Object r;
        long nanos = unit.toNanos(timeout);
        return reportGet((r = result) == null ? timedGet(nanos) : r);
    }
    private Object timedGet(long nanos) throws TimeoutException {
        if (Thread.interrupted())
            return null;
        if (nanos <= 0L)
            throw new TimeoutException();
        long d = System.nanoTime() + nanos;
        Signaller q = new Signaller(true, nanos, d == 0L ? 1L : d); // avoid 0
        boolean queued = false;
        Object r;
        while ((r = result) == null) {//一直循环,直到result不为空
            if (!queued)
                queued = tryPushStack(q);
            else if (q.interruptControl < 0 || q.nanos <= 0L) {
                q.thread = null;
                cleanStack();
                if (q.interruptControl < 0)
                    return null;
                throw new TimeoutException();
            }
            else if (q.thread != null && result == null) {
                try {
                    ForkJoinPool.managedBlock(q);
                } catch (InterruptedException ie) {
                    q.interruptControl = -1;
                }
            }
        }
        if (q.interruptControl < 0)
            r = null;
        q.thread = null;
        postComplete();
        return r;
    }

Dubbo服务上线怎么兼容旧版本

在配置文件中通过version 绑定不同接口
<dubbo:reference id="peopleService1" interface="com.demo.PeopleService" version="1.0.0" />
<dubbo:reference id="peopleService2" interface="com.demo.PeopleService" version="2.0.0" />

当一个服务接口有多种实现时怎么做

当一个接口有多种实现时,可以用 group 属性来分组,服务提供方和消费方都指定同一个 group 即可。

Dubbo可以直连服务吗

可以!在指定接口配置
@Reference(check = false,url = "dubbo://127.0.0.1:10086")

posted @ 2023-06-16 21:43  请别耽误我写BUG  阅读(77)  评论(0编辑  收藏  举报