高性能Java RPC框架Dubbo

  摘自《Java微服务分布式架构企业实战》

  讲解了使用Spring Cloud来解决微服务应用程序开发过程中所遇到的一系列诸如客户端如何调用服务、服务与服务之间如何进行通信、服务如何管理、岩机或出现故障该如何处理等一系列问题,实现了基于Spring Boot+Spring Cloud的架构完成微服务项目的开发。在很多时候,Dubbo与一些第三方组件结合使用也可以实现以上效果,因此,本章将继续讲解微服务系统开发的第二种方式,即Spring Boot+Dubbo+ZooKeeper的方式开发项目。
1 什么是Dubbo
  Apache Dubbo(incubating)是一款高性能、轻量级的开源Java RPC分布式服务框架,它提供了三大核心能力:面向接口的远程方法调用、智能容错和负载均衡,并可以通过调用ZooKeeper实现服务自动注册和发现。它最大的特点是架构分层,使用这种方式可以使得各个层次之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方(Provider)和服务消费方(Consumer)两个角色。
  1. 1 Dubbo的服务治理
  在前面学习Spring Cloud后,大家对服务治理也有了一定的理解,微服务系统的开发过程中,服务治理是必不可少的,面对错综复杂的服务,只有有条不紊地管理好这些服务,才能保证系统的稳定。除了Spring Cloud可以做服务治理外,Dubbo也具备服务治理的功能。例如:透明远程调用,就像调用本地方法一样调用远程方法;只需简单配置,没有任何API侵入;负载均衡机制,客户端负载均衡,可在内网替代硬件负载均衡器;容错重试机制,服务模拟后台数据,重试次数、超时机制等;自动注册发现,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者;性能日志监控,统计服务的调用次数和调用时间的监控中心;服务治理中心包含路由规则、动态配置、服务降级、访问控創、权重调整、负载均衡、手动配置等。

    1. 2 Dubbo的核心功能

  Remoting:远程通信,提供对多种NIO框架抽象封装,包括“同步转异步”和“请求-响应”模式的信息交换方式。

  Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支持以及软负载均衡、失败容错、地址路由、动态配置等集群支持。

  Registry:服务注册中心,服务自动发现,基于注册中心目录服务,使服务消费方能动态地查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。

  1. 3Dubbo的组件角色

  Dubbo组件中有五个重要的组成部分,各个角色如下。

  Provider:服务提供。

  Consumer:调用远程服务的服务消费方。

  Registry:服务注册与发现的注册中心。

  Monitor:统计服务的调用次数和调用时间的监控中心。

  Container:服务运行容器。

  各个组件角色调用关系说明如下。

  服务容器(Container)负责启动、加载、运行服务提供者。

  服务提供者(Provider)在启动时,向注册中心注册自己提供的服务。

  服务消费者(Consumer)在启动时,向注册中心订阅自己所需的服务。

  注册中心(Registry)返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

  服务消费者(Consumer)从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

  服务消费者(Consumer)和提供者(Provider)在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心(Monitor).
2 Dubbo Admin 管理控制台
  Dubbo Admin 工程是Dubbo管理控制台(本身与Dubbo服务没有直接关系),主要包含提供者、路由规则、动态配置、访问控制、权重调节、负载均衡、负责人等管理功能。DubboAdmin默认使用消息注册中心类型为ZooKeeper.

  管理控制台为内部裁剪版本,开源部分主要包含路由规则、动态配置、服务降级、访问控制、权重调整、负载均衡等管理功能。

 

3 Dubbo的负载均衡
  Dubbo的负载均衡如图11. 8所示。

当服务提供方是集群的时候,为了避免大量请求一直落到一个或几个服务提供方机器上,从而使这些机器负载很高,甚至“打死”,需要做一定的负载均衡策略。Dubbo提供了多种均衡策略,默认为random,也就是每次随机调用一台服务提供者的机器。Dubbo提供的负载均衡策略分为Random LoadBalance、RoundRobin LoadBalance、LeastActive LoadBalance、ConsistentHash LoadBalance等,下面将详细介绍每一种策略。Random LoadBalance:随机策略。按照概率设置权重,比较均匀,并且可以动态调节服

