文章目录

一、初识Dubbo

  Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
  Dubbo提供服务自动注册、自动发 现等高效服务治理方案, 可以和Spring框架无缝集成。

  • 什么是RPC
      RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务A如果想要调用服务B中的某个方法该怎么办呢?使用HTTP请求当然可以,但是可能会比较麻烦。 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. 服务消费方得到最终结果。
  • 为什么要用Dubbo
      Dubbo的诞生和SOA分布式架构的流行有着莫大的关系。SOA面向服务的架构(Service Oriented Architecture),也就是把工程按照业务逻辑拆分成服务层、表现层两个工程。服务层中包含业务逻辑,只需要对外提供服务即可。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。SOA架构中有两个主要角色:服务提供者(Provider)和服务使用者(Consumer)。
      如果你要开发分布式程序,可以从Dubbo提供的下面四点特性来说为什么要用Dubbo:
  1. 负载均衡——同一个服务部署在不同的机器时该调用那一台机器上的服务。
  2. 服务调用链路生成——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo可以为我们解决服务之间互相是如何调用的。
  3. 服务访问压力以及时长统计、资源调度和治理——基于访问压力实时管理集群容量,提高集群利用率。
  4. 服务降级——某个服务挂掉之后调用备用服务。
  • 什么是分布式
      分布式或者说SOA分布式重要的就是面向服务,分布式就是我们把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。比如电商系统可以简单地拆分成订单系统、商品系统、登录系统等等,拆分之后的每个服务可以部署在不同的机器上,如果某一个服务的访问量比较大的话也可以将这个服务同时部署在多台机器上。

  Dubbo具备的功能有:服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出。
  Dubbo核心部分包括:

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

  Dubbo能做什么?

  1. 透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API入侵。
  2. 软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器,降低成本。
  3. 服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能平滑添加或删除服务提供者。
  4. Dubbo采用全Spring配置方式,透明化接入应用,对应用没有任何API侵入,只需用Spring加载Dubbo配置即可,Dubbo基于Spring的Schema扩展进行加载。

  Dubbo默认使用Netty通信框架。因此Dubbo不需要Web容器,如果硬要用Web容器,只会增加复杂性,也浪费资源。

  • 服务治理

      概括起来,需要服务治理的几个原因:

过多的服务URL配置困难;
负载均衡分配节点压力过大的情况下也需要部署集群;
服务依赖混乱,启动顺序不清晰;
过多服务导致性能指标分析难度较大,需要监控。

1.1 Dubbo特性

  • 1、连通性
      1)注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。
      2)监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示。
      3)服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销。
      4)服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销。
      5)注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外。
      6)注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者。
      7)注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表。
      8)注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。
  • 2、健状性
      1)监控中心宕掉不影响使用,只是丢失部分采样数据。
      2)数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务。
      3)注册中心对等集群,任意一台宕掉后,将自动切换到另一台。
      4)注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯。
      5)服务提供者无状态,任意一台宕掉后,不影响使用。
      6)服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复。
  • 3、伸缩性
      1)注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心。
      2)服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者。

1.2 Dubbo与Spring Cloud

  Dubbo是SOA时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。而Spring Cloud诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了Spirng、Spirng Boot的优势之上,两个框架在开始目标就不一致,Dubbo定位服务治理、Spirng Cloud是一个生态。

  • 1、通信方式不同
      Dubbo使用的是RPC通信,而Spring Cloud使用的是HTTP RESTFUL方式。
  • 2、组成部分不同
DubboSpring Cloud
服务注册中心ZookeeperSpring Cloud Netflix Eureka
服务调用方式RPCREST API
服务网关Spring Cloud Netflix Zuul
断路器不完善Spring Cloud Netflix Hystrix
分布式配置Spring CLoud Config
服务跟踪Spring Cloud Sleuth
消息总线Spring CLoud Bus
数据流Spring Cloud Stream
批量任务Spring Cloud Task

  Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC通信。
  SpringCloud是基于Http协议+Rest接口调用远程过程的通信,相对来说,Http请求会有更大的报文(因为HTTP协议包含大量的请求头、响应头信息),占的带宽也会更多。但是REST相比RPC更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖。

二、Dubbo架构设计

2.1 从角色理解


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

  工作流程:

第一步:provider 向注册中心去注册。
第二步:consumer 从注册中心订阅服务,注册中心会通知 consumer 注册好的服务。
第三步:consumer 调用 provider。
第四步:consumer 和 provider 都异步通知监控中心。

  Dubbo服务器注册与发现的流程:

  • 1、服务容器负责启动,加载,运行服务提供者。
  • 2、服务提供者在启动时,向注册中心注册自己提供的服务(发送本机IP、端口、应用信息和提供服务信息发送至注册中心存储)。
  • 3、服务消费者在启动时,向注册中心订阅自己所需的服务(并发送应用信息、所求服务信息至注册中心)。
  • 4、注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  • 5、服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。服务提供者状态变更会实时通知注册中心、在由注册中心实时推送至服务消费者。
  • 6、服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
  • 角色间通信事项
      注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。
      监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示。
      注册中心、服务提供者、服务消费者三者之间均为长连接,监控中心除外。
      注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者。
      注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表。
      注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。
      服务提供者无状态,任意一台宕掉后,不影响使用。
      服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复。
  • 角色设计的好处
      Consumer与Provider解偶,双方都可以横向增减节点数。
      注册中心对本身可做对等集群,可动态增减节点,并且任意一台宕掉后,将自动切换到另一台。
      去中心化,双方不直接依懒注册中心,即使注册中心全部宕机短时间内也不会影响服务的调用。
      服务提供者无状态,任意一台宕掉后,不影响使用。

2.2 从分层理解


  图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service和Config层为API,其它各层均为SPI(Service Provider Interface,SPI机制的作用就是服务发现,也就是说,我们有一些服务,然后通过SPI机制,就能让这些服务被需要的人所使用)。

  • 1、服务接口层(Service)
      给服务提供者和消费者来实现的。
  • 2、配置层(Config)
      主要是对dubbo进行各种配置的。
  • 3、服务代理层(Proxy)
      无论是 consumer 还是 provider,dubbo 都会给你生成代理,代理之间进行网络通信。
  • 4、服务注册层(Registry)
      封装服务地址的注册与发现。
  • 5、集群层(Cluster)
      封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务。
  • 6、监控层(Monitor)
      对rpc接口的调用次数和调用时间进行监控。
  • 7、远程调用层(Protocol)
      封装rpc调用。
  • 8、信息交换层(Exchange)
      封装请求响应模式,同步转异步。
  • 9、网络传输层(Transport)
      抽象mina和netty为统一接口。
  • 10、数据序列化层(Serialize)
      数据传输的序列化和反序列化。

