Dubbo 总结

基本架构

  • Consumer
  • Producer
  • Registry
    • zookeeper为例
      • /dubbo |/com.bob.dubbo.service.CityDubboService | | | |/providers /consumers /configurators / routers | |/ip port class 所有的method版本例如:anyhost=true application=dst-operate-servicedubbo=2.6.2generic=falseinterface=com.dst.modules.dataanal.CarRentalRecordServicemethods=generatorData,createBatchRecord,deleteAll,selectListByCondition,updateRecordByAccount,queryRecordList,updateByBackcarReasonByCode,queryRentCarListByvinCode,queryRecordListByPickCode,queryRecordListByServiceOrderCode,updateOldServiceCode,createRecord,updateBatchCarRentalRecordById,updateBatchByRenewLease,updateRecord,queryRecordListByDeliveryCodepayload=20971520pid=41756side=providertimestamp=1554781127816
      • 消费者也会注册节点便于管理、监控;消费者在第一次调用时会获取提供方ip,并缓存到本地,同时消费者会利用zookeeper提供的watcher机制订阅提供者变更信息zookeeper观察机制;服务端只存储事件的信息,客户端存储事件的信息和Watcher的执行逻辑.ZooKeeper客户端是线程安全的每一个应用只需要实例化一个ZooKeeper客户端即可,同一个ZooKeeper客户端实例可以在不同的线程中使用。ZooKeeper客户端会将这个Watcher对应Path路径存储在ZKWatchManager中,同时通知ZooKeeper服务器记录该Client对应的Session中的Path下注册的事件类型。当ZooKeeper服务器发生了指定的事件后,ZooKeeper服务器将通知ZooKeeper客户端哪个节点下发生事件类型,ZooKeeper客户端再从ZKWatchManager中找到相应Path,取出相应watcher引用执行其回调函数process。每个watcher注册一次只会触发一次,需要重新注册才能再触发服务上线,向zk注册临时节点;服务下线,zk本身通过发起心跳感知服务端是否存在,心跳长期无响应则删除节点
  • Monitor

组件分层

流程

服务暴露发现过程

服务启动的时候,provider和consumer根据配置信息,连接到注册中心 register,分别向注册中心注册和订阅服务。

消费者到提供者的调用过程

  1. 消费者发送请求
    1. 获取本次集群中的全部 invokers(应用启动的时候,消费者会订阅服务,并且拉取所有订阅的提供者节点信息到Directory中)
    2. Router 从 invokers 中根据路由规则获取满足规则的 invokers
    3. 确定 Cluster 集群容错策略进行 invoker 调用
    4. 根据 LoadBalance 负载均衡策略,挑选最终调用的 invoker
    5. 执行消费者 filter 调用链比如用于统计的 MonitorFilter,用于处理当前上下文信息的 ConsumerContextFilter 等等,过滤器这一部分给用户提供了很大的扩展空间
    6. 获取具体的协议(默认dubbo协议,请求由header和body组成,header指定body的size)
    7. 根据协议对请求对象进行编码、序列化(hessian)
    8. 通过通讯组件(Netty)发送数据
  2. 提供者处理请求
    1. 反序列化、解码
    2. 利用派发器决定这个请求由IO线程 或 业务线程池执行
    3. 执行提供者 filter 链
    4. 获取invoker,并反射调用真正的实现类方法(底层实现保存在 hashmap,通过请求信息获取到真正实现)
    5. 将结果进行编码、序列化
    6. 返回响应
  3. 消费者接受响应
    1. 反序列化、解码
    2. 通过 CompletableFuture 异步/同步等待获取响应
    3. 得到响应后,通过调用请求编号从 Future Map 中获取对应的 DefaultFuture,然后唤醒业务线程
      • 消费端超时问题处理?
        • 旧版本通过 ScheduledThreadPoolExecutor,新版本通过 HashedWheelTimer 实现。HWT 在dubbo的心跳检测、超时,Redisson的看门狗定时续期中都有使用到。
        • HWT相对传统定时对比。
          1. 牺牲了精度(精度取决于时间间隔,即时间轮每个格子的时间)
          2. 添加任务直接接入链表队列尾部o(1),具体执行任务会从中取出并分配到时间轮上传统由于要维护优先队列(o(logn))