务提供者的权重。

  RoundRobin LoadBalance:轮询策略。按照约定后的权重设置轮询比率,会存在执行比较慢的服务提供者堆积请求的情况,如一个机器执行得非常慢,但是机器没有宕机(如果宕机了,那么当前机器会从ZooKeeper的服务列表删除),当很多新的请求到达该机器后,由于之前的请求还没有处理完毕,会导致新的请求被堆积,久而久之,所有消费者调用这台机器上的请求都被阻塞。

  Least Active LoadBalance:最少活跃调用数。如果每个服务提供者的活跃数相同,则随机选择一个。在每个服务提供者里面维护着一个活跃数计数器,用来记录当前同时处理请求的个数,也就是并发处理任务的个数。所以如果这个值越小说明当前服务提供者处理的速度越快或者当前机器的负载比较低,所以路由选择时就选择该活跃度最小的机器。如果一个服务提供者处理速度很慢,由于堆积,那么同时处理的请求就比较多,也就是活跃调用数目越大,这也使得慢的服务提供者收到更少请求,因为越慢的服务提供者的活跃度越大。
  ConsistentHash LoadBalance:一致性Hash策略。一致性Hash,可以保证相同参数的请求总是发到同一服务提供者,当某一台提供者服务宕机时,原本发往该提供者的请求,基于虚拟节点,平摊到其他服务提供者,不会引起剧烈变动。在解决分布式系统中,负载均衡的问题可以使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器都固定处理一部分请求(并维护这些请求的信息),起到负载均衡的作用。但是普通的余数Hash(Hash(如用户ID)%服务器机器数)算法伸缩性很差,当新增或者下线服务器机器时,用户ID与服务器的映射关系会大量失效。一致性Hash则利用Hash环对其进行了改进。

  一致性Hash有要有以下特性。·

  单调性(Monotonicity):单调性是指如果已经有一些请求通过Hash分派到了相应的服务器进行处理,又有新的服务器加入到系统中时,应保证原有的请求可以被映射到原有的或者新的服务器中去,而不会被映射到原来的其他服务器上去。

  分散性(Spread):分布式环境中,客户端请求时可能不知道所有服务器的存在,可能只知道其中一部分服务器,在客户端看来它看到的部分服务器会形成一个完整的Hash环,那么可能会导致同一个用户的请求被路由到不同的服务器进行处理。这种情况显然是应该避免的,因为它不能保证同一个用户的请求落到同一个服务器。所谓分散性是指上述情况发生的严重程度。  

  平衡性(Balance):平衡性也就是说负载均衡,是指客户端Hash后的请求应该能够分散到不同的服务器上去。一致性Hash可以做到每个服务器都进行处理请求,但是不能保证每个服务器处理的请求的数量大致相同。