2.3 线程模型

  Dubbo默认的底层网络通讯使用的是Netty,服务提供方NettyServer使用两级线程池,其中EventLoopGroup(boss)主要用来接受客户端的链接请求,并把接受的请求分发给EventLoopGroup(worker) 来处理,boss和worker线程组我们称之为IO线程。
  如果事件处理的逻辑能迅速完成,并且不会发起新的IO请求,比如只是在内存中记个标识,则直接在IO线程上处理更快,因为减少了线程池调度。
  但如果事件处理逻辑较慢,或者需要发起新的IO请求,比如需要查询数据库,则必须派发到线程池,否则IO线程阻塞,将导致不能接收其它请求。

  需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景:

	<dubbo:protocol name="dubbo" dispatcher="all" 
		threadpool="fixed" threads="100" />

  根据请求的消息类被IO线程处理还是被业务线程池处理,Dubbo提供了下面几种线程模型:
  all:所有消息都派发到线程池,包括请求、响应、连接事件、断开事件、心跳等。
  direct:所有消息都不派发到线程池,全部在IO线程上直接执行。
  message:只有请求响应消息派发到线程池,其它连接断开事件、心跳等消息,直接在IO线程上执行。
  execution:只请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在IO线程上执行。
  connection:在IO线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。

  Dubbo提供的线程池策略,扩展接口ThreadPool的SPI实现有如下几种:
  fixed:固定大小线程池,启动时建立线程,不关闭,一直持有(缺省)。
  cached:缓存线程池,空闲一分钟自动删除,需要时重建。
  limited:可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。

2.4 所用协议

  Dubbo默认使用dubbo协议。支持的协议有:dubbo://(推荐)、rmi://、hessian://、http://、webservice://、thrift://、memcached://、redis://、rest://。
  Dubbo的设计目的是为了满足高并发小数据量的rpc调用,在大数据量下的性能表现并不好,建议使用rmi或http协议。
  这些协议对应的默认端口号:

协议端口
dubbo20880
http80
hessian80
rmi80
2.4.1 协议比较
  • dubbo(默认)
      单一长连接和NIO异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。
      Dubbo协议适用常见:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者(并发量很高),尽量不要用Dubbo协议传输大文件或超大字符串。
      为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就 100 个连接。然后后面直接基于长连接 NIO 异步通信,可以支撑高并发请求。长连接,通俗点说,就是建立连接过后可以持续发送请求,无须再建立连接。

而短连接,每次要发送请求之前,需要先重新建立一次连接。

  Dubbo协议适用场景:常规远程服务方法调用。
  Dubbo协议实现:

连接个数:单连接;
连接方式:长连接;
传输协议:TCP;
传输方式:NIO异步传输;
序列化:Hessian二进制序列化。

  • rmi
      采用JDK标准的rmi协议实现,传输参数和返回参数对象需要实现Serializable接口,使用Java标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件。在依赖低版本的Common-Collections包,Java序列化存在安全漏洞。
      连接个数:多连接;
      连接方式:短连接;
      传输协议:TCP;
      传输方式:同步传输;
      序列化:Java 标准二进制序列化;
      适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多可传文件,一般较少用。
      适用场景:常规远程服务方法调用,与原生RMI服务互操作。
  • webservice
      基于CXF的frontend-simple和transports-http实现。
      基于WebService的远程调用协议。
      连接个数:多连接。
      连接方式:短连接。
      传输协议:HTTP。
      传输方式:同步传输。
      序列化:SOAP文本序列化。
      适用场景:系统集成,跨语言调用。
  • http
      采用Spring的HttpInvoker实现。
      基于http表单的远程调用协议。
      连接个数:多连接。
      连接方式:短连接。
      传输协议:HTTP。
      传输方式:同步传输。
      序列化:表单序列化(JSON)。
      适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。
      适用场景:需同时给应用程序和浏览器JS使用的服务。
  • hessian
      Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo缺省内嵌Jetty作为服务器实现。
      基于 Hessian 的远程调用协议。
      连接个数:多连接。
      连接方式:短连接。
      传输协议:HTTP。
      传输方式:同步传输。
      序列化:Hessian二进制序列化。
      适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件,一般较少用。
      适用场景:页面传输,文件传输,或与原生hessian服务互操作。
  • memcache
      基于memcached实现的RPC协议。
  • redis
      基于redis实现的RPC协议。
2.4.2 dubbo(默认)协议特点

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

  • 特性
      缺省协议,使用基于mina1.1.7和hessian3.2.1的tbremoting交互。
      连接个数:单连接。
      连接方式:长连接。
      传输协议:TCP。
      传输方式:NIO异步传输。
      序列化:Hessian二进制序列化。
      适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。
      适用场景:常规远程服务方法调用。
  • 约束
      参数及返回值需实现Serializable接口。
      参数及返回值不能自定义实现List、Map、Number、Date、Calendar等接口,只能用JDK自带的实现,因为hessian会做特殊处理,自定义实现类中的属性值都会丢失。
      Hessian序列化,只传成员属性值和值的类型,不传方法或静态变量,兼容情况。
数据通讯情况结果
A -> B类A多一种属性(或者说类B少一种属性)不抛异常,A多的那个属性的值,B没有,其他正常
A -> B枚举A多一种雷剧(或者说B少一种枚举),A使用多出来的枚举进行传输抛异常
A -> B枚举A多一种雷剧(或者说B少一种枚举),A不使用多出来的枚举进行传输不抛异常,B正常接收数据
A -> BA和B的属性名相同,但类型不同抛异常
A -> B序列化ID不同正常传输

  接口增加方法,对客户端无影响,如果该方法不是客户端需要的,客户端不需要重新部署。
  输入参数和结果集中增加属性,对客户端无影响,如果客户端并不需要新属性,不用重新部署。
  输入参数和结果集属性名变化,对客户端序列化无影响,但是如果客户端不重新部署,不管输入还是输出,属性名变化的属性值是获取不到的。
  总结:服务器端和客户端对领域对象并不需要完全一致,而是按照最大匹配原则。

2.4.3 协议相关问题
  • 1、为什么要消费者比提供者个数多?
      因Dubbo协议采用单一长连接,假设网络为千兆网卡 ,根据测试经验数据每条连接最多只能压满7MByte(不同的环境可能不一样,供参考),理论上1个服务提供者需要20个服务消费者才能压满网卡。
  • 2、为什么不能传大包?
      因Dubbo协议采用单一长连接,如果每次请求的数据包大小为500KByte,假设网络为千兆网卡 ,每条连接最大7MByte(不同的环境可能不一样,供参考),单个服务提供者的TPS(每秒处理事务数)最大为:128MByte/500KByte=262。单个消费者调用单个服务提供者的 TPS(每秒处理事务数)最大为:7MByte/500KByte=14。如果能接受,可以考虑使用,否则网络将成为瓶颈。
  • 3、为什么采用异步单一长连接?
      因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如Morgan的提供者只有6台提供者,却有上百台消费者,每天有1.5亿次调用,如果采用常规的hessian服务,服务提供者很容易就被压跨,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步IO,复用线程池,防止C10K问题。
      网络服务在处理数以万计的客户端连接时,往往出现效率底下甚至完全瘫痪,这被成为C10K问题。(C10K = connection 10 kilo问题)。k表示kilo,即1000。比如:kilometer(千米),kilogram(千克)。
  • 4、Dubbo支持服务多协议吗?
      Dubbo允许配置多协议,在不同服务上支持不不同协议或者同一服务上同时支持多种协议。

2.5 架构特性

