文章目录
1. Dubbo与RPC的关系
1.1 什么是RPC?
维基百科这样解释:
远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用,所以,对于Java程序员而言,RPC就是远程方法调用。
如何理解RPC是一个计算机通信协议呢?我们已经知道RPC是专注于远程方法调用,如果实现远程方法调用,基本的就是通过网络,通过传输数据来进行调用。如下图所示
可以看到远程方法A 想要调用远程方法B,需要定义 数据类型 和 传输协议。而这些需要定义的东西作为一个协议存在于调用方和接收方,后续所有调用都遵守这个已制定的协议,这就是RPC通信协议。所以,我们其实可以看到RPC的自定义性是很高的,各个公司内部都可以实现自己的一套RPC框架,而Dubbo就是阿里所开源出来的一套RPC框架。
RPC和 HTTP、TCP的关系就是:RPC是基于HTTP、TCP协议来传输数据的,对于所传输的数据,可以交由RPC的双方来协商定义,但基本都会包括:
- 调用的是哪个类或接口
- 调用的是哪个方法,方法名和方法参数类型(考虑方法重载)
- 调用方法的入参
1.2 Dubbo与RPC的关系
上面说到实现RPC框架需要定义 数据类型 和 传输协议。而Dubbo作为阿里开源出来的RPC框架,已经制定好了对应的 传输数据类型 和传输协议,使用Dubbo必须遵循Dubbo制定好的规则。
Dubbo的传输协议见下文!
3. 自定义RPC框架思路
服务端:
- 注册服务到zk或redis。以map的形式保存起来,key = 服务名,value = List<服务器地址>。客户端请求可以负载到value的某个地址上。
注意:如果只把服务放在本地缓存中,那么其他的服务将调用失败,因为不同的服务属于不同的jvm,其他服务将无法感知另一个服务中的本地缓存。 - 把服务和服务的实现类注册到本地缓存。以map的形式保存起来,key = 服务名,value = 服务的实现类。目的是:当服务端接受到客户端请求,可以根据客户端传来的接口名,从本地缓存中拿到其实现类,然后通过反射调用客户端想要调用的方法
- 根据不同的协议启动不同的服务器。如果是Http协议则启动Tomcat,如果是Dubbo协议则启动Netty。
客户端:
-
指定传输的数据类型,包括接口(服务)名、方法名、参数类型、参数名,并封装成一个类Invocation。
-
当客户端调用某个接口时,采用jdk动态代理的方式,调用invoke代理方法,在代理方法中做增强逻辑。逻辑如下:
2.1:填充数据类型Invocation
2.2:从zk或redis中根据服务名拉取服务器地址,并负载均衡到某一个服务器地址下
2.3:获取客户端协议(dubbo 或 http),并根据协议向服务端发送数据Invocation -
客户端DispartchServlet拦截到客户端发过来的请求。通过JSON序列化二进制数据为Invocation 对象。根据对象中的接口名,从本地缓存中拿到对应的实现类,利用反射调用客户端客户端想要调用的方法,并输出。完成了远程服务调用!
2. Dubbo的基本使用
首先附上dubbo官方使用文档:https://dubbo.apache.org/zh/docs/v2.7/user/examples/loadbalance/
2.1 Dubbo是什么?
Apache Dubbo 是一款高性能、轻量级的开源 Java 服务框架,提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。
其中有以下几个关键点
-
注册与发现:Dubbo使用zookeeper做服务的注册中心,就是服务的提供者以临时节点的形式将服务Server信息注册保存到Zookeeper的dubbo目录下的provider的节点下,供消费者发现调用。
-
负载均衡: Dubbo支持负载均衡策略,就是同一个Dubbo服务被多台服务器启用后,会在在Zookeeper提供者节点下显示多个相同接口名称节点。消费者在调用Dubbo负载均衡服务时,采用权重的算法策略选择具体某个服务器上的服务,权重策略以*2倍数设置。
-
容错机制:Dubbo的提供者在Zookeeper上使用的是临时节点,一旦提供者所在服务挂掉,该节点的客服端连接将会关闭,故节点自动消失。所以消费者调用接口时将不会轮询到已经挂掉的接口上(延迟例外)。
-
Dubbo容器:Dubbo在java jvm中有自己的容器,和Spring IOC的bean一样,将服务对象保存到自己的容器中。
-
监控中心:监控中心主要是用来服务监控和服务治理。服务治理包含:负载均衡策略、服务状态、容错、路由规则限定、服务降级等。具体可以下载Dubbo监控中心客户端查看与设置。
-
Dubbo的协议:点击链接获取更多协议的详细信息
①:dubbo协议: Dubbo默认协议是dubbo协议,采用单一长连接和 NIO 异步通讯,基于hessian作为序列化协议,适合于数据量小但并发高的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
②:hessian协议: Hessian底层采用Http通讯(同步),走hessian序列化协议。适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用
③:http协议: 走json序列化,适用于浏览器查看,同时给应用程序和浏览器JS使用的服务。
④:rmi协议:走java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用
⑤:webservice协议:采用SOAP文本序列化,适用HTTP传输,常用于系统集成,跨语言调用
⑥:redis协议:基于 Redis实现的 RPC 协议。
⑦:rest协议:基于标准的Java REST API实现的REST调用支持
2.2 负载均衡
生产者在为某个接口暴露服务时,可以根据协议、ip、端口号、服务、group、version等六要素暴露多个接口实例,达到类似于集群的形式。如下所示:任意修改某个要素就算是这个接口已暴露的实例!在代码中可以通过修改@Service注解的值来暴露不同的服务实例@Service(interfaceName = "com.tuling.DemoService", version = "generic") 这样就会暴露http://ip:port/DemoService + generic服务,消费时要根据生产者暴露的规则来进行消费。
如果在application.properties配置文件中,配置了多个协议,Dubbo会默认会根据配置暴露多个服务实例,如果做下面的配置,那么上面的DemoService接口在zookeeper上就会有两个服务实例,一个Http的,一个Dubbo的!
# 配置多协议 # dubbo协议 dubbo.protocols.p1.id=dubbo1 dubbo.protocols.p1.name=dubbo dubbo.protocols.p1.port=20881 dubbo.protocols.p1.host=0.0.0.0 # http协议 dubbo.protocols.p2.id=dubbo2 dubbo.protocols.p2.name=http dubbo.protocols.p2.port=20882 dubbo.protocols.p2.host=0.0.0.0
那么面对多个服务实例,消费端调用时是如何进行选择的呢?Dubbo为我们提供了四种负载均衡策略,可以通过负载均衡策略来选择一个服务实例进行调用!默认的负载策略为 random 随机调用。四种策略如下:
- Random 随机:按权重设置随机概率,可通过配置权重修改概率
- RoundRobin 轮询:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
- LeastActive 最少活跃数:活跃数是指调用前后的计数差,服务调用越快,活跃数越小。提供者越慢,接收的请求就越少,因为越慢的提供者的调用前后计数差会越大,活跃数也会变大
- ConsistentHash 一致性Hash:相同参数的请求总是发到同一提供者。
注意:比较难理解的是LeastActive 最少活跃数,理论上最少活跃数应该是在服务提供者端进行统计的,服务提供者统计有多少个请求正在执行中。但是Dubbo却选择在消费端进行统计最少活跃数,为什么能在消费端进行统计?逻辑如下:
- 消费者会缓存所调用服务的所有提供者,比如记为p1、p2、p3三个服务提供者,每个提供者内都有一个属性记为active,默认位0
- 消费者在调用次服务时,如果负载均衡策略是leastactive
- 消费者端会判断缓存的所有服务提供者的active,选择最小的,如果都相同,则随机选出某一个服务提供者后,假设位p2,Dubbo就会对p2.active+1
- 然后真正发出请求调用该服务
- 消费端收到响应结果后,对p2.active-1
- 这样就完成了对某个服务提供者当前活跃调用数进行了统计,并且并不影响服务调用的性能,下次调用会再次判断最小的active,这就解释了为什么服务提供者越慢,接收的请求就越少!因为它的active值大!
配置方式
- Provider端配置:生产者通过在暴露服务的@Servic注解上进行配置:@Service(loadbalance = "roundrobin"),配置时需要注意负载均衡方式均为小写!
- Consumer端配置:消费端通过@Reference(loadbalance = "leastactive ")
如果Provider和Consumer都配置,则以Consumer端配置的为准!
2.3 服务超时
在服务提供者(服务端)和服务消费者上都可以配置服务超时时间,这两者是不一样的。
@Service(version = "timeout", timeout = 4000) //服务提供者端超时时间 @Reference(version = "timeout", timeout = 3000,retries = 1) //服务消费者端超时时间
消费者调用一个服务,分为三步:
- 消费者发送请求(网络传输)
- 服务端执行服务
- 服务端返回响应(网络传输)
如果在服务端和消费端只在其中一方配置了timeout
那么没有歧义,表示消费端调用服务的超时时间,消费端如果超过时间还没有收到响应结果,则消费端会抛超时异常,但,服务端不会抛异常,服务端在执行服务后,会检查执行该服务的时间,如果超过timeout,则会打印一个超时日志。服务会正常的执行完。
如果在服务端和消费端各配了一个timeout
那情况就比较复杂了,假设
- 服务执行为5s
- 消费端timeout=3s
- 服务端timeout=6s
那么消费端调用服务时,消费端会收到超时异常(因为消费端超时了),服务端一切正常(服务端没有超时)。如果配置服务端timeout=4s,那么由于服务执行为5s,所以服务端也会打印警告,标识服务端也超时了!
2.4 集群容错
一个服务提供多个实例(集群),集群容错是指:集群容错表示:服务消费者在调用某个服务时,这个服务有多个服务提供者,在经过负载均衡后选出其中一个服务提供者之后进行调用,但调用报错后,Dubbo所采取的后续处理策略。如图:如果服务实例1调用失败,则会尝试调用服务实例2或者3,默认重试2次。
集群容错可以在@Service 和 @Reference注解上进行配置:如果两者都配置,以消费端为主!
@Service( cluster = "failfast") //服务端超集群容错 @Reference(cluster = "failfast") //消费端集群容错
Dubbo提供了六种集群容错方案:
- Failover Cluster:失败自动切换
当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。 - Failfast Cluster:快速失败
只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录 - Failsafe Cluster:失败安全
出现异常时,不抛异常,直接忽略。通常用于写入审计日志等操作 - Failback Cluster:失败自动恢复
后台记录失败请求,定时重发。通常用于消息通知操作 - Forking Cluster:并行调用多个服务器
只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数 - Broadcast Cluster:广播调用所有提供者
逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息
2.5 服务降级
服务降级表示:服务消费者在调用某个服务提供者时,如果该服务提供者报错了,所采取的措施。可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。集群容错和服务降级的区别在于:
- 集群容错是整个集群范围内的容错
- 服务降级是单个服务提供者的自身容错
服务降级可以在消费端的 @Reference注解上使用mock来指定降级方案
- mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
- mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
//服务降级:如果调用失败返回123 @Reference(version = "timeout", timeout = 1000, mock = "fail: return 123")
更多服务降级方案可参考本地伪装:https://dubbo.apache.org/zh/docs/v2.7/user/examples/local-mock/
本地伪装其实也是对Mock的应用,便于服务端在客户端执行容错逻辑
2.6 本地存根
消费端通过Dubbo远程调用服务端,其业务实现基本都在服务端。但有些时候想在消费端也执行部分逻辑,比如:做 ThreadLocal 缓存(这个用处最大),提前验证参数,调用失败后伪造容错数据等等,此时就需要在@Reference中带上 Stub,消费端生成服务的代理 Proxy 实例,会把 Proxy 通过构造函数传给 Stub,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
//本地存根,开启stub @Reference(stub = "true") 或者 @Reference(stub = "com.foo.DemoServiceStub") //指定stub对象
还需要自定义一个类实现DemoService接口,表示为DemoService做的本地存根,这个类是放在消费端的
public class DemoServiceStub implements DemoService { private final DemoService demoService; // 构造函数传入真正的远程代理对象 public DemoServiceStub(DemoService demoService){ this.demoService = demoService; } @Override public String sayHello(String name) { // 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等 try { return demoService.sayHello(name); // safe null } catch (Exception e) { // 你可以容错,可以做任何AOP拦截事项 return "容错数据"; } } }
注意:实现类中必须有一个传入远程 DemoService 实例的构造函数
使用上述存根代码执行后,如果调用失败,则会执行DemoServiceStub中的容错方案,控制台打印”容错数据“!
2.7 参数回调
参数回调是指:当消费端调用服务成功后,希望服务端能够回调一下消费端的逻辑
既然是服务端回调消费端的逻辑,那么这个逻辑一定是存在消费端的!以DemoService服务为例
消费端调用:
@Reference(version = "callback") private DemoService demoService; //调用服务 demoService.sayHello("aaa", "d1", new DemoServiceListenerImpl())
上述代码new DemoServiceListenerImpl()中要包含着具体的回调逻辑
// 回调逻辑接口 public interface DemoServiceListener { void changed(String msg); }
// 回调逻辑实现类 public class DemoServiceListenerImpl implements DemoServiceListener { @Override public void changed(String msg) { System.out.println("被回调了:"+msg); } }
服务端回调
// DemoService接口 public interface DemoService { // 回调方法 default String sayHello(String name, String key, DemoServiceListener listener) { return null; }; }
// @Method注明了是sayHello()中索引为2的参数参与了回调,以及最大同时支持3个回调,上述代码只有一个,如果写4个就报错 @Service(version = "callback", methods = {@Method(name = "sayHello", arguments = {@Argument(index = 2, callback = true)})}, callbacks = 3) public class CallBackDemoService implements DemoService { @Override public String sayHello(String name, String key, DemoServiceListener callback) { callback.changed(); //代理对象直接回调消费端的changed方法 return ""; // 正常访问 } }
在服务端回调时,需要注意:
- sayHello()方法中的DemoServiceListener为代理对象,并不是消费端传过来的DemoServiceListenerImpl对象
- 需要在@Service中使用@Method注明是哪个方法中哪个参数参与了回调,以及最大同时支持几个回调
结果:
在消费端打印的是changed的内容,但这个方法是在服务端被执行的
2.8 异步调用
上文所讲的内容都是依托于同步调用的,Dubbo也提供了异步调用方式,异步调用与同步调用的求别在于:
- 服务端需要使用CompletableFuture.supplyAsync开启一个线程执行任务
- 客户端需要使用CompletableFuture.whenComplete监听异步线程执行完毕
消费端代码示例:
@Reference(version = "async") private DemoService demoService; public static void main(String[] args) throws IOException { ConfigurableApplicationContext context = SpringApplication.run(AsyncDubboConsumerDemo.class); DemoService demoService = context.getBean(DemoService.class); // 调用直接返回CompletableFuture CompletableFuture<String> future = demoService.sayHelloAsync("异步调用"); // 5 //这个方法只有等异步线程执行结束才会调用 future.whenComplete((v, t) -> { if (t != null) { t.printStackTrace(); } else { System.out.println("Response: " + v); } }); System.out.println("结束了"); }
服务端代码示例
public interface DemoService { // 同步调用方法 String sayHello(String name); // 异步调用方法 default CompletableFuture<String> sayHelloAsync(String name) { return null; }; }
@Service(version = "async") public class AsyncDemoService implements DemoService { //同步调用 @Override public String sayHello(String name) { System.out.println("sayhello方法 " + name); return name; } // 主要关注这个异步调用 @Override public CompletableFuture<String> sayHelloAsync(String name) { System.out.println("执行了异步服务" + name); //相当于在异步线程里执行sayHello方法! return CompletableFuture.supplyAsync(() -> { return sayHello(name); }); } }
执行结果如下:
消费端:
服务端:
可以看到他们之间打印的顺序也是异步的体现!
2.9 泛化调用、泛化服务
泛化调用: 在Dubbo中,如果某个服务想要支持泛化调用,就可以将该服务的generic属性设置为true,那对于服务消费者来说,就可以不用依赖该服务的接口,直接利用GenericService接口来进行服务调用。泛化调用可以用来做服务测试。
@EnableAutoConfiguration public class GenericDubboConsumerDemo { //调用DemoService服务,并不需要注入DemoService,也不需要引入依赖 @Reference(id = "demoService", version = "default", interfaceName = "com.tuling.DemoService", generic = true) private GenericService genericService; public static void main(String[] args) throws IOException { ConfigurableApplicationContext context = SpringApplication.run(GenericDubboConsumerDemo.class); GenericService genericService = (GenericService) context.getBean("demoService"); Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"周瑜"}); System.out.println(result); } }
泛化服务: 可以不实现具体的某个接口,而是实现GenericService接口,并在@Service上标明接口名即可,在调用直接注入DemoService就可以使用!
@Service(interfaceName = "com.tuling.DemoService", version = "generic") public class GenericDemoService implements GenericService { @Override public Object $invoke(String s, String[] strings, Object[] objects) throws GenericException { System.out.println("执行了generic服务"); return "执行的方法是" + s; } }
3. dubbo的REST协议
dubbo支持多种远程调用方式,例如dubbo RPC(二进制序列化 + tcp协议)、http invoker(二进制序列化 + http协议)、hessian(二进制序列化 + http协议)、WebServices (文本序列化 + http协议)、REST(文本序列化 + http协议)等等的支持。
当我们用Dubbo提供了一个服务后,如果消费者没有使用Dubbo也想调用服务,那么这个时候就可以让我们的服务支持REST协议,这样消费者就可以通过REST形式调用我们的服务了。更多REST协议内容点击查看官网!
①:服务端配置文件修改协议为rest
dubbo.protocol.name=rest
②:服务端实现:使用@Path指定Rest风格的访问路径(注意:所有暴露的服务都必须加@Path)
@Service(version = "rest") @Path("demo") public class RestDemoService implements DemoService { @GET @Path("say") @Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8}) @Override public String sayHello(@QueryParam("name") String name) { System.out.println("执行了rest服务" + name); URL url = RpcContext.getContext().getUrl(); return String.format("%s: %s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问 } }
这样就可以通过浏览器访问这个服务了,其他消费端也可以直接通过HttpCliet等非dubbo的形式去调用服务!
4. dubbo的控制台
5. dubbo的服务路由
经过服务路由可以配置黑名单、白名单、读写分离、隔离不同机房网段等等,这点在官网有很详细的解释 点击查看官网详情!!!
dubbo提供的标签路由还可以用来发布版本,什么是蓝绿发布、灰度发布?