4 Dubbo+Kryo实现高速序列化
  1.Dubbo中的序列化

  序列化就是将一个对象变成一个二进制流;反序列化就是将二进制流转换成对象。

  当两个系统之间存在参数传递的时候,如果传递的参数是一个对象,它必须实现序列化,而且如果这个对象的属性还是一个对象,那么相应的另外一个对象也需要序列化。

  如图11. 9所示,调用系统在调用缓存服务的时候,通过工厂方法传递一个RedisService,这无意间就形成了参数传递,不仅 RedisService需要实现序列化,而且 RedisService内封装的 RedisTemplate也要实现序列化。我们当然是不希望去改动 RedisTemplate的,所以在使用Dubbo时,应该注意这种参数上的传递。

  Dubbo RPC是Dubbo体系中最核心的一种高性能、高吞吐量的远程调用方式,可以称为多路复用的TCP长连接调用。

  长连接:避免了每次调用新建TCP连接,提高了调用的响应速度。

 

  多路复用:单个TCP连接可交替传输多个请求和响应的消息,降低了连接的等待闲置时间,从面减少了同样并发数下的网络连接数,提高了系统吞吐量。

  RPC的封装过程如下:

  (1)服务消费方调用服务;

  (2)服务消费方接收到调用后,将方法参数等封装成能够进行网络传输的消息体:

  (3)服务消费方找到服务地址,将消息发送到服务端;

  (4)服务提供方接收到消息后进行解码(解码就是所谓的序列化);

  (5)服务提供方根据解码的结果调用本地服务;

  (6)本地服务执行服务消费方发送的请求,并将执行后的结果打包成消息体发送给服务消费方:

  (7)服务消费方接收到消息解码,拿到结果。

  RPC通过JDK的动态代理的方式来实现调用远程服务,理论上获取服务提供的接口,当服务消费方调用服务的时候回执行invoke()方法,实现远程调用。

  Dubbo RPC主要用于两个Dubbo系统之间的远程调用,特别适合高并发、小数据的互联网场景。而序列化对于远程调用的响应速度、吞吐量、网络带宽消耗等同样也起着至关重要的作用,是提升分布式系统性能的最关键因素之一。
  2.RPC序列化的优点

  (1)通用性(支持复杂数据结构,如map).

  (2)性能(序列化效率和节约内存,节省宽带)。

  (3)可扩展性(由于业务发展的变化快)。

  3.Dubbo中支持的序列化方式

  Dubbo序列化:阿里尚未开发成熟的高效Java序列化实现,阿里不建议在生产环境使用它。

  Hessian2序列化:Hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的Hessian2序列化,而是阿里修改过的 Hessian Lite,它是Dubbo RPC默认启用的序列化方式。

  JSON序列化:目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用Dubbo中自己实现的简单JSON库,但其实现都不是特别成熟,而且JSON这种文本序列化性能一般不如上面两种二进制序列化。

  Java序列化:主要是采用JDK自带的Java序列化实现,性能很不理想。

  在通常情况下,这四种主要序列化方式的性能从上到下依次递减。对于Dubbo RPC这种追求高性能的远程调用方式来说,实际上只有第一种和第二种两种高效序列化方式比较般配,而Dubbo序列化由于还不成熟,所以实际只剩下Hessian2序列化可用,所以DubboRPC默认采用 Hessian2序列化。

  由于Hessian序列化是一种比较老的序列化实现,而且它是跨语言的,所以不是单独针对Java进行优化的。而Dubbo RPC实际上完全是一种Java to Java的远程调用,其实没有必要采用跨语言的序列化方式(当然肯定也不排斥跨语言的序列化)。

  最近几年,各种新的高效序列化方式层出不穷,不断刷新序列化性能的上限,最典型的包括专门针对Java 语言的Kryo、FST,跨语言的Protostuff、ProtoBuf、Thrift、Avro、MsgPack等。这些序列化方式的性能多数都显著优于Hessian2(甚至包括尚未成熟的Dubbo序列化),鉴于此,为Dubbo引入Kryo和FST这两种高效Java序列化实现,来逐步取代Hessian2.

  其中,Kryo是一种非常成熟的序列化实现,已经在Twitter、Groupon、Yahoo以及多个著名开源项目(如Hive、Storm)中广泛使用。而FST是一种较新的序列化实现,目前还缺乏足够多的成熟使用案例。在面向生产环境的应用中,目前更优先选择Kryo.

5.持续集成与持续交付  

  当今互联网软件从开发到发布的过程已经形成了一套标准流程,在这套流程中持续集成(Continuous integration,CI)是其重要的组成部分,具备在产品尚未形成风险时进行测试,并能够快速响应,最大程度地减少风险,降低错误代码的成本,摒弃掉一些重复性的手工流程操作,从而实现自动化;每次集成都通过自动化的构建(包括编译、发布、自动化测试)来验证,从而尽早地发现集成错误,使得开发人员能够更加专注于代码的开发,而且能够保持部署的频率,快速生成可以实现部署的软件,提高开发人员的工作效率。

   1持续集成概述

  持续集成指的是频繁地(一天多次)将代码集成到主干。在企业开发过程中,通常一个项目是由一个团队协作完成,团队的成员分工明确,每个人都会有自己的相关工作。因此,每个成员都会很频繁地集成自己的工作任务,如果每一次的集成都是自动化构建完成,将会有以下两个好处:

  (1)快速地发现存在的错误。每当完成一次更新,都会自动集成到主干,可以快速地发现错误,错误定位也比较容易。

  (2)防止分支大幅度偏离主干。主干在不断更新,开发人员如果不能够及时、经常地集成自己的工作,将会导致在后续的集成工作中难度变得越来越大,甚至难以实现集成。持续集成的流程如图13. 1所示。

  图13. 1中开发人员结队完成代码的编程,然后提交代码到仓库,触发持续集成服务,由持续集成服务来进行构建、测试、返回结果,开发人员看到结果后可以快速发现错误,修改完有问题的代码后继续提交到仓库,再一次进行触发持续集成服务,完成构建、测试、返回结果等。

  持续集成服务注重开发人员提交新代码后立刻完成一系列的构建、测试,并根据返回的结果及时判断新提交的代码能否合适地集成在一起。与持续集成相关的还有另外两个概持续集成概述