2.5.1 集群容错


  各节点的关系:

  1. Invoker是Provider的一个可调用Service的抽象, Invoker封装了Provider地址及Service接口信息。
  2. Directory代表多个Invoker,可以把它看成List<Invoker>,但与List不同的是,它的值可能是动态变化的,比如注册中心推送变更。
  3. Cluster将Directory中的多个Invoker伪装成一个Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个.
  4. Router负责从多个Invoker中按路由规则选出子集,比如读写分离,应用隔离等。
  5. LoadBalance负责从多个Invoker中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选。

  Dubbo的默认集群容错方案是Failover Cluster
  Dubbo的集群容错方案有以下几种:

  • 1、Failover Cluster
      失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过retries="2"来设置重试次数(不含第一次)。示例:
	<!--2种写法-->
	<dubbo:service retries="2" cluster="failover"/>
	<dubbo:reference retries="2" cluster="failover"/>

  cluster="failover"可以不用写,因为默认就是failover。

  • 2、Failfast Cluster
      快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。配置示例:
    <!--2种写法-->
	<dubbo:service cluster="failfast" />
	<dubbo:reference cluster="failfast" />

  cluster="failfast"和 把 cluster=“failover”、retries="0"是一样的效果,retries="0"就是不重试。

  • 3、Failsafe Cluster
      失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。配置示例:
	<!--2种写法-->
	<dubbo:service cluster="failsafe" />
	<dubbo:reference cluster="failsafe" />
  • 4、Failback Cluster
      失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。配置示例:
	<!--2种写法-->
	<dubbo:service cluster="failback" />
	<dubbo:reference cluster="failback" />
  • 5、Forking Cluster
      并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。配置示例:
   <!--2种写法-->
	<dubbo:service cluster="forking" forks="2"/>
	<dubbo:reference cluster="forking" forks="2"/>
  • 6、Broadcast Cluster
      广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。可以在不同的级别配置。
  • 不同的配置级别
    <!--服务端服务级别-->
	<dubbo:service interface="..." loadbalance="roundrobin" />
    <!--客户端服务级别-->
   <dubbo:reference interface="..." loadbalance="roundrobin" />
    <!--服务端方法级别-->
	<dubbo:service interface="..."> <dubbo:method name="..." loadbalance="roundrobin" />
    <!--客户端方法级别-->
	<dubbo:reference interface="..."> <dubbo:method name="..." loadbalance="roundrobin" />
2.5.2 负载均衡

  Dubbo负载均衡在客户端。
  Dubbo提供了多种均衡策略,默认采用Random LoadBalance策略,可以自行扩展负载均衡策略。

  • 1、Random LoadBalance(默认,基于权重的随机负载均衡机制)
      随机,按权重设置随机概率。
      调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重(权重可以在dubbo控制台配置)。
  • 2、RoundRobin LoadBalance(均匀的轮询负载均衡机制)
      轮循,按公约后的权重设置轮训比率。
      存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
  • 3、LeastActive LoadBalance
      最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
      使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
      每个服务维护一个活跃数计数器。当A机器开始处理请求,该计数器加1,此时A还未处理完成。若处理完毕则计数器减1。而B机器接受到请求后很快处理完毕。那么A、B的活跃数分别是1、0。当又产生了一个新的请求,则选择B机器去执行(B活跃数最小),这样使慢的机器A收到少的请求。
  • 4、ConsistentHash LoadBalance
      一致性Hash,相同参数的请求总是发到同一提供者。
      当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
      缺省只对第一个参数Hash,如果要修改,配置示例:
	<dubbo:parameter key="hash.arguments" value="0,1" />

  缺省用160份虚拟节点,如果要修改,配置示例:

	<dubbo:parameter key="hash.nodes" value="320" />
  • 2种负责均衡策略配置方式
     【1、xml配置方式】
    <!--服务端服务级别-->
    <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>

 【2、注解配置方式】
  消费方基于基于注解的服务级别配置方式:

    @Reference(loadbalance = "roundrobin")
    HelloService helloService;
  • 负载均衡的相关类
      dubbo的负载均衡策略,主体向外暴露出来是一个接口,名字叫做loadBlace,位于com.alibaba.dubbo.rpc.cluster包下,很明显根据包名就可以看出它是用来管理集群的。
      1、RandomLoadBalance类。首先遍历每个提供服务的机器,获取每个服务的权重,然后累加权重值,判断每个服务的提供者权重是否相同,如果每个调用者的权重不相同,并且每个权重大于0,那么就会根据权重的总值生成一个随机数,再用这个随机数,根据调用者的数量每次减去调用者的权重,直到计算出当前的服务提供者随机数小于0,就选择那个提供者!另外,如果每个机器的权重的都相同,那么权重就不会参与计算,直接选择随机算法生成的某一个选择,完全随机。
      2、RoundRobinLoadBlance类(轮询调用)。轮询调用的过程主要是维护了局部变量的一个LinkdesHashMap(有顺序的Map)去存储调用者和权重值的对应关系,然后遍历每个调用者,把调用者和当前大于0的权重值放进去,再累加权重值。还有一个全局变量的map,找到第一个服务调用者,首先是找到每个服务的key值和method,这里可以理解为标识第一个调用者的唯一key,然后再给它对应的值保证原子性的+1(AtomicPositiveInteger是原子的),再对这个值取模总权重,再每次对其权重值-1,知道它取模与总权重值等于0就选择该调用者,可以称之为"降权取模"(只是一种的计算层面,而不是真正降权)。总结:轮询调用并不是简单的一个接着一个依次调用,它是根据权重的值进行循环的。
      3、LeastActiveLoadBlance类(最少活跃数调用法)。这个方法的主要作用根据服务的提供者的运行状态去选择服务器,主要的思路就是遍历每个调用者,然后获取每个服务器的运行状态,如果当前运行的运行状态小于最小的状态-1,把它保存在leastIndexs中的第一个位置,并且认定所有的调用者权重都相同,然后直接返回那个调用者(这里的逻辑是:找到最少活跃数(在代码层反应就是:active的值))。如果计算出的权重值和最少的权重值相同,那么把它保存在leastIndexs数组里面,累加权重值,如果当前的权重值不等于初始值firstWeight,那么就认定不是所有的调用者的权重不同。然后再遍历lestIndexs,取权重累加值的随机数生成权重偏移量,在累减它,到它小于0的时候返回那个调用者。如果这些都不符合,就从leastIndexs随机选一个index,返回那个调用者。
      4、ConsistentHashLoadBalance(一致性Hash算法)。doSelect方法进行选择。一致性Hash负载均衡涉及到两个主要的配置参数为hash.arguments与hash.nodes:当进行调用时候根据调用方法的哪几个参数生成key,并根据key来通过一致性hash算法来选择调用节点。
      可以看到除了一致性Hash算法,其它都是根据权重进行计算的。

三、Dubbo功能

3.1 服务分组

  当一个接口有多种实现时,可以用group区分。
  不同的组:

	<dubbo:service group="feedback" interface="com.xxx.IndexService" />
	<dubbo:service group="member" interface="com.xxx.IndexService" />

3.2 多版本

  当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
  可以按照以下的步骤进行版本迁移:

  1. 在低压力时间段,先升级一半提供者为新版本;
  2. 再将所有消费者升级为新版本;
  3. 然后将剩下的一半提供者升级为新版本。
	<!--老版本服务提供者配置示例-->
	<dubbo:service interface="com.foo.BarService" version="1.0.0" />

    <!--新版本服务提供者示例-->
	<dubbo:service interface="com.foo.BarService" version="2.0.0" />

    <!--老版本服务消费者示例-->
	<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />

    <!--新版本服务消费者示例-->
	<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />

