1.RPC 场景和过程
1.1RPC 场景
在微服务环境下,存在大量的跨 JVM 进行方法调用的场景,如下图:
具体到某一个调用来说,希望 A 机器能通过网络,调用 B 机器内的某个服务方法,并得到返回值:
1.2RPC 的实现切入口
从本质上来讲,某个 jvm 内的对象方法,是无法在 jvm 外部被调用的,如下的代码:
OrderService orderService = (OrderService) ctx.getBean("orderService");
OrderEntiry entiry = orderService.getDetail("1");
orderService.getDetail("1")的这一句调用,是无法脱离本地 jvm 环境被调用的。但是,好在 java 中的对象方法的调用,还有反射模式的调用:
Method method = target.getClass().getMethod(methodName, argTypes);
return method.invoke(target, args);
只要我们传入反射需要的目标对象 orderService,方法名称 getDetail和参数值"1",反射就能将 orderService.getDetail 调用起来于是,我们可以把调用某个方法的过程,改造成这样:
Map info = new HashMap();
info.put("target","orderService");
info.put("methodName","getDetail");
info.put("arg","1");
//反射调用
Object result = InvokeUtils.call(info,ctx);
现在,只要谁告诉我了反射需要信息,target/method/arg,我就能调用本地的任何对象方法了。
1.3网络通信传递反射信息
1.3.1 定义一个继承自 remote 接口的类
public interface InfoService extends Remote {//继承 remote 接口
String RMI_URL = "rmi://127.0.0.1:9080/InfoService";
int port = 9080;
Object passInfo(Mapinfo) throws RemoteException;
}
创建一个实现类(为简化实现,继承 UnicastRemoteObject 类):
public class InfoServiceImpl extends UnicastRemoteObject implements InfoService {
public InfoServiceImpl() throws RemoteException {
super();
}
@Override
public Object passInfo(Mapinfo) {
System.out.println("恭喜你,调通了,参数:"+JSON.toJSONString(info));
info.put("msg","你好,调通了!");
return info;}
}
1.3.2 RMI 开放服务到指定 URL
只需要将实例绑定注册到指定的 URL 和 port 上,远程即可调用此实例;
InfoService infoService = new InfoServiceImpl();
//注冊通讯端口
LocateRegistry.createRegistry(9080);
//注冊通讯路径
Naming.bind("rmi://127.0.0.1:9080/InfoService", infoService);
1.3.3 RMI 远程通过 URL 连接并调用
//取远程服务实现
infoService = (InfoService) Naming.lookup(InfoService.RMI_URL);
//呼叫远程反射方法
Mapinfo = new HashMap();
info.put("target","orderService");
info.put("methodName","getDetail");
info.put("arg","1");
Object result = infoService.passInfo(info);
至此,已经实现了通过 RMI 跨机器传递 target/method/arg。
1.4远程调用的融合
可以看到,前两步,已经实现了跨机器的反射信息收发和反射动作调用。我们现在只需要在 InfoService 的实现上,对传递过来的 info 信息,直接发起反射调用即可。
InfoService infoService = new InfoServiceImpl(){
public Object passInfo(Mapinfo) { //对象,方法,参数
super.passInfo(info); //info 内包含的信息,是反射需要的信息
Object result = InvokeUtils.call(info,ctx);
System.out.println("测试 InvokeUtils.call 调用功能,调用结果:" + JSON.toJSONString(result));
return result;
}
};
这样,远程机器只要通过 infoService 传递信息过来,就自动将目标服务反射调用,并返回结果值回去,整个 RPC 过程完成。
1.5对客户端友好的透明化封装
虽然在上一节中,RPC 的整个调用链条已经拉通,但是我们发现还有一个易出错的地方,就是客户端封装反射信息的地方,功能不够内聚,容易出现手误,代码易读性也很差。
//呼叫远程反射方法
Mapinfo = new HashMap();
info.put("target","orderService");
info.put("methodName","getDetail");
info.put("arg","1");
Object result = infoService.passInfo(info);
有什么改善的办法呢?仔细核对,其实 target/method/arg 这三个信息,全部都可以从接口方法调用OrderService.getDetail("1")中得到。
于是,我们可以为此接口做一个代理对象,在代理对象内部完成反射信息的包装:
OrderService service = new OrderService(){
@Override
public OrderEntiry getDetail(String id) {
Map info = new HashMap();
// 写死了反射的目标,静态代理
info.put("target","orderService");//对象
info.put("methodName","getDetail");//方法
info.put("arg",id);//参数
OrderEntiry result = null;
try {
result = (OrderEntiry)infoService.passInfo(info);
} catch (RemoteException e) {
e.printStackTrace();
}
return result;
}
};
大功告成,从此,客户端远程传递反射信息的过程,直接变成调用接口的代理对象即可。
调用者,甚至不再需要区分,此接口代理对象到底是谁,像调用正常的本地服务一起使用就 ok。
1.6Dubbo 使命
在上面的章节,我们重点详述了,一个具体的 RPC 调用的全过程。那么在现实工作中,服务节点间的 RPC 调用是非常普遍并且错踪复杂的。
我们除了要关心 RPC 的过程实现,还需要考虑:
1.服务方是集群时,如何挑选一台机器来响应客户端?
2.因网络抖动引起的调用失败,如何重试来弥补?
3.服务方机器的动态增减,如何能够让客户端及时了解到并做出调整?
..... Dubbo 的使命,即是解决上述围绕 RPC 过程的一览子问题
2. Dubbo 简介
在分布式服务架构下,各个服务间的相互 rpc 调用会越来越复杂。最终形成网状结构,此时服务的治理极为关键。
Dubbo 是一个带有服务治理功能的 RPC 框架,提供了一套较为完整的服务治理方案,其底层直接实现了 rpc 调用的全过程,并尽力做事 rpc 远程对使用者透明。下图展示了 Dubbo 服务治理的功能。
简单的说,Dubbo 就是个服务调用的框架,如果没有分布式的需求,其实是不需要用的,只有在分布式的时候,才有使用 Dubbo 这样的分布式服务框架的需求,并且本质上是个服务调用的东东。
其核心部分包含:
远程通讯:提供对多种基于长连接的 NIO 框架抽象封装,包括多种线程模型、序列化以及“请求-响应”模式的信息交换方式。
集群容错:提供基于接口方法的透明远程过程调用,包括多协议支持以及软负载均衡,失败容错、地址路由、动态配置等集群支持。
自动发现:基于注册中心目录服务,使服务消费方能动态的查询×××提供方,使地址透明,使服务提供方可以平滑增加或减少机器。
2.1dubbo 的架构及特点
下图展示了 dubbo 的整体结构:
Dubbo 总体架构设计一共划分了 10 层,而最上面的 Service 层是留给实际想要使用 Dubbo 开发分布式服务的开发者实现业务逻辑的接口层。图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
服务接口层(Service):该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。
配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心,可以直接 new 配置类,也可以通过 Spring 解析配置生成配置类。
服务代理层(Proxy):服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton,以 ServiceProxy 为中心,扩展接口为 ProxyFactory。
服务注册层(Registry):封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry 和 RegistryService。可能没有服务注册中心,此时服务提供方直接暴露服务。
集群层(Cluster):封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBalance。将多个服务提供方组合为一个服务提供方,实现对服务消费方透明,只需要与一个服务提供方进行交互。
监控层(Monitor):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService。
远程调用层(Protocol):封将 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocol、Invoker 和 Exporter。Protocol 是服务域,它是 Invoker暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。Invoker 是实体域,它是 Dubbo 的核心模型,其他模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用。它有可能是一个本地的实现,也可能是一个远程的实现,也可能是一个集群实现。
信息交换层(Exchange):封装请求响应模式,同步转异步,以 Request和 Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer。
网络传输层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec。
数据序列化层(Serialize):可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool。
从上图可以看出,Dubbo 对于服务提供方和服务消费方,从框架的 10 层中分别提供了各自需要关心和扩展的接口,构建整个服务生态系统(服务提供方和服务消费方本身就是一个以服务为中心的)。
2.2Dubbo 服务的角色关系
服务提供方和服务消费方之间的调用关系,如图所示:
节点角色说明:
调用关系说明:
0:服务容器负责启动,加载,运行服务提供者。
1:服务提供者在启动时,向注册中心注册自己提供的服务。
2:服务消费者在启动时,向注册中心订阅自己所需的服务。
3:注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
4:服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
5:服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
3. Dubbo 的基础配置使用
3.1xml 配置方式
3.1.1 dubbo 功能标签集
dubbo 标签的使用以及标签属性详解见下。
3.1.2 标签详解
所有配置项分为三大类,参见下表中的"作用"一列。
服务发现:表示该配置项用于服务的注册与发现,目的是让消费方找到提供方。
服务治理:表示该配置项用于治理服务间的关系,或为开发测试提供便利条件。
性能调优:表示该配置项用于调优性能,不同的选项对性能会产生影响。 所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方,各属性对应 URL 的参数,参见配置项一览表中的"对应 URL 参数"列。
注意:只有 group,interface,version 是服务的匹配条件,三者决定是不是同一个服务,其它配置项均为调优和治理参数。
服务提供者暴露服务配置:
配置类:com.alibaba.dubbo.config.ServiceConfig
服务消费者引用服务配置:
配置类:com.alibaba.dubbo.config.ReferenceConfig
服务提供者协议配置:
配置类:com.alibaba.dubbo.config.ProtocolConfig
说明:如果需要支持多协议,可以声明多个
注册中心配置:
配置类:com.alibaba.dubbo.config.RegistryConfig
说明:如果有多个不同的注册中心,可以声明多个
监控中心配置:
配置类:com.alibaba.dubbo.config.MonitorConfig
应用信息配置:
配置类:com.alibaba.dubbo.config.ApplicationConfig
模块信息配置:
配置类:com.alibaba.dubbo.config.ModuleConfig
服务提供者缺省值配置:
配置类:com.alibaba.dubbo.config.ProviderConfig
说明:该标签为
服务消费者缺省值配置:
配置类:com.alibaba.dubbo.config.ConsumerConfig
说明:该标签为
方法级配置:
配置类:com.alibaba.dubbo.config.MethodConfig
说明:该标签为
比如:
方法参数配置:
配置类:com.alibaba.dubbo.config.ArgumentConfig
说明:该标签为
选项参数配置:
配置类:java.util.Map
说明:该标签为
3.2注解方式
注解方式的底层与 XML 一致,只是表现形式上的不同。目标都是将 Dubbo 基础信息配入,主要涉及以下五个必不可少的信息:ApplicationConfig、ProtocolConfig 、 RegistryConfig、service、reference 。
3.2.1 EnableDubbo 开启服务
@EnableDubbo:开启注解 Dubbo 功能 ,其中可以加入 scanBasePackages 属性配置包扫描的路径,用于扫描并注册 bean。其中 封装了组件 @DubboComponentScan,来扫描 Dubbo @Service 注解暴露 Dubbo 服务,以及扫描 Dubbo @Reference 字段或者方法注入 Dubbo 服务代理。
其它 Dubbo 三种公共信息的配置,有两种方式,根据自己喜好选用。
3.2.2 Configuration 方式配置公共信息
@Configuration 方式:分别将 ApplicationConfig、ProtocolConfig 、 RegistryConfig 创建到 IOC容器中即可,如下:
@Configuration
@EnableDubbo(scanBasePackages = "com.enjoy.service")
class ProviderConfiguration {
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("busi-provider");
return applicationConfig;
}
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setProtocol("zookeeper");
registryConfig.setAddress("192.168.0.128");
registryConfig.setPort(2181);
return registryConfig;
}
@Bean
public ProtocolConfig protocolConfig() {
ProtocolConfig protocolConfig = new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setPort(20880);
return protocolConfig;
}
}
3.2.3 Property 方式自动装配公共信息
Property 方式:使用 Springboot 属性文件方式,由 Dubbo 自动将文件信息配置入容器,示例如下:
@Configuration
@EnableDubbo(scanBasePackages = "com.enjoy.service")
@PropertySource("classpath:/dubbo-provider.properties")
static class ProviderConfiguration {}
dubbo-provider.properties 文件内容
dubbo.application.name=busi-provider
dubbo.registry.address=zookeeper://192.168.0.128:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880