念,分别是持续交付和持续部署。
  2.持续交付

  持续交付(Continuous Delivery)是指频繁交付软件的新版本给评审团队,使产品能够在短期内完成,减少开发成本,以供评审。如果评审通过,那么代码就进入生产阶段,随时能够进行发布,保持软件的稳定性,避免重大风险的发生。

  持续交付可以看作持续集成的下一步。它注重的是软件的随时交付性,不管怎么更新都可以部署到生产环境中。持续交付的流程如图13. 2所示。  

  图13. 2中,开发人员提交项目代码到仓库,触发持续集成服务,由持续集成服务来进行构建、测试、返回结果,完成持续集成后,如果没有错误可以继续向测试环境测试(Test),如果没有问题发生,就继续在集成测试环境测试(Staging),如果也没有问题发生就可以部署到类生产环境(Production)中。需要注意的是,最后部署的环境中软硬件配置必须和生产环境的配置保持一致。

  持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的类生产环境(Production-like Environments)中。如在完成代码的单元测试后,可以把代码部署到连接数据库的环境中做更多的测试。如果代码没有问题,可以继续手动部署到生产环境中,如图13. 3所示。

 

  图13. 3和图13. 2的区别是在部署到类生产环境时需要进行手动部署。

  3.持续部署

  持续部署(Continuous Deployment)是持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境,如图13. 4所示。

  持续部署的目标是代码在任何时刻都是可部署的,可以进入生产阶段。持续部署的前提是能自动化完成测试、构建、部署等步骤。

  4持续集成的操作流程
  按照持续集成的操作流程设计,相关的项目从代码提交到投入生产,其中的整个过程可以分为以下几个步骤。
  1.提交(Commit)

  持续集成的整个流程都始于本地代码的一次提交,当代码的开发者开发完成一个项目,提交代码到仓库后方能继续后续工作。
  2.(第一轮)测试

  在测试之前大家应该了解一个概念-hook(钩子),hook是存放在$GIT_DIR/hooks目录下的一个小脚本,这个脚本文件可以触发完成一些特定的动作。当代码仓库对提交操作配置了hook后,在下次的代码提交时就会触发脚本,进行自动化测试。在第一轮的测试中,至少要进行一次单元测试。测试的种类分为单元测试、集成测试、端对端测试。在单元测试中,主要是对代码中的函数或者相应的功能模块进行测试;在集成测试中,主要是对一个项目中的某些功能模块进行测试;在端对端的测试中,需要完成由用户界面开始,涉及整个应用系统环境在现实模拟情况下的所有情形的测试,例如,数据库链路、网络通信、软硬件等相关测试。
  3.构建

  完成以上两步后,代码即可合并主干,进行代码的交付。代码交付完成后,接下来就是构建工作。在构建过程中将会把项目的源代码转换成能够成功运行的代码,包含了相关依赖的安装、各种动静态资源的配置等。
在企业中常用的构建工具有以下几种:Jenkins、Travis、Codeship、Strider.以上软件中,开源软件有Jenkins和Strider.其中,Travis和Codeship 可以对开源的项目提供免费服务。这些软件能够一次执行完成构建和测试的任务。
  4.测试(第二轮)  

  一般情况下构建完成后就会进行第二轮测试任务。当然,若在第一轮的测试过程中已经完成了涉及的全部相关测试内容,则可以跳过当前测试。需要注意的是,如果第一轮测试覆盖面比较广,无须第二次测试时,构建任务需要在测试任务之前完成。以上的所有测试都是自动化测试,只有少数无法使用自动化测试的任务需要手动完成。

需要强调的是,每当更新一个新版本时任何一个更新点都需要确保能够完成测试。否则有可能会因为测试的覆盖面低导致功能或环境测试不彻底,在后续的部署阶段引发严重的错误。
  5.部署

  以上四步完成后,此时的代码就可以直接完成部署操作了,部署的相关工具有Ansible、
Chef、Puppet等。将该代码的所有文件打包存档,然后上传至服务器,之后在服务器中将文件解压,放置在本地目录,并通过运行路径的符号链接(Symlink)指向该目录,最后重启应用即可。
  6.回滚

  在部署的过程中如果当前版本发生问题,这时就需要回滚到上一个版本的构建结果。最简单的做法就是修改符号链接,将运行路径指向上一个版本的目录。

posted @ 2022-04-14 16:24  嘉禾世兴  阅读(422)  评论(0编辑  收藏  举报