3.3 事件通知

  在调用之前、调用之后、出现异常时,会触发oninvoke、onreturn、onthrow三个事件,可以配置当事件发生时,通知哪个类的哪个方法 。
  服务消费者Callback配置示例:

   <bean id ="demoCallback" 
      class = "com.alibaba.dubbo.callback.implicit.NofifyImpl" />
   <dubbo:reference id="demoService" 
	  interface="com.alibaba.dubbo.callback.implicit.IDemoService" 
	  version="1.0.0" 
	  group="cn" >
     <dubbo:method name="get" 
         async="true" 
	     onreturn = "demoCallback.onreturn" 
	     onthrow="demoCallback.onthrow" />
   </dubbo:reference>

   async表示结果是否马上返回,onreturn表示是否需要回调。
  存在以下几种组合情况 :

异步回调模式: async=true onreturn=“xxx”
同步回调模式: async=false onreturn=“xxx”
异步无回调 : async=true
同步无回调 : async=false

3.4 本地伪装

  本地伪装通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过Mock数据返回授权失败。
  在Spring配置文件中按以下方式配置:

    <!--2种方式-->
   <dubbo:service interface="com.foo.BarService" mock="true" />
   <dubbo:service interface="com.foo.BarService" mock="com.foo.BarServiceMock" />

  如果服务的消费方经常需要try-catch捕获异常,如:

	Offer offer = null;
	try {
		offer = offerService.findOffer(offerId);
	} catch (RpcException e) {
		logger.error(e);
	}

  可以考虑改为Mock实现,并在Mock实现中return null。如果只是想简单的忽略异常,在2.0.11以上版本可用:

	<dubbo:service interface="com.foo.BarService" mock="return null" />

3.5 令牌验证

  通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者,可以防止消费者绕过注册中心访问提供者.另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者。图示:

  可以全局设置开启令牌验证:

	<!--随机token令牌,使用UUID生成-->
	<dubbo:provider interface="com.foo.BarService" token="true" />
	<!--固定token令牌,相当于密码-->
	<dubbo:provider interface="com.foo.BarService" token="123456" />

  也可在服务级别设置:

	<!--随机token令牌,使用UUID生成-->
	<dubbo:service interface="com.foo.BarService" token="true" />
	<!--固定token令牌,相当于密码-->
	<dubbo:service interface="com.foo.BarService" token="123456" />

  还可在协议级别设置:

	<!--随机token令牌,使用UUID生成-->
	<dubbo:protocol name="dubbo" token="true" />
	<!--固定token令牌,相当于密码-->
	<dubbo:protocol name="dubbo" token="123456" />

3.6 优雅停机

  Dubbo是通过JDK的ShutdownHook来完成优雅停机的,所以如果用户使用kill -9 PID等强制关闭指令,是不会执行优雅停机的,只有通过kill PID时,才会执行。

  • 服务提供方
      停止时,先标记为不接收新请求,新请求过来时直接报错,让客户端重试其它机器。然后,检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超时,则强制关闭。
  • 服务消费方
      停止时,不再发起新的调用请求,所有新的调用在客户端即报错。
      然后,检测有没有请求的响应还没有返回,等待响应返回,除非超时,则强制关闭。
  • 设置方式
      设置优雅停机超时时间,缺省超时时间是10秒,如果超时则强制关闭。示例:
    <dubbo:application ...>
      <dubbo:parameter key="shutdown.timeout" value="60000" /> <!-- 单位毫秒 -->
   </dubbo:application>

3.7 注册中心

  Dubbo默认采用Zookeeper注册中心

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

四、Dubbo配置

4.1 Dubbo配置

  • Dubbo的核心配置
配置配置说明解释
dubbo:service服务配置用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心
dubbo:reference引用配置用于创建一个远程服务代理,一个引用可以指向多个注册中心
dubbo:protocol协议配置用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受
dubbo:application应用配置用于配置当前应用信息,不管该应用是提供者还是消费者
dubbo:module模块配置用于配置当前模块信息,可选
dubbo:registry注册中心配置用于配置连接注册中心相关信息
dubbo:monitor监控中心配置用于配置连接监控中心相关信息,可选
dubbo:provider提供方配置当 ProtocolC onfig 和 ServiceCo nfig 某属 性没有配置时,采用此缺省值,可选
dubbo:consumer消费方配置当 Reference Config 某属性没有配置时,采用此缺省值,可选
dubbo:method方法配置用于 ServiceCo nfig 和 Reference Config 指 定方法级的配置信息
dubbo:argument参数配置用于指定方法参数配置
  • Dubbo的配置来源

JVM System Properties,-D参数;
Externalized Configuration,外部化配置;
ServiceConfig、ReferenceConfig等编程接口采集的配置;
本地配置文件dubbo.properties。

  • 绕过注册中心如何配置
      xml配置示例:
	<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" 
		url="dubbo://localhost:20890"/>

  -D配置示例:

	java -D com.alibaba.xxx.XxxService=dubbo://localhost:20890

  properties配置示例:

	java -D dubbo.resolve.file=xxx.properties
	com.alibaba.xxx.XxxService=dubbo://localhost:20890

4.2 XML配置

  provider.xml 示例:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
   xmlns="http://www.springframework.org/schema/beans"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
   http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
  <dubbo:application name="demo-provider"/>
  <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
  <dubbo:protocol name="dubbo" port="20890"/>
  <bean id="demoService" class="org.apache.dubbo.samples.basic.impl.DemoServiceImpl"/>
  <dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>

  consumer.xml示例:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
   xmlns="http://www.springframework.org/schema/beans"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
   http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
  <dubbo:application name="demo-consumer"/>
  <dubbo:registry group="aaa" address="zookeeper://127.0.0.1:2181"/>
  <dubbo:reference id="demoService" check="false"
interface="org.apache.dubbo.samples.basic.api.DemoService"/>
</beans>

  不同粒度配置的覆盖关系,以timeout为例,下图显示了配置的查找顺序。其它retries、loadbalance、actives等类似:

方法级优先,接口级次之,全局配置再次之。
如果级别一样,则消费方优先,提供方次之。

  建议由服务提供方设置超时,因为一个方法需要执行多长时间,服务提供方更清楚,如果一个消费方同时引用多个服务,就不需要关心每个服务的超时设置。

  • 属性配置
      如果你的应用足够简单,例如,不需要多注册中心或多协议,并且需要在spring容器中共享配置,那么,我们可以直接使用
    dubbo.properties 作为默认配置。
    Dubbo可以自动加载classpath根目录下的dubbo.properties,但是你同样可以使用JVM参数来指定路径: -Ddubbo.properties.file=xxx.properties
  • 映射规则
      可以将xml的tag名和属性名组合起来,用‘.’分隔。每行一个属性。

  dubbo.application.name=foo 相当于 <dubbo:application name="foo" />
  dubbo.registry.address=10.20.153.10:9090 相当于 <dubbo:registry address="10.20.153.10:9090" />

  如果在xml配置中有超过一个的tag,那么你可以使用‘id’进行区分。如果你不指定id,它将作用于所有tag。

  dubbo.protocol.rmi.port=1099 相当于 <dubbo:protocol id="rmi" name="rmi" port="1099" />
  dubbo.registry.china.address=10.20.153.10:9090 相当于 <dubbo:registry id="china" address="10.20.153.10:9090"/>

一个典型的dubbo.properties配置样例:

dubbo.application.name=foo
dubbo.application.owner=bar
dubbo.registry.address=10.20.153.10:9090
  • 重写与优先级

      优先级从高到低:

JVM -D参数,当你部署或者启动应用时,它可以轻易地重写配置,比如,改变dubbo协议端口;
XML, XML中的当前配置会重写dubbo.properties中的;
Properties,默认配置,仅仅作用于以上两者没有配置时。

  1:如果在classpath下有超过一个dubbo.properties文件,比如,两个jar包都各自包含了dubbo.properties,dubbo将随机选择一个加载,并且打印错误日志。
  2:如果 id 没有在 protocol 中配置,将使用 name 作为默认属性。

  • 并发请求上限
      服务端并发限流参数:executes。客户端并发限流参数:actives。
      示例1:限制com.foo.BarService的每个方法,服务器端并发执行(或占用线程池线程数)不能超过10个:
    <dubbo:service interface="com.foo.BarService" executes="10" />

  示例2:限制com.foo.BarService的sayHello方法,服务器端并发执行(或占用线程池线程数)不能超过10个:

   <dubbo:service interface="com.foo.BarService">  
      <dubbo:method name="sayHello" executes="10" />
   </dubbo:service>

  示例3:限制com.foo.BarService的每个方法,客户端并发执行(或占用连接的请求数)不能超过10个:

    <!--2种写法-->
    <dubbo:service interface="com.foo.BarService" actives="10" />
    <dubbo:reference interface="com.foo.BarService" actives="10" />

  示例4:限制com.foo.BarService的sayHello方法,每客户端并发执行(或占用连接的请求数)不能超过10个:

    <!--2种写法-->
   <dubbo:service interface="com.foo.BarService">  
       <dubbo:method name="sayHello" actives="10" />
   </dubbo:service>

   <dubbo:reference interface="com.foo.BarService">  
       <dubbo:method name="sayHello" actives="10" />
   </dubbo:service>

  如果 <dubbo:service><dubbo:reference> 都配了actives, <dubbo:reference> 优先。

4.3 注解配置

  • 服务提供方
      Service 注解暴露服务:
@Service
public class AnnotationServiceImpl implements AnnotationService {
    @Override
    public String sayHello(String name) {
       return "annotation: hello, " + name;
    }
}

  增加应用共享配置:

# dubbo-provider.properties
dubbo.application.name=annotation-provider
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880

  指定Spring扫描路径:

@Configuration
@EnableDubbo(scanBasePackages = "org.apache.dubbo.samples.simple.annotation.impl")
@PropertySource("classpath:/spring/dubbo-provider.properties")
static public class ProviderConfiguration {
  
}
  • 服务消费方
      Reference 注解引用服务:
@Component("annotationAction")
public class AnnotationAction {
    @Reference
    private AnnotationService annotationService;
 
    public String doSayHello(String name) {
        return annotationService.sayHello(name);
   }
}

  增加应用共享配置:

# dubbo-consumer.properties
dubbo.application.name=annotation-consumer
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.consumer.timeout=3000

  指定Spring扫描路径:

@Configuration
@EnableDubbo(scanBasePackages = "org.apache.dubbo.samples.simple.annotation.action")
@PropertySource("classpath:/spring/dubbo-consumer.properties")
@ComponentScan(value = {"org.apache.dubbo.samples.simple.annotation.action"})
static public class ConsumerConfiguration {
}

  调用服务:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConsumerConfiguration.class);
    context.start();
    final AnnotationAction annotationAction = (AnnotationAction) context.getBean("annotationAction");
    String hello = annotationAction.doSayHello("world");
}

五、相关问题

5.1 Dubbo支持哪些序列化方式

  dubbo 支持 hession、Java 二进制序列化、json、SOAP 文本序列化多种序列化协议。hessian 是其默认的序列化协议
  Hessian 的对象序列化机制有 8 种原始类型:

原始二进制数据
boolean
64-bit date(64 位毫秒值的日期)
64-bit double
32-bit int
64-bit long
null
UTF-8 编码的 string

  另外还包括 3 种递归类型:

list for lists and arrays
map for maps and dictionaries
object for objects

  还有一种特殊的类型:ref:用来表示对共享对象的引用。

5.2 Dubbo超时时间怎样设置

  两种方式:

  • 1、服务提供者端设置超时时间。在Dubbo的用户文档中,推荐如果能在服务端多配置就尽量多配置,因为服务提供者比消费者更清楚自己提供的服务特性。
  • 2、服务消费者端设置超时时间。如果在消费者端设置了超时时间,以消费者端为主,即优先级更高。因为服务调用方设置超时时间控制性更灵活。如果消费方超时,服务端线程不会定制,会产生警告。

5.3 服务调用超时问题怎么解决

  Dubbo在调用服务不成功时,默认是会重试两次的。这样在服务端的处理时间超过了设定的超时时间时,就会有重复请求,比如在发邮件时,可能就会发出多份重复邮件,执行注册请求时,就会插入多条重复的注册数据,那么怎么解决超时问题呢?如下对于核心的服务中心,去除dubbo超时重试机制,并重新评估设置超时时间。业务处理代码必须放在服务端,客户端只做参数验证和服务调用,不涉及业务流程处理。全局配置示例:

   <dubbo:provider delay="-1" timeout="6000" retries="0"/>

  当然Dubbo的重试机制其实是非常好的QOS(Quality of Service,服务质量,指一个网络能够利用各种基础技术,为指定的网络通信提供更好的服务能力,是网络的一种安全机制, 是用来解决网络延迟和阻塞等问题的一种技术)保证,它的路由机制,是会帮你把超时的请求路由到其他机器上,而不是本机尝试,所以dubbo的重试机器也能一定程度的保证服务的质量。但是请一定要综合线上的访问情况,给出综合的评估。

5.4 Dubbo在安全机制方面的做法

  Dubbo通过Token令牌防止用户绕过注册中心直连,然后在注册中心上管理授权。Dubbo还提供服务黑白名单,来控制服务所允许的调用方。

5.5 Dubbo支持服务降级吗

  Dubbo2.2.0以上版本支持。通过dubbo:reference中,设置 mock=“return null”。mock的值也可以修改为true,然后再跟接口同一个路径下实现一个Mock类,命
名规则是 “接口名称+Mock” 后缀。然后在Mock类里实现自己的降级逻辑。

5.6 服务提供者能实现失效踢出是什么原理

  服务失效踢出基于Zookeeper的临时节点原理。(服务机器会在zk上注册一个临时节点,服务失效则临时节点被删除)。

5.7 服务读写推荐的容错策略是怎样的

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

5.8 Dubbo的管理控制台能做什么

  管理控制台主要包含:路由规则,动态配置,服务降级,访问控制,权重调整,负载均衡等管理功能。

5.9 服务调用超时会怎么样

  dubbo 在调用服务不成功时,默认是会重试两次。

5.10 Dubbo服务暴露的过程

  Dubbo会在 Spring实例化完bean之后,在刷新容器最后一步发布ContextRefreshEvent事件的时候,通知实现了ApplicationListener的ServiceBean类进行回调onApplicationEvent事件方法,Dubbo会在这个方法中调用ServiceBean父类ServiceConfig的export方法,而该方法真正实现了服务的(异步或者非异步)发布。

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

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

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

  可以用版本号(version)过渡,多个不同版本的服务注册到注册中心,版本号不同的服务相互间不引用。这个和服务分组的概念有一点类似。

5.13 Dubbo可以对结果进行缓存吗

  可以,Dubbo提供了声明式缓存,用于加速热门数据的访问速度,以减少用户加缓存的工作量。示例:

	<dubbo:reference cache="true" />

  其实比普通的配置文件就多了一个标签cache=“true”。

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

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

