概述
本文的目的是想说明Dubbo的整体设计,以及这样设计的原因。
我们先来看一个简单的RPC调用程序:
服务接口及实现
package com.alibaba.study.rpc.service;
/**
* HelloService
*/
public interface HelloService {
/**
* 服务方法
*
* @param name
* @return
*/
String hello(String name);
}
---
package com.alibaba.study.rpc.service.impl;
import com.alibaba.study.rpc.service.HelloService;
/**
* HelloServiceImpl
*/
public class HelloServiceImpl implements HelloService {
@Override
public String hello(String name) {
return "Hello " + name;
}
}
RPC框架
package com.alibaba.study.rpc.framework;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.ServerSocket;
import java.net.Socket;
/**
* RpcFramework
*/
public class RpcFramework {
/**
* 暴露服务
*
* @param service 服务实现
* @param port 服务端口
* @throws Exception
*/
public static void export(final Object service, int port) throws Exception {
if (service == null) {
throw new IllegalArgumentException("service instance == null");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Export service " + service.getClass().getName() + " on port " + port);
// 以指定端口创建ServerSocket
ServerSocket server = new ServerSocket(port);
for (; ; ) {
try {
// 等待接收请求
final Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
try {
// 获取请求的数据流
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
// 获取客户端请求的方法名
String methodName = input.readUTF();
// 获取客户端请求的参数类型列表
Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
// 获取客户端请求的参数列表
Object[] arguments = (Object[]) input.readObject();
// 创建对象输出流对象,用于响应结果给客户端
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try {
// 通过反射,获取服务接口指定的方法
Method method = service.getClass().getMethod(methodName, parameterTypes);
// 反射调用
Object result = method.invoke(service, arguments);
// 将结果响应给客户端
output.writeObject(result);
} catch (Throwable t) {
output.writeObject(t);
} finally {
output.close();
}
} finally {
input.close();
}
} finally {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 引用服务
*
* @param <T> 接口泛型
* @param interfaceClass 接口类型
* @param host 服务器主机名
* @param port 服务器端口
* @return 远程服务
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static <T> T refer(final Class<T> interfaceClass, final String host, final int port) throws Exception {
if (interfaceClass == null) {
throw new IllegalArgumentException("Interface class == null");
}
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("The " + interfaceClass.getName() + " must be interface class!");
}
if (host == null || host.length() == 0) {
throw new IllegalArgumentException("Host == null!");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port);
/**
* 使用JDK的动态代理创建接口的代理对象
* 说明:
* 在 InvocationHandler#invoke方法内部实现Socket与ServerSocket的通信。当使用代理对象调用方法时,内部使用Socket进行通信,然后把通信的结果返回。
*/
return (T) Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class<?>[]{interfaceClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
// 创建Socket,用于连接ServerSocket
Socket socket = new Socket(host, port);
try {
// 创建用于发送数据到ServerSocket的输出流
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try {
//--------------------- 数据契约 ----------------------------/
// 方法名
output.writeUTF(method.getName());
// 参数类型
output.writeObject(method.getParameterTypes());
// 参数值
output.writeObject(arguments);
//------------------------ 数据契约 --------------------------/
// 创建用于接收ServerSocket的输入流
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
// 读取ServerSocket响应的数据
Object result = input.readObject();
if (result instanceof Throwable) {
throw (Throwable) result;
}
// 返回结果
return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
});
}
}
服务暴露
package com.alibaba.study.rpc.provider;
import com.alibaba.study.rpc.framework.RpcFramework;
import com.alibaba.study.rpc.service.HelloService;
import com.alibaba.study.rpc.service.impl.HelloServiceImpl;
/**
* RpcProvider
*/
public class RpcProvider {
public static void main(String[] args) throws Exception {
// 服务实现
HelloService service = new HelloServiceImpl();
// 暴露服务
RpcFramework.export(service, 1234);
}
}
引用服务
package com.alibaba.study.rpc.consumer;
import com.alibaba.study.rpc.framework.RpcFramework;
import com.alibaba.study.rpc.service.HelloService;
/**
* RpcConsumer
*/
public class RpcConsumer {
public static void main(String[] args) throws Exception {
// 引用服务代理对象
HelloService service = RpcFramework.refer(HelloService.class, "127.0.0.1", 1234);
while (true) {
String hello = service.hello("World");
System.out.println(hello);
Thread.sleep(1000);
}
}
}
这个例子中,通信是使用同步阻塞的Socket来实现的,采用端对端的方式。远程调用使用的是JDK的动态代理,在invoke方法中实现网络通信。参数序列化使用的是JDK的ObjectStream。一个完善的RPC框架其实就是在这例子的基础上进行多方位扩展和改进。比如,网络通信可以使用性能更好的NIO框架Netty,动态代理可以使用javaassist字节码生成方式[注意:不是javaassist提供的动态代理接口,该接口比JDK自带的还慢],序列化方式可以采用fastjson、hession2以及kryo等技术。如果服务数量达到一定规模,可以引进注册中心进行服务的治理。节点间的通信方式可以有多种,因此可以扩展多协议。除此之外,性能和健壮性也是一个优秀的RPC框架所必须的,如集群容错、负载均衡、重试机制、服务降级…这些都会在后面分析的Dubbo框架中得到很好的体现。
RPC框架的核心流程
1、生产者server:加载服务接口,并缓存;服务注册,将服务接口以及服务主机信息写入注册中心(本例使用的是 zookeeper),启动网络服务器并监听。消费方(client)调用以本地调用方式调用服务;
2、client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;代理服务接口生成代理对象,服务发现(连接 zookeeper,拿到服务地址列表,通过客户端负载策略获取合适的服务地址)。
3、client stub找到服务地址,并将消息发送到服务端;
4、server stub收到消息后进行解码;
5、server stub根据解码结果调用本地的服务;
6、本地服务执行并将结果返回给server stub;
7、server stub将返回结果打包成消息并发送至消费方;
8、client stub接收到消息,并进行解码;
9、服务消费方得到最终结果。
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。JAVA一般使用动态代理方式实现远程调用。
概述
首先,一个RPC框架的最基本的内容是什么?
我们先来思考下,其实RPC框架想要达成的一个效果就是发起一个远程调用就像本地调用一样,只不过中间的过程被框架封装了,对用户是透明的。
那么达成这个效果,需要下面三种技术。
1、动态代理。包装用户的接口,将远程调用透明化。
2、网络框架。需要封装网络框架给生产者和消费者。
3、序列化和反序列化。将用户发送和服务器响应数据进行网络传输。
我们可以通过上述三部分就可以完成一次远程调用。
如果考虑到分布式等场景。还需要如下内容:
1、注册中心。
2、负载均衡策略。
那么,我们对一次远程抽象应该是这样的:
Result invoke(Invocation invocation);
1、代表一次远程方法调用,invoke。
2、方法入参类型等等上下文,invocation。
3、请求结果,Result。
Dubbo中所有的模块都是围绕这个Invoker建立的,它代表着一次完整的远程调用。
然后在调用执行过程中,才会有序列化和反序列化、网络请求模块、等等的过程,都是在invoker中开始的。
引用Dubbo文档中的一段话就是:Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。
Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。
任何框架或组件,总会有核心领域模型,比如:Spring 的 Bean,Struts 的 Action,Dubbo 的 Service,Napoli 的 Queue 等等。这个核心领域模型及其组成部分称为实体域,它代表着我们要操作的目标本身。实体域通常是线程安全的,不管是通过不变类,同步状态,或复制的方式。
服务域也就是行为域,它是组件的功能集,同时也负责实体域和会话域的生命周期管理, 比如 Spring 的 ApplicationContext,Dubbo 的 ServiceManager 等。服务域的对象通常会比较重,而且是线程安全的,并以单一实例服务于所有调用。
什么是会话?就是一次交互过程。会话中重要的概念是上下文,什么是上下文?比如我们说:“老地方见”,这里的“老地方”就是上下文信息。为什么说“老地方”对方会知道,因为我们前面定义了“老地方”的具体内容。所以说,上下文通常持有交互过程中的状态变量等。会话对象通常较轻,每次请求都重新创建实例,请求结束后销毁。简而言之:把元信息交由实体域持有,把一次请求中的临时状态由会话域持有,由服务域贯穿整个过程。
构建框架的模块组合关系时会有两种情况:
1、模块需要根据入参来选择。
2、模块可以在启动的时候就确定。
理解这个很重要。
理论上我们可以通过Protocol就能够完成一次Dubbo的远程调用,Protocol层是为了给Config层使用的,主要是用来获取Invoker,其他的模块不重要,或者说不影响核心流程。重要的模块是Protocol,Invoker,Exchange和Transport。然后回到我们上文中提到的:
- 1、动态代理。包装用户的接口,将远程调用透明化。
- 2、网络框架。需要封装网络框架给生产者和消费者。
- 3、序列化和反序列化。将用户发送和服务器响应数据进行网络传输。
上面3个部分都可以抽象出来,具体实现可以有多种,比如:
- 1、动态代理可以有JDK自带的,还可以是Javassist。
- 2、网络框架我们可以用BIO,还可以使用性能更好的NIO的Netty还有Mina。
- 3、序列化和反序列化,我们可以选择JDK的序列化,还可以使用Hession的。
这些都是可能根据使用场景不同而变化的,而封装变化就需要我们定义出对应的接口:
比如Dubbo中针对动态代理定义了Proxy。
目前Dubbo中不支持使用BIO。而针对NIO的进行了封装Transport。
序列化和反序列化Dubbo定义了Serialization。
Dubbo中分层是通过接口调用的,而且针对不同的参数可以动态指定不同的实现。这种设计方式称为微内核的方式,就是需要识别框架中可能发生变更的部分抽象成接口,然后使用SPI的方式灵活的切换实现。微内核是一种面向功能进行拆分的可扩展性架构。内核功能是比较稳定的,只负责管理插件的生命周期,不会因为系统功能的扩展而不断进行修改。功能上的扩展全部封装到插件之中,插件模块是独立存在的模块,包含特定的功能,能拓展内核系统的功能。
以上是Dubbo的总体的设计原则。
SPI
API和SPI的区别:
- API是接口和实现类都由服务提供方实现,调用方仅仅只有调用方法的权限,一般的,如果接口和实现类位于同一个jar包体系中,就属于API。
- SPI,则是由服务调用方提供接口,由服务实现方(提供方)提供接口的实现。
SPI全名Service Provider Interface,它是 JDK 内置的一个服务发现机制, SPI机制使得接口和具体实现完全解耦,接口和实现可以不位于同一个jar包中。我们只需要声明接口,具体的实现类在的配置中选择即可。SPI早在Java6的时候就被引入JDK中了。
JDK的SPI原理如下
JDK的SPI机制的简单原理如下:
- 服务实现的查找、加载是通过JDK自带的java.util.ServiceLoader类的load静态方法实现的,该方法会查找全部本地以及引入的jar包中的META-INF/services目录下的和接口同名的文件中的具体服务实现类。
- 随后会将服务实现类封装到一个ServiceLoader对象中,该对象是一个Iterable的实现,可以进行for-each循环迭代!此后,我们就可以对ServiceLoader进行迭代来选择适合自己需求的服务实现。
JDK的SPI机制有如下缺点:
- JDK的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- JDK的SPI机制仅仅是扩展服务实现类。没有IoC和AOP的支持,功能比较原始。
Dubbo的SPI机制有以下优点:
- 按需加载。Dubbo 的扩展能力不会一次性实例化所有实现,而是用哪个扩展类则实例化哪个扩展类,减少资源浪费。使用者指定一个需要加载的实现名,随后Dubbo的SPI会通过名字去文件里面找到对应的实现类全限定名然后加载并实例化即可,无需实例化全部实现类。
- 扩展点IoC和AOP的支持。Dubbo SPI在一个Dubbo扩展点实例化之后,返回之前,可以进行IoC和AOP增强:
通过调用injectExtension方法进行setter方法注入其他的依赖,这就是IoC。
通过wapperclass和装饰设计模式对实例进行一层层的包装,实现了重复逻辑的抽取,这就是AOP。 - 自适应扩展。Dubbo的SPI增加了自适应扩展机制,根据请求时候的参数来动态选择对应的扩展,提高了 Dubbo 的扩展能力。
- 扩展排序。可以对扩展实现进行排序。能够基于用户需求,指定扩展实现的执行顺序。
Dubbo的SPI和Java的SPI对于配置文件的约定中,相同的地方在于配置文件的名字都是接口名,不同的地方则有两点:
Dubbo的SPI分了三类目录:
- META-INF/services/:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。
- META-INF/dubbo/:该目录存放用户自定义的 SPI 配置文件。
- META-INF/dubbo/internal/:该目录存放 Dubbo 内部使用的 SPI 配置文件。
Dubbo的文件内容为key=value键值对的形式,即扩展名=具体的类全路径名,在使用时只需要配置扩展名,Dubbo即可找到应对的具体实现类并初始化。
注册中心设计
一个注册中心主要包含一下功能:
- 1、注册,注册是指在注册中心创建一个节点
- 2、取消注册
- 3、订阅,订阅是指在创建的某个节点上增加监听
- 4、取消订阅
我们针对上面这个功能抽象出Registry。并且需要一个工厂类RegistryFactory来创建Registry。
同时,针对订阅功能,我们需要抽象一个回调,NotifyListener来更新节点变化信息到目录中。
Dubbo 的集群模块主要功能是将多个服务提供者伪装成一个提供者供消费方调用,核心组件如下图所示:
从上图可以看出,Dubbo 集群模块涉及以下 4 个核心组件:
-
Directory - 服务目录
代表多个 Invoker,可以把它看成 List,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更。它是后续路由规则、负载均衡策略以及集群容错的基础。 -
Cluster - 集群
将 Directory 中的多个 Invoker 伪装成一个 Invoker ,对上层透明。伪装过程包含了容错逻辑,用于在某些 Provider 节点发生故障时让 Consumer 的调用请求能够发送到正常的 Provider 节点,从而保证整个系统的可用性。 -
Router - 路由
负责从 Directory 中按路由规则选出子集,比如应用在 读写分离、应用隔离 、灰度发布 等。 -
LoadBalance - 负载均衡
负责从 Router 中选出具体的一个 Invoker。选的过程包含了负载均衡算法。
** Cluster 层的核心流程: 当调用进入 Cluster 模块时,Cluster 会从 Directory 中获取当前 Invoker 集合;然后按照 Router 进行路由,从 Directory 中筛选出符合条件的 Invoker 子集;接着按照 LoadBalance 进行负载均衡,从 Router 子集中选出一个最终要调用的 Invoker 对象。**
组件模块化的方式
Dubbo中每个模块都是使用抽象工厂+抽象类+模板模式来具体不同的实现。
抽象工厂用来管理组件或者模块的生命周期。
抽象类用来抽取通用的逻辑。
模板模式用来实现对核心逻辑具体化后,不同的子逻辑在不同的具体实现中有所不同。