一、Dubbo介绍
Dubbo是阿里2011年开源的一款服务化开发框架,中间经过一段时间的停止维护后,终于在2017重启维护,目前已捐献给Apache基金会,称为全球顶级项目。大家通常会将它和SpringCloud做对比,实际上笔者认为,它们虽然能做同样的事情,但各有侧重点和优缺点,本章我们在学习Dubbo的同时,也会点出它们的不同。
1.1 Dubbo架构路线路
目前大型软件工程基本已经告别单体应用,逐步转向服务化、分布式集群,当然这种转向并非一日之功,它是经历了阶段性变化的。
1.2 Dubbo调用模型
任何服务化都离不开远程调用(通信),下面这个图可以基本说明Java RMI的基本过程,有过早期EJB的同学肯定对其有深刻印象:
但是这个过程对于工程师来说稍显复杂,而且并非业务核心,目前大部分服务框架(Dubbo、SpringCloud等)都能做到屏蔽远程细节,减轻了工程师的心智负担。对于Dubbo来讲,我们在实际开发中,大部分就跟两个概念打交道(稍显武断),即Consumer、Provider:
Consumer即消费(调用)端,Provider即服务提供端。
二、注册中心之Zookeeper
通常情况下,Provider会以多服务集群的形式存在,那么Consumer该如何找到Provider呢?答案就是注册中心,Dubbo中推荐的注册中心是Zookeeper(后面简称zk)。
服务提供者:provider
服务消费者:consumer
服务注册中心:registry
我们可以从官网直接下载其安装包,然后打开conf/zoo.cfg做些配置:
tickTime=2000 dataDir=/var/lib/zookeeper clientPort=2181
启动命令:
bin/zkServer.sh start
或者
bin/zkServer.cmd
zk不是本次学习的重点,我们暂时知道其作用并且会应用即可。
三、Dubbo开发初试
3.1 Dubbo核心代码讲解
首先我们看下Dubbo开发的基础过程及核心代码:
第一步,服务定义及实现:
publicinterfaceGreetingsService { StringsayHi(Stringname); }
publicclassGreetingsServiceImplimplementsGreetingsService { publicStringsayHi(Stringname) { return"hi "+name+" ,welcome to dubbo"; } }
第二步,注册服务 为 provider:
privatestaticStringzookeeperHost=System.getProperty("zookeeper.address", "127.0.0.1"); publicstaticvoidmain(String[] args) throwsException { ServiceConfig<GreetingsService>service=newServiceConfig<>(); service.setApplication(newApplicationConfig("first-dubbo-provider")); service.setRegistry(newRegistryConfig("zookeeper://"+zookeeperHost+":2181")); service.setInterface(GreetingsService.class); service.setRef(newGreetingsServiceImpl()); service.export(); System.out.println("dubbo service started"); newCountDownLatch(1).await(); }
第三步,定义消费者consumer:
privatestaticStringzookeeperHost=System.getProperty("zookeeper.address", "127.0.0.1"); publicstaticvoidmain(String[] args) { ReferenceConfig<GreetingsService>reference=newReferenceConfig<>(); reference.setApplication(newApplicationConfig("first-dubbo-consumer")); reference.setRegistry(newRegistryConfig("zookeeper://"+zookeeperHost+":2181")); reference.setInterface(GreetingsService.class); GreetingsServiceservice=reference.get(); Stringmessage=service.sayHi("dubbo"); System.out.println(message); }
3.2 Dubbo工程实践
上述代码示例主要暂时开发一款Dubbo应用的主要步骤,但在【实际项目中】,我们会把服务接口放在一个单独的公共包里,这样可以提高重用性,也可使工程结构更加清晰规范。下面我们按照这个格式来做项目结构:
其中:
dubbo-demo-interface是接口提供方,它是所有【即将暴露出去提供业务逻辑】的接口集合;
dubbo-demo-provider是服务提供方,它负责实现并发布所有业务服务,以便外部接入;
dubbo-demo-consumer是服务消费方,即调用方;
当然,大家在工作中可以根据实际情况来定项目结构,后面我们将以该结构进行延展。
DubboDemo根目录下的父parent中,我们需要将依赖配置成,以便让子模块中可以直接继承依赖版本:
<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><dubbo.version>3.0.3</dubbo.version></properties><dependencyManagement><dependencies><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId><version>${dubbo.version}</version></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-dependencies-zookeeper</artifactId><version>${dubbo.version}</version><type>pom</type></dependency></dependencies></dependencyManagement>
3.3 实现服务并注册
首先我们在dubbo-demo-interface定义服务接口:
publicinterfaceHelloService { publicStringsayHello(Stringname); }
然后打开dubbo-demo-provider,在pom中引入dubbo-demo-interface及dubbo/zk相关依赖:
<dependencies><dependency><groupId>com.learn.dubbo</groupId><artifactId>dubbo-demo-interface</artifactId><version>${project.parent.version}</version></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-dependencies-zookeeper</artifactId><type>pom</type></dependency></dependencies>
现在,我们可以实现业务接口:
publicclassHelloServiceImplimplementsHelloService { publicStringsayHello(Stringname) { System.out.println("call sayHello "+name); return"hello "+name; } }
其中@DubboService是用于定义dubbo服务的注解。然后,新建配置类:
scanBasePackages="com.learn.dubbo.provider") ("classpath:/spring/dubbo-provider.properties") (publicclassProviderConfiguration { }
@EnableDubbo 启用Dubbo注解扫描,scanBasePackages配置扫描包路径
@PropertySource 设置provider常见配置信息,内容如下:
dubbo.application.name=hello-provider dubbo.registry.address=zookeeper://127.0.0.1:2181 dubbo.protocol.name=dubbo dubbo.protocol.port=20881
这里指定了zk的地址信息,以便服务能注册进去。dubbo.protocol.port设置成-1,则表示当前服务随机端口,这对服务集群时非常方便(在无须手动设置端口的情况下,避免端口占用问题)
然后,我们可以先启动本项目,使服务注册到注册中心,以便让消费者接入调用:
AnnotationConfigApplicationContextcontext=newAnnotationConfigApplicationContext(ProviderConfiguration.class); context.start(); System.in.read();
3.4 消费服务
打开dubbo-demo-consumer,同样加入如下依赖:
<dependencies><dependency><groupId>com.learn.dubbo</groupId><artifactId>dubbo-demo-interface</artifactId><version>${project.parent.version}</version></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-dependencies-zookeeper</artifactId><type>pom</type></dependency></dependencies>
配置类与配置信息
scanBasePackages="com.learn.dubbo.consumer") ("classpath:/spring/dubbo-consumer.properties") (value= {"com.learn.dubbo.consumer"}) (publicclassConsumerConfiguration { }
和前面的ProviderConfiguration类似,不过配置内容不一样:
dubbo.application.name=hello-consumer dubbo.registry.address=zookeeper://127.0.0.1:2181 dubbo.consumer.timeout=3000
为了让调用逻辑更加清晰,我们新增一个对外的服务类:
publicclassHelloAction { privateHelloServicehelloService; publicStringsayHelloAction(Stringname){ returnhelloService.sayHello(name); } }
不难看出,我们使用 @DubboReference注解来做注入,最后,启动该服务并调用之:
AnnotationConfigApplicationContextcontext=newAnnotationConfigApplicationContext(ConsumerConfiguration.class); context.start(); helloService.sayHelloAction("dubbo "+i)
在实际项目中,微服务实例不可能只有一个,也就是说provider一般是以集群的形式出现的,在consumer调用时,请求会路由到不同实例。验证方法也比较简单,首先多次启动provider(注意端口问题),然后让consumer循环调用,即可看出轮询结果。
3.5 负载均衡策略
Random LoadBalance:随机,调用量越大分布越均匀,这是默认配置
RoundRobin LoadBalance:轮询
LeastActive LoadBalance:最少活跃调用数,慢的provider收到较少的请求
ConsistentHash LoadBalance:一致性Hash,相同参数的请求总是发到同一提供者
我们可以在消费端引用时通过注解类配置负载策略:
loadbalance=LoadbalanceRules.ROUND_ROBIN) (
假如接口里面有多个方法,能否采用不同负载策略呢?当然是可以的。
我们可以打开HelloService,再定义一个方法:
publicdefaultStringqueryData(Stringcondition){ return""; }
default是Java8提供的,用于定义默认实现的接口方法,这里主要是为了避免在更改接口之后,需要强制修改实现类的情况,保证了兼容性。此时我们可以在消费端单独指定该方法的负载策略:
loadbalance=LoadbalanceRules.ROUND_ROBIN,methods= { (name="queryData",loadbalance=LoadbalanceRules.RANDOM ) (})
此时queryData方法将采用随机负载,而该service的其他方法将采用轮询负载。
四、序列化对象传输
前面我们的示例中,服务方法的参数和返回值都是比较简单的字符串类型,但在实际项目中,几乎都是以更为复杂的对象形式传参,下面我们先演示一个例子。
首先我们在dubbo-demo-interface中新建用于传输的类型,这里给出两个:
/*** 请求实体对象*/publicclassRequestVo { privateintid; privateStringname; /***getter setter略**/} /*** 响应实体对象*/publicclassResponseVo { privateintid; privateStringname; privateStringdata; /***getter setter略**/}
然后在HelloService中新增服务方法:
publicdefaultResponseVosaveData(RequestVorequestVo){ returnnull; }
在provider中实现该方法,此时我们重新启动provider并让consumer调用,会报出以下异常:
即传输对象未实现序列化接口,不能完成该调用,此时可以让RequestVo、ResponseVo实现java.io.Serializable,即可完成调用。
序列化是将对象转换成二进制流,反序列化是将二进制流转化成对象,在某种程度上,序列化/反序列化会直接影响通信效率。
目前Dubbo支持以下序列化方式:
1. hessian2:阿里基于hessian2改造之后的跨语言的序列化,目前是默认配置;
2. json序列化:目前有两种,即fastjson和dubbo json,但性能一般;
3. java序列化:JDK自带序列化,性能不好;
4. Kryo:目前在开源项目中运用广泛,性能较好;
5. FST: 比较新,性能较好,但缺乏更多成熟案例;
序列化生成字节大小比较
序列化实现 |
请求字节数 |
响应字节数 |
Kryo |
272 |
90 |
FST |
288 |
96 |
Dubbo Serialization |
430 |
186 |
Hessian |
546 |
329 |
FastJson |
461 |
218 |
Json |
657 |
409 |
Java Serialization |
963 |
630 |
远程调用方式 |
平均响应时间 |
平均TPS(每秒事务数) |
Dubbo: FST |
1.211 |
8244 |
Dubbo: kyro |
1.182 |
8444 |
Dubbo: serialization |
1.43 |
6982 |
Dubbo: hessian2 |
1.49 |
6701 |
Dubbo: fastjson |
1.572 |
6352 |
五、容错及重试机制
在大规模服务中,总会出现各种异常情况,比如服务卡顿、网络抖动等等,都会导致客户端在调用时出现问题。通常来讲,我们要“面向失败编程”,即提前规划好调用异常时的解决方案。目前,dubbo提供了5中容错模式:
1. Failover 失败时自动切换到下一个服务,常用于读操作
2. Failfast 失败时快速报错,常用于写操作
3. Failsafe 失败时直接忽略,常用于非核心操作,比如日志等
4. Forking 并行调用多个服务器,只要一个成功返回,常用于实时性要求较高的读操作
5. Broadcast 广播调用所有服务,任意一个失败就整体失败,常用于通知所有服务更新数据
在@DubboReference中配置cluster/retries即可:
( loadbalance=LoadbalanceRules.ROUND_ROBIN, methods= { name="queryData",loadbalance=LoadbalanceRules.RANDOM )} ( ,cluster=ClusterRules.FAIL_OVER, retries=2)
六、请求过滤器/拦截器
过滤器在早期的Web开发中非常常见,它主要用于提供请求前后自定义处理逻辑,比如登录鉴权、权限路由、日志打印等。这个概念被延伸到常规的业务开发中,很多框架自身都提供了类似前后拦截处理的机制,所以我们也称之为拦截器。Dubbo中的过滤器在执行RPC调用的时候生效,很多核心功能是基于它扩展而来的。Dubbo本身内置了可以直接开箱可用的过滤器,我们可以在resources下的META-INF\dubbo\internal\org.apache.dubbo.rpc.Filter中找到:
echo=org.apache.dubbo.rpc.filter.EchoFilter generic=org.apache.dubbo.rpc.filter.GenericFilter genericimpl=org.apache.dubbo.rpc.filter.GenericImplFilter token=org.apache.dubbo.rpc.filter.TokenFilter accesslog=org.apache.dubbo.rpc.filter.AccessLogFilter classloader=org.apache.dubbo.rpc.filter.ClassLoaderFilter context=org.apache.dubbo.rpc.filter.ContextFilter exception=org.apache.dubbo.rpc.filter.ExceptionFilter executelimit=org.apache.dubbo.rpc.filter.ExecuteLimitFilter deprecated=org.apache.dubbo.rpc.filter.DeprecatedFilter compatible=org.apache.dubbo.rpc.filter.CompatibleFilter timeout=org.apache.dubbo.rpc.filter.TimeoutFilter tps=org.apache.dubbo.rpc.filter.TpsLimitFilter
AccessLogFilter:请求日志过滤器
ExceptionFilter:异常处理过滤器
TpsLimitFilter:服务端限流过滤器
ExecuteLimitFilter:限制服务最大并行调用的过滤器
比如要配置AccessLogFilter生效,可以直接在@DubboService上设置:
accesslog="C:/opt/logs/provider.log") (
当然,我也可以自定义过滤器,步骤分为三步:
1. 定义A类,实现com.learn.dubbo.provider.filter接口,并使用@Activate启用
2. 将定义好的A,配置在META-INF/dubbo/org.apache.dubbo.rpc.Filter中
3. provider或者consumer指定配置
下面我们定义一个简单的用于打印请求信息的过滤器测试一下。首先,新建过滤器类:
group=PROVIDER) (publicclassPrintRequestInfoFilterimplementsFilter { publicResultinvoke(Invoker<?>invoker, Invocationinvocation) throwsRpcException { StringmethodName=invocation.getMethodName(); Class[] parameterTypes=invocation.getParameterTypes(); Object[] arguments=invocation.getArguments(); URLurl=invoker.getUrl(); Classcls=invoker.getClass(); System.out.println("serviceName:"+serviceName); System.out.println("methodName:"+methodName); System.out.println("parameterTypes:"+Arrays.toString(parameterTypes)); System.out.println("arguments:"+Arrays.toString(arguments)); System.out.println("url: "+url); System.out.println("cls: "+cls); returninvoker.invoke(invocation); } }
@Activate中的group指定该过滤器使用范围,这里指定了provider。
然后,在META-INF/dubbo/org.apache.dubbo.rpc.Filter(没有则新建)中配置:
printfilter=com.learn.dubbo.provider.filter.PrintRequestInfoFilter
最后,在使用的地方引用之:
accesslog="C:/opt/logs/provider.log",filter="printfilter") (
七、Dubbo Admin 体验
安装dubbo-admin控制台:
gitclonehttps://github.com/apache/dubbo-admin.gitcddubbo-adminmvncleanpackagecddubbo-admin-distribution/targetjava-Dserver.port=8081-jardubbo-admin-0.3.0.jar
默认账号root/root