5.15 注册了多个同一样的服务,如果测试指定的某一个服务呢

  可以配置环境点对点直连,绕过注册中心,将以服务接口为单位,忽略注册中心的提供者列列表。

5.16 Dubbo和Dubbox之间的区别

  Dubbox是继Dubbo停止维护后,当当网基于Dubbo做的一个扩展项目,如加了服务可Restful调用,更新了开源组件等。
  Dubbox和Dubbo本质上没有区别,Dubbox扩展了Dubbo而已,以下扩展出来的功能:

  1. 支持 REST 风格远程调用(HTTP + JSON/XML);
  2. 支持基于Kryo和FST的Java高效序列化实现;
  3. 支持基于Jackson的JSON序列化;
  4. 支持基于嵌入式Tomcat的HTTP remoting体系;
  5. 升级Spring至3.x;
  6. 升级ZooKeeper客户端;
  7. 支持完全基于Java代码的Dubbo配置。

5.17 Dubbo对JDK版本的最低要求

  JDK1.6。

5.18 IO线程池默认大小

  CPU个数+1。

5.19 Dubbo用到了哪些设计模式?

  • 1、工厂模式
      Provider在export服务时,会调用ServiceConfig的export方法。ServiceConfig中有个字段:
	private static final Protocol protocol = 
		ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

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

  • 2、装饰器模式
      Dubbo在启动和调用阶段都大量使用了装饰器模式。以Provider提供的调用链为例,具体的调用链代码是在ProtocolFilterWrapper的buildInvokerChain完成的,具体是将注解中含有group=provider的Filter实现,按照order排序,最后的调用顺序是:EchoFilter -> ClassLoaderFilter -> GenericFilter -> ContextFilter ->ExecuteLimitFilter -> TraceFilter -> TimeoutFilter -> MonitorFilter ->ExceptionFilter。
      更确切地说,这里是装饰器和责任链模式的混合使用。例如,EchoFilter的作用是判断是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现。而像ClassLoaderFilter则只是在主功能上添加了功能,更改当前线程的ClassLoader,这是典型的装饰器模式。
  • 3、观察者模式
      Dubbo的Provider启动时,需要与注册中心交互,先注册自己的服务,再订阅自己的服务,订阅时,采用了观察者模式,开启一个listener。注册中心会每5秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个notify消息,provider接受到notify消息后,即运行NotifyListener的notify方法,执行监听器方法。
  • 4、动态代理模式
      Dubbo扩展JDK SPI的类ExtensionLoader的Adaptive实现是典型的动态代理实现。Dubbo需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是ExtensionLoader的createAdaptiveExtensionClassCode 方法。代理类的主要逻辑是,获取URL参数中指定参数的值作为获取实现类的key。

5.20 Dubbo有哪几种配置方式

  1)Spring配置方式;2)Java API配置方式。

5.21 Dubbo启动时如果依赖的服务不可用会怎样

  Dubbo缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止Spring初始化完成,默认check=“true”,可以通过check="false"关闭检查。

5.22 在Provider上可以配置的Consumer端的属性有哪些?

  1)timeout:方法调用超时。
  2)retries:失败重试次数,默认重试2次。
  3)loadbalance:负载均衡算法,默认随机。
  4)actives 消费者端,最大并发调用限制。

5.23 Dubbo中Zookeeper做注册中心,如果注册中心集群都挂掉,发布者和订阅者之间还能通信吗

  可以的,启动Dubbo时,消费者会从zookeeper拉取注册的生产者的地址接口等数据,缓存在本地。
  每次调用时,按照本地存储的地址进行调用。
  注册中心对等集群,任意一台宕机后,将会切换到另一台;注册中心全部宕机后,服务的提供者和消费者仍能通过本地缓存通讯。服务提供者无状态,任一台 宕机后,不影响使用;服务提供者全部宕机,服务消费者会无法使用,并无限次重连等待服务者恢复;
  挂掉是不要紧的,但前提是你没有增加新的服务,如果你要调用新的服务,则是不能办到的。

5.24 zookeeper宕机与dubbo直连的情况

  在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用的本地缓存进行通讯,这只是dubbo健壮性的一种体现。
  dubbo的健壮性表现:

  1. 监控中心宕掉不影响使用,只是丢失部分采样数据
  2. 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
  3. 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
  4. 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
  5. 服务提供者无状态,任意一台宕掉后,不影响使用
  6. 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复

  注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。所以,我们可以完全可以绕过注册中心——采用 dubbo 直连 ,即在服务消费方配置服务提供方的位置信息。
  xml配置方式:

<dubbo:reference id="userService" 
    interface="com.test.service.UserService"
    url="dubbo://localhost:20880" />

  注解方式:

   @Reference(url = "127.0.0.1:20880") 
   HelloService helloService;

5.25 为什么要用Dubbo

  内部使用了 Netty、Zookeeper,保证了高性能高可用性。使用 Dubbo 可以将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,可用于提高业务复用灵活扩展,使前端应用能更快速的响应多变的市场需求。
  最重要是,分布式架构可以承受更大规模的并发流量。

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

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

5.27 Dubbo默认使用什么注册中心,还有别的选择吗

  推荐使用 Zookeeper 作为注册中心,还有 Redis、Multicast、Simple 注册中心,但不推荐。

5.28 Dubbo 核心的配置有哪些

配置配置说明解释
dubbo:service服务配置用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心
dubbo:reference引用配置用于创建一个远程服务代理,一个引用可以指向多个注册中心
dubbo:protocol协议配置用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受
dubbo:application应用配置用于配置当前应用信息,不管该应用是提供者还是消费者
dubbo:module模块配置用于配置当前模块信息,可选
dubbo:registry注册中心配置用于配置连接注册中心相关信息
dubbo:monitor监控中心配置用于配置连接监控中心相关信息,可选
dubbo:provider提供方配置当 ProtocolC onfig 和 ServiceCo nfig 某属 性没有配置时,采用此缺省值,可选
dubbo:consumer消费方配置当 Reference Config 某属性没有配置时,采用此缺省值,可选
dubbo:method方法配置用于 ServiceCo nfig 和 Reference Config 指 定方法级的配置信息
dubbo:argument参数配置用于指定方法参数配置

5.29 Dubbo推荐使用什么序列化框架

  默认使用Hessian序列化,还有Duddo、FastJson、Java自带序列化。hessian是一个采用二进制格式传输的服务框架,相对传统soap web service,更轻量,更快速。
  http的协议约定了数据传输的方式,hessian也无法改变太多:
  1、 hessian中client与server的交互,基于http-post方式。
  2、hessian将辅助信息,封装在http header中,比如“授权token”等,我们可以基于http-header来封装关于“安全校验”“meta数据”等。hessian提供了简单的”校验”机制。
  3、对于hessian的交互核心数据,比如“调用的方法”和参数列表信息,将通过post请求的body体直接发送,格式为字节流。
  4、对于hessian的server端响应数据,将在response中通过字节流的方式直接输出。
  hessian的协议本身并不复杂,在此不再赘言;所谓协议(protocol)就是约束数据的格式,client按照协议将请求信息序列化成字节序列发送给server端,server端根据协议,将数据反序列化成“对象”,然后执行指定的方法,并将方法的返回值再次按照协议序列化成字节流,响应给client,client按照协议将字节流反序列话成”对象”。