其他特性

SPI机制

SPI 全称为 Service Provider Interface,是一种服务发现机制,本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样可以在运行时,动态为接口替换实现类。

Dubbo也正是通过SPI机制实现了众多的扩展功能,而且dubbo没有使用java原生的SPI机制,而是对齐进行了增强和改进。

Java 原生 SPI 缺点:
1. 不能按需加载。Java SPI在加载扩展点的时候,会一次性加载所有可用的扩展点,很多是不需要的,会浪费系统资源
2. 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类
3. 不支持AOP与IOC
4. 如果扩展点加载失败,会导致调用方报错,导致追踪问题很困难

SPI在dubbo应用很多,包括协议扩展、集群扩展、路由扩展、序列化扩展等等。

使用方式可以在META-INF/dubbo目录下配置:

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

然后通过dubbo的ExtensionLoader按照指定的key加载对应的实现类,这样做的好处就是可以按需加载,性能上得到优化。

Dubbo SPI还支持自适应拓展机制,基于参数,在运行时动态选择到具体的目标类,然后执行。

代码中通过ExtensionLoader的getAdaptiveExtension获取自适应拓展。自适应类可以通过@Adaptive注解标注,例如:

@SPI("lru")
public interface CacheFactory {

    /**
     * CacheFactory implementation class needs to implement this return underlying cache instance for method against
     * url and invocation.
     * @param url
     * @param invocation
     * @return Instance of Cache containing cached value against method url and invocation.
     */
    @Adaptive("cache")
    Cache getCache(URL url, Invocation invocation);

}

如果在运行期间 url 传入了 cache 这个key,那么这个自适应对象就会找到 cache 这个 key 对应的实现类,实现动态匹配实现类。

原理参考:https://cn.dubbo.apache.org/zh-cn/docsv2.7/dev/source/adaptive-extension/

单一长连接

  • 单一长连接:

    每个 consumer 默认启动时,会通过Netty NIO为每个 provider 实例节点创建一个长连接,减少连接成本(通过配置lazy=”true” 可以将连接延迟到调用时;通过connections=”n”可以配置连接数,一般不配置)。

    通过请求 id 区分同一个连接上的各个请求。创建一个连接客户端同时也会创建一个心跳客户端,客户端默认基于 60 秒发送一次心跳来保持连接的存活。

    适合大并发小数据量及消费者数量远大于提供者。

优雅停机

  • Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果用户使用 kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。
  • 服务提供方: 停止时,先标记为不接收新请求,新请求过来时直接报错,让客户端重试其它机器。 然后,检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超时,则强制关闭。
  • 服务消费方: 停止时,不再发起新的调用请求,所有新的调用在客户端即报错。 然后,检测有没有请求的响应还没有返回,等待响应返回,除非超时,则强制关闭。

内部组件对比选型

序列化

  • 方案对比
    • hession
      • 跨语言、高效、二进制
    • json
      • 文本形式,性能差
    • dubbo
      • 尚不成熟
    • java
      • 性能差
    • protobuf
      • 性能好
    • Thrift
      • 性能好,包含序列化之外的功能

事件派发策略

  • all(default)
    • 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。 即worker线程接收到事件后,将该事件提交到业务线程池中,自己再去处理其他事。
  • direct
  • message
  • execution
  • connection

负载均衡策略

    1. (加权) 随机 (default)
    1. (加权)轮询
    1. 最小活跃数(活跃数低即处理慢的提供者接收到更少的请求):每个服务提供者对应一个活跃数 active,初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。
    1. 一致性hash:基于dubbo一致性Hash,相同参数的请求总是发到同一提供者。通过参数计算hash值,以此计算请求哪个提供者

集群容错策略(前6个属于容错,后面几个有对应的应用场景)

    1. failover 失败重试其他服务器,常用于读
    1. failfast 快速失败,请求失败则失败,常用于非幂等场景,如新增
    1. failback 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作
    1. failsafe 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作
    1. forking 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数
    1. broadcast 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
    1. mergeable 分组聚合,将集群中的调用结果聚合起来,然后再返回结果。比如菜单服务,接口一样,但有多种实现,用group区分,现在消费方需从每种group中调用一次返回结果,合并结果返回,这样就可以实现聚合菜单项
    1. available 获取可用的服务方。遍历所有Invokers通过invoker.isAvalible判断服务端是否活着,只要一个有为true,直接调用返回,不管成不成功
    1. mock 本地伪装,通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败
  • 10. zone-aware 注册中心集群间负载均衡