5.30 Dubbo有哪几种集群容错方案

集群容错方案说明
Failover Cluster失败自动切换,自动重试其他服务器(默认)
Failfast Cluster快速失败,立即报错,只发起一次调用
Failsafe Cluster失败安全,出现异常时,直接忽略
Failback Cluster失败自动恢复,记录失败请求,定时重发
Forking Cluster并行调用多个服务器,只要一个成功即返回
Broadcast Cluster广播逐个调用所有提供者,任意一个报错即报错

5.31 Dubbo telnet命令能做什么

  dubbo服务发布之后,我们可以利用telnet命令进行调试、管理。Dubbo2.0.5以上版本服务提供端口支持telnet命令。
  连接服务:

# 键入回车进入 Dubbo 命令模式
telnet localhost 8090

  查看服务列表:

dubbo>ls
com.test.TestService
dubbo>ls com.test.TestService
create
delete
query

   ls : 显示服务列表。
   ls -l : 显示服务详细信息列表。
   ls XxxService:显示服务的方法列表。
   ls -l XxxService:显示服务的方法详细信息列表。

5.32 Dubbo必须依赖的包有哪些

  Dubbo必须依赖JDK,其他为可选。

5.33 Dubbo服务暴露的过程。

  Dubbo会在Spring实例化完Bean之后,在刷新容器最后一步发布 ContextRefreshEvent事件的时候,通知实现了ApplicationListener的ServiceBean类进行回调onApplicationEvent事件方法,Dubbo会在这个方法中调用ServiceBean父类ServiceConfig的export方法,而该方法真正实现了服务的(异步或者非异步)发布。

5.34 在使用过程中可能会遇到哪些问题

  • 1、 同时配置了XML和properties文件,则properties中的配置无效
      只有XML没有配置时,properties才生效。
  • 2、为了方便开发测试,线下有一个所有服务可用的注册中心,这时,如果有一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行
      解决:让服务提供者开发方,只订阅服务,而不注册正在开发的服务,通过直连测试正在开发的服务。设置 dubbo:registry 标签的 register 属性为 false。
  • 3、Spring2.x初始化死锁问题
      在Spring解析到dubbo:service时,就已经向外暴露了服务,而Spring还在接着初始化其他Bean。如果这时有请求进来,并且服务的实现类里有调用applicationContext.getBean()的用法。getBean线程和Spring初始化线程的锁的顺序不一样,导致了线程死锁,不能提供服务,启动不了。
      解决:不要在服务的实现类中使用applicationContext.getBean();,如果不想依赖配置顺序,可以将dubbo:provider的deplay属性设置为-1,使dubbo在容器初始化完成后再暴露服务。
  • 4、服务注册不上
      检查dubbo的jar包有没有在classpath中,以及有没有重复的jar包。
      检查暴露服务的Spring配置有没有加载。
      在服务提供者机器上测试与注册中心的网络是否通。
  • 5、出现 RpcException: No provider available for remote service 异常,表示没有可用的服务提供者
      检查连接的注册中心是否正确。
      到注册中心查看相应的服务提供者是否存在。
      检查服务提供者是否正常运行。
  • 6、出现” 消息发送失败” 异常
      通常是接口方法的传入传出参数未实现Serializable接口。
  • 7、Dubbo的设计目的是为了满足高并发小数据量的rpc调用,在大数据量下的性能表现并不好,建议使用rmi或http协议

5.39 Dubbo SPI和Java SPI区别

  • JDK SPI
      JDK标准的SPI会一次性加载所有的扩展实现,如果有的扩展吃实话很耗时,但也没用上,很浪费资源。所以只希望加载某个的实现,就不现实了。
  • DUBBO SPI
      1,对Dubbo进行扩展,不需要改动Dubbo的源码。
      2,延迟加载,可以一次只加载自己想要加载的扩展实现。
      3,增加了对扩展点IOC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。
      4,Dubbo的扩展机制能很好的支持第三方IoC容器,默认支持Spring Bean。

5.40 Dubbo支持分布式事务吗

  目前暂时不支持,可与通过tcc-transaction框架实现,tcc-transaction是开源的TCC补偿性分布式事务框架。
  TCC-Transaction通过Dubbo隐式传参的功能,避免自己对业务代码的入侵。

5.41 Dubbo的使用场景有哪些

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

5.42 Dubbo核心功能有哪些

  Remoting:网络通信框架,提供对多种NIO框架抽象封装,包括“同步转异 步”和“请求-响应”模式的信息交换方式。
  Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支 持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
  Registry:服务注册,基于注册中心目录服务,使服务消费方能动态的查找服务 提供方,使地址透明,使服务提供方可以平滑增加或减少机器。

5.43 Dubbo的SPI

  spi:service provider interface,比如你有个接口,现在这个接口有 3 个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要 spi 了,需要根据指定的配置或者是默认的配置,去找到对应的实现类加载进来,然后用这个实现类的实例对象。你通过配置 接口 A = 实现 A2 ,那么在系统实际运行的时候,会加载你的配置,用实现 A2 实例化一个对象来提供服务。
  spi 机制一般用在哪儿?插件扩展的场景,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,插到你的开源框架里面,从而扩展某个功能,这个时候 spi 思想就用上了。

  • Java spi 思想的体现
      比如说 jdbc。Java 定义了一套 jdbc 的接口,但是 Java 并没有提供 jdbc 的实现类。实际上项目跑的时候,要使用 jdbc 接口的哪些实现类呢?一般来说,我们要根据自己使用的数据库,比如 mysql,你就将 mysql-jdbc-connector.jar 引入进来;oracle,你就将oracle-jdbc-connector.jar 引入进来。
  • dubbo 的 spi
      dubbo 也用了 spi 思想,不过没有用 jdk 的 spi 机制,是自己实现的一套 spi 机制。
Protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getA

  Protocol 接口,在系统运行的时候,,dubbo 会判断一下应该选用这个 Protocol 接口的哪个实现类来实例化对象来使用。它会去找一个你配置的 Protocol,将你配置的 Protocol 实现类,加载到 jvm 中来,然后实例化对象,就用你的那个 Protocol 实现类就可以了。
  上面那行代码就是 dubbo 里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现。

5.44 如何基于dubbo进行服务治理、服务降级、失败重试以及超时重试

  • 服务治理
      1、调用链路自动生成。基于 dubbo 做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将各个服务之间的依赖关系和调用链路生成出来,做成一张图,显示出来。

      2、服务访问压力以及时长统计。自动统计各个接口和服务之间的调用次数以及访问延时。
      1)一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次。
      2)第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次。
      3、其它:

服务分层(避免循环依赖)。
调用链路失败监控和报警。
服务鉴权。
每个服务的可用性的监控(接口调用成功率?几个 9?99.99%,99.9%,99%)。

  • 服务降级
      比如说服务 A 调用服务 B,结果服务 B 挂掉了,服务 A 重试几次调用服务 B,还是不行,那么直接降级,走一个备用的逻辑,给用户返回响应。
      调用接口失败的时候,可以通过 mock 统一返回 null。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+ Mock ” 后缀。然后在 Mock 类里实现自己的降级逻辑。
public class HelloServiceMock implements HelloService {
    public void sayHello() {
        // 降级逻辑
    }
}
  • 失败重试和超时重试
      所谓失败重试,就是 consumer 调用 provider 要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。
<dubbo: id ="xxxx" interface ="xx" check ="true" async ="false" timeout="6000" retries="0"/>

  如某个服务的接口,要耗费 5s,你这边不能干等着,你这边配置了 timeout 之后,我等待 2s,还没返回,我直接就撤了,不能干等你。
  timeout :一般设置为 200ms ,我们认为不能超过 200ms 还没返回。

5.45 分布式服务接口的幂等性如何设计

  没有通用的一个方法,这个应该结合业务来保证幂等性。
  幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款、不能多插入一条数据、不能将统计值多加了 1。这就是幂等性。
  保证幂等性主要是三点:

  • 1、对于每个请求必须有一个唯一的标识,举个栗子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次。
  • 2、每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录个状态啥的,比如支付之前记录一条这个订单的支付流水。
  • 3、每次接收请求需要进行判断,判断之前是否处理过。比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。

5.46 分布式服务接口请求的顺序性如何保证

  其实分布式系统接口的调用顺序,也是个问题,一般来说是不用保证顺序的。因为一旦引入顺序性保障,比如使用分布式锁,会导致系统复杂度上升,而且会带来效率低下,热点数据压力过大等问题。
  如果就是要保证顺序,可以用 Dubbo 的一致性 hash 负载均衡策略,将比如某一个订单 id 对应的请求都给分发到某个机器上去,接着就是在那个机器上,因为可能还是多线程并发执行的,你可能得立即将某个订单 id 对应的请求扔一个内存队列里去,强制排队,这样来确保他们的顺序性。


  但是这样引发的后续问题就很多,比如说要是某个订单对应的请求特别多。因此还是不建议保证顺序性,可以将有顺序性的多个操作合并到一个操作里。

六、Dubbo使用注意事项

6.1 最佳实践

  • 分包
      建议将服务接口,服务模型,服务异常等均放在API包中,因为服务模型及异常也是API的一部分,同时,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。
      如果需要,也可以考虑在API包中放置一份Spring的引用配置,这样使用方,只需在Spring加载过程中引用此配置即可,配置建议放在模块的包目录下,以免冲突,如: com/alibaba/china/xxx/dubbo-reference.xml 。
  • 粒度
      服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo暂未提供分布式事务支持。
      服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。
      不建议使用过于抽象的通用接口,如: Map query(Map) ,这样的接口没有明确语义,会给后期维护带来不便。
  • 版本
      每个接口都应定义版本号,为后续不兼容升级提供可能,如:<dubbo:service interface="com.xxx.XxxService" version="1.0" />
      建议使用两位版本号,因为第三位版本号通常表示兼容升级,只有不兼容时才需要变更服务版本。
      当不兼容时,先升级一半提供者为新版本,再将消费者全部升为新版本,然后将剩下的一半提供者升为新版本。
  • 兼容性
      服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需通过变更版本号升级。
  • 枚举值
      如果是完备集,可以用Enum ,比如: ENABLE , DISABLE 。
      如果是业务种类,以后明显会有类型增加,不建议用Enum,可以用String代替。
      如果是在返回值中用了Enum,并新增了Enum值,建议先升级服务消费方,这样服务提供方不会返回新值。
      如果是在传入参数中用了Enum,并新增了Enum值,建议先升级服务提供方,这样服务消费方不会传入新值。
  • 序列化
      服务参数及返回值建议使用POJO对象,即通过setter、getter方法表示属性的对象。
      服务参数及返回值不建议使用接口,因为数据模型抽象的意义不大,并且序列化需要接口实现类的元信息,并不能起到隐藏实现的意图。
      服务参数及返回值都必需是byValue的,而不能是byReference的,消费方和提供方的参数或返回值引用并不是同一个,只是值相同,Dubbo不支持引用远程对象。
  • 异常
      建议使用异常汇报错误,而不是返回错误码,异常信息能携带更多信息,以及语义更友好。如果担心性能问题,在必要时,可以通过override掉异常类的fillInStackTrace()方法为空方法,使其不拷贝栈信息。
      查询方法不建议抛出checked异常,否则调用方在查询时将过多的try…catch,并且不能进行有效处理。
      服务提供方不应将DAO或SQL等异常抛给消费方,应在服务实现中对消费方不关心的异常进行包装,否则可能出现消费方无法反序列化相应异常。
  • 调用
      不要只是因为是Dubbo调用,而把调用try…catch起来。try…catch应该加上合适的回滚边界上。
      对于输入参数的校验逻辑在Provider端要有。如有性能上的考虑,服务实现者可以考虑在API包上加上服务Stub类来完成检验。
  • Provider上尽量多配置Consumer端属性
      原因如下:
      1)作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等。
      2)在Provider配置后,Consumer不配置则会使用Provider的配置值,即Provider配置可以作为Consumer的缺省值 。否则,Consumer会使用Consumer端的全局设置,这对于Provider不可控的,并且往往是不合理的
      3)Provider上尽量多配置Consumer端的属性,让Provider实现者一开始就思考Provider服务特点、服务质量的问题。
  • Provider上配置合理的Provider端属性
      Provider上可以配置的Provider端属性有:
  1. threads:服务线程池大小。
  2. executes:一个服务提供者并行执行请求上限,即当Provider对一个服务的并发调用到上限后,新调用会Wait,这个时候Consumer可能会超时。在方法上配置dubbo:method则并发限制针对方法,在接口上配置dubbo:service ,则并发限制针对服务。

6.2 使用建议

  • Dubbo服务划分
      1、服务划分目标:抽取系统中独立的业务模块服务化,按业务独立性进行垂直划分,抽象出基础服务层。
      2、子系统划分把控:合理划分,过细过粗都不行。
      3、注意事项:
  1. 表:避免出现A服务关联B服务的表的数据操作;服务一旦划分了,那么数据库即便没分开,也要当成db表分开了来进行编码;否则AB服务难以进行垂直拆库。
  2. 避免服务耦合度高,依赖调用;如果出现,考虑服务调优。
  3. 避免分布式事务,不要拆分过细。
  • Dubbo接口划分
      1、接口尽可能大粒度,接口中的方法不要以业务流程来,这个流程尽量在方法逻辑中调用,接口应代表一个完整的功能对外提供。
      2、接口应以业务为单位,业务相近的进行抽象,避免接口数量爆炸。
      3、参数先做校验,在传入接口。
      4、要做到在设计接口时,已经确定这个接口职责、预测调用频率。
  • 依赖
      web应用大致分为两层:biz和web,实际上biz可能由内部多个工程组成,这里biz只是一个抽象概念。impl依赖api和biz,web依赖impl和biz,没有其他依赖关系,严禁biz依赖api。关系:
  • 分包
      biz-service、biz-api、biz-web。
      web依赖于api;service依赖于api;service和web没有依赖;controller是一个工程,service+dao是一个工程。

      biz-service放converter、dao、service.impl、application、config、properties;
      biz-api放domain、enumeration、exception;
      biz-web放controller、config、properties;
      common放公共代码。
  • 事务
      如果仅仅是应用拆分,而没有数据库的拆分,那么仍可视为单机事务。
      如果数据库也进行了拆分,每个应用使用的表被放在相应机器的数据库中,那么需要考虑分布式事务问题。
      分布式事务的前提是多个数据库之间难以保证一致性,单一数据库本身的事务机制可以保证数据一致性。

6.3 开发参考

  Dubbo官方文档:Dubbo官方文档
  Dubbo开发示例:超简单,手把手教你搭建Dubbo工程

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