注册中心

  • 方案对比
    • zookeeper
      • CP
        • 无法保证可用性:选举leader的时间太长,30 ~ 120s, 且选举期间整个zookeeper集群都是不可用的由于master才能写,所以tps有限(W级别),无法水平拓展来增加性能,增加节点反而增加了同步的成本。Dubbo通过客户端本地缓存来缓解这个问题。
      • 语言:Java
    • etcd
      • CP
        • 原理/介绍
          • 一致性的KV存储系统一致性:ETCD[Raft]协议保证,对比zookeeper使用Zab(类PAXOS),前者容易理解,方便工程实现运维方面:ETCD方便运维,zookeeper难以运维;项目活跃度:ETCD社区与开发活跃,zookeeper已经快死了;API:ETCD提供HTTP+JSON, gRPC接口,跨平台跨语言,zookeeper需要使用其客户端;访问安全方面:ETCD支持HTTPS访问,zookeeper在这方面缺失;
          • 任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端写操作,通过Raft协议保证写操作对状态机的改动会可靠的同步到其他节点
      • 语言:Go
    • consul
      • CP
      • 语言:Ruby
    • Eureka
      • AP
        • 特性:自我保护机制
          • 如果一个服务器出问题,不需要任何类型的选举,客户端会自动连接到一个新的Eureka服务器Eureka有一个服务心跳的概念,可以阻止过期数据:如果一个服务长时间没有发送心跳,那么Eureka将从服务注册中将其删除。但在出现网络分区、Eureka在短时间内丢失过多客户端时,它会停用这一机制,进入“自我保护模式”。网络恢复后,它又会自动退出该模式。这样,虽然它保留的数据中可能存在错误,却不会丢失任何有效数据。Eureka在客户端会有缓存。即使所有Eureka服务器不可用,服务注册信息也不会丢失。缓存在这里是恰当的,因为它只在所有的Eureka服务器都没响应的情况下才会用到。Eureka就是为服务发现而构建的。它提供了一个客户端库,该库提供了服务心跳、服务健康检查、自动发布及缓存刷新等功能。使用ZooKeeper,这些功能都需要自己实现。
      • 生态:不活跃,目前开源社区已不再更新
    • Nacos
      • 维护:阿里巴巴
      • 语言:Java
      • 功能
          1. 服务发现和服务监控检测
          1. 动态配置服务
          1. 动态DNS
          1. 服务及其元数据管理
      • 分布式场景:AP/CP 同时存在
        • 原理:
          • 如果在Nacos上注册临时节点,那么Nacos就是AP服务,保证高可用。如果Nacos上注册永久节点,那么Nacos就是CP服务,保证数据一致性
          • AP:Distro协议 类似Gossip,每个节点维护部分数据,小部分节点失效会丢失数据,但集群仍然可用CP:Raft 类似 Zab(zookeeper)必须过半数节点同步后,才可以访问,存在阻塞的过程
        • 服务发现场景可以采用AP
          • 因为对于服务发现来说,获取不到提供者列表远比获取到下线的提供者要严重。后者可以通过容错机制重试其他节点。
      • 生态:活跃
  • 在注册中心的场景中,注册中心不可用 对比 数据不一致不一致:会导致客户端拿到的服务列表不一致,导致流量不均衡,但是若有最终一致的保证及failover机制,实际影响不大;不可用:例如同机房 业务服务提供节点要注册(或更新、缩容、扩容等)到zookeeper集群,该机房和其他机房出现网络分区,由于无法连接zookeeper的leader,导致本机房业务消费房无法感知本机房的提供方的变化,导致无法调用,这是难以容忍的。所以针对注册中心可能ap会更合适

线程池(启动时确认)

  • fixed
    • 固定大小线程池,线程,不关闭,一直持有。(缺省)coresize:200maxsize:200队列:SynchronousQueue回绝策略:AbortPolicyWithReport – 打印线程信息jstack,之后抛出异常
  • cache
    • 缓存线程池,无核心线程数,有最大线程数,队列为SynchronousQueue,所以提交任务时,会直接创建线程,空闲一分钟自动删除,需要时重建
  • limit
    • 可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。
  • eager
    • 类似tomcat中的线程池,当核心线程都在忙碌时(通过对比已提交未执行任务数量(AtomicInteger)和核心线程数量),优先保障线程数量到达最大线程数,再放队列

dubbo迭代版本特性

  1. dubbo 2.7.5 更新
    • 应用粒度服务注册
      • 一个实例只向注册中心注册一条记录,不同于之前一个接口注册一条,彻底解决服务推送性能瓶颈;同时由于这样的模型与主流微服务体系如 SpringCloud、K8S 等天然是对等的,因此为 Dubbo 解决和此类异构体系间的互联互通清除了障碍,dubbo通过对同一接口(服务)发布不同的协议来支持异构服务互联互通
      • 由于注册中心只有 应用-实例 的信息,缺少RPC(如接口等)信息,那么RPC相关的数据Dubbo新版本通过「服务自省」机制实现:服务自省在服务消费端和提供端之间建立了一条内置的 RPC 服务信息协商机制,这也是“服务自省”这个名字的由来。服务端实例会暴露一个预定义的 MetadataService RPC 服务,消费端通过调用 MetadataService 获取每个实例 RPC 方法相关的配置信息(如重试、负载均衡策略等等)。实际上就是提供方和消费方对于RPC数据的同步,目前有两种形式: 1)内建 MetadataService。 2)独立的元数据中心,通过中细化的元数据集群协调数据。消费端可以在调用时查询服务方的MetadataService,或者提供方同步到元数据中心,当注册中心有变动时下发到消费方,消费方再去元数据中心查。另外还有一个配置中心,保存了 service – app 的映射,调用变成先查配置中心,找到app后查注册中心(缓存的注册中心)找到提供方ip,然后根据提供方的RPC数据然后进行调用。
    • HTTP/2 (gRPC) 协议支持
      • 通用性
    • Protobuf 支持
      • Protobuf 实现了语言中立的服务定义,不依赖于具体的语言,例如java那么只能通过interface来定义服务。
      • 序列化上更高效,更通用
    • 调用链路优化
      • 在服务注册阶段提前生成 ServiceDescriptor 和 MethodDescriptor
    • 消费端线程池模型优化
    • TLS 安全传输链路
    • Bootstrap API【beta】
    • 多注册中心集群负载均衡
  2. dubbo 3.0 更新
      1. 应用级别服务发现
      1. 下一代RPC协议 Triple
      1. K8s服务集成,告别传统注册中心

对比同类框架

和 spring cloud 对比

  • 架构
    • 基本一致
  • 组成
    • spring cloud功能更全,dubbo需要第三方组件才可以达到一样的效果spring cloud包含服务网关、熔断器、配置中心等组件
  • 通信方式
    • spring cloud feign采用 http 协议,jackson序列化;dubbo默认dubbo协议,性能较优一些(序列化性能及协议相对简洁,少了一些header)

和 Istio 对比

  • Istio 是开源的服务网格的实现,不同于dubbo、spring clound等,它是以独立的代理进程的形式提供服务治理能力,和业务服务完全解耦
  • 对比
      1. 服务发现角度和 负载均衡角度dubbo需要部署注册中心,而 Istio 直接利用k8s,从中获取服务和实例的关系,从而可以实现负载均衡,解决了服务注册和发现不及时的问题和减少单独维护注册中心的成本,架构更加简洁,当然前提已经使用k8s管理维护微服务。
      1. 解决多语言问题无论业务使用什么语言,只要能够开放端口用于访问管理就可以使用网格管理
      1. 解决升级SDK产生的兼容问题传统sdk升级需要各业务线自己升级,同时需要测试发布等等,产生额外的工作量同时风险较大,Istio由于是独立进程,发布和普通应用类似,更加平滑
      1. 缺点: 1. 额外的运维成本 2. 增加了额外的网络调用,增加了响应延时

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Scroll to Top