从零手撸一个Rpc框架-1-Dubbo和Motan

Dubbo和Motan

这个Rpc博客系列的重点主要是记录我自己从零开发一款Rpc框架的过程,所以不会详细去介绍市面上已有的框架,但是在我们正式开始之前,还是不得不介绍一下市面上有代表性的Rpc框架,这里选了两款,dubbo和motan,dubbo是阿里研发的,已经捐献给了Apache;motan是微博的于2016年开源。

先说一下Rpc框架的分类,大致可以分为两类,这两类代表了两个不同的发展方向:

  • 服务治理型
  • 跨语言型

dubbo和motan框架都属于服务治理型框架,在具备基础的Rpc调用功能之上,提供了集群容错、路由、负载均衡、服务注册发现等具有集群治理属性的功能。而跨语言型Rpc框架,顾名思义是为了不同语言之间的服务能够互相调用彼此的服务,比较有名的有ICE,Thrift等,这些服务一般提供一种更加抽象的描述语言,这种语言用来抽象一个服务接口,然后根据不同的语言类型转化为不同的具体代码,这种框架不是我们要讨论的重点。

目前Java开发的Rpc框架更加侧重于服务治理型,这里再说一下Spring Cloud,Spring Cloud实际上属于微服务框架,Rpc只是Spring Cloud提供的其中一个功能,所以Spring Cloud是Rpc框架的更加先进的版本,但是Spring Cloud太庞大,其中集成的框架过于繁多,而dubbo和motan属于轻量型框架。

dubbo和motan本身非常的轻巧,dubbo和motan分别只含有13.6W行和3.5W行代码(包含测试代码),要知道SpringFramework有60W行代码。dubbo的代码稍多一下,因为dubbo对于不同的层提供了不同的实现,举个例子,dubbo在服务注册发现层,提供了consul、redis、default、multicast、nacos、sofa、zookeeper、etcd3这n种实现,在实际使用过程中,只会用得着其中一个,并且里面有些实现并没与太大的用处,可能仅供学习,比如没人能拿redis去做服务注册。相对来说motan的实现就轻巧很多,服务注册只支持了zookeeper和consul这两种最常用的框架。

两个框架的区别

  • dubbo在代理层使用了javassist框架作为代理生成器,javassist框架可以动态编译加载代码,这使得有些地方看起来十分容易让人疑惑,motan更加直接的使用了Java原生的动态代理,看起来更加亲切一些。
  • dubbo对于很多调用都抽象成了Invoke这个接口,然后这个接口上通常都会包一层Proxy,有的时候看起来给人感觉有点强行设计的感觉,没那么明了,motan的调用更加清晰一些。
  • dubbo提供了配置中心,而motan没有。
  • dubbo提供了大而全的各层的实现,motan只提供了最常用的实现。
  • 一些细节的实现不同,比如dubbo过期使用了哈希轮转算法(代码是复制的netty的代码),motan使用了最朴素的实现方式。
  • dubbo提供了更丰富的请求重发策略。
  • dubbo提供了monitor模块。
  • motan使用了连接池,dubbo的dubbo协议使用单一链接。

两个框架的相同点:

看过源码就会发现,motan简直就是dubbo的精缩版,两个框架不仅在架构分层上惊人的相似,就连很多类的命名都一样,两者的设计几乎如出一辙

dubbo:

  • config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 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
  • exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
  • transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec
  • serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool

[来源]:dubbo框架设计

motan相对应的层:

  • config 几乎一样
  • proxy 几乎一样,使用的Java动态代理而不是javassist
  • registry 几乎一样,支持了zookeeper和consul
  • cluster 没有Directory和Router,提供了ClusterLoadBalance接口
  • 没有moniter,但是提供了Switcher,可以实现简单的熔断功能
  • protocol 使用了motan协议
  • 没有exchange,但提供了rpc层,用来封装底层的通信
  • transport 几乎一样,提供了netty和netty4的实现
  • serialize 几乎一样,提供了fastjson、Hessian2和Java原生实现

在其他设计方面,两者都是用自定义的URL作为整个通信的总线,都是用SPI作为扩展接口,并都实现了ExtensionLoader,两者底层都使用netty作为传输层,当然dubbo支持其他的协议例如http等,两者都没有写什么代码注释,两者相似的地方还有很多很多,不一一列举了。

这里有关于motan更加详细的介绍:从motan看RPC框架设计

关于两个框架更加详细的内容就不再继续介绍了,以后可能会专门写系列博客分析他们的代码,但是不是本系列的重点。

比较的目的

我们比较了半天这两个框架,主要的目的是为了学习和弄明白一个Rpc框架需要哪些层次结构,或者说,一次Rpc调用需要经过哪些步骤,弄清楚了这些,一个Rpc的蓝图就已经在脑海里面了,之后要做的事情就是使用自己编程技巧将自己理解的Rpc框架写出来。

在开始敲代码之前,我们看看一次Rpc调用要经过哪些具体的步骤,一个Rpc框架需要分为Client端和Server端两边来看:Client端需要拿到接口的引用,Server端需要给Client暴露服务:

1.Client端要拿到引用

例如我们有一个接口:

1
2
3
public interface IService {
String say(String name);
}

那我们的Client端的代码希望是这样的(具体的步骤我们写在代码注释里面了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ClientTest {
public static void main(String[] args) throws Exception {
// 定义一个引用
Reference<IService> reference = new Reference<>();
// 定义我们需要拿到的服务接口
reference.setInterfaceClass(IService.class);
// 获取服务引用
IService service = reference.getRefer();
// 调用服务的方法
String result = service.say("zrj");
// 获得返回
System.out.println(result);
}
}

Client端是没有IService的具体实现的,具体实现在Server端,所以可以想象的到,在调用IService service = reference.getRefer();时,我们返回的应该是IService的一个动态代理对象,这个对象里面封装了调用方法时具体执行的步骤,在调用String result = service.say("zrj");方法时,获取这次方法调用的各种参数、具体的方法信息等元数据,然后将这些元数据封装成一个请求,最后将这个请求序列化为byte数组,然后调用传输层去连接Server,并将请求发送给Server。最后Server返回一个封装后的结果,Client端收到这个结果后,获取结果并返回。

2.Server端要暴露服务

在Client端调用之前,服务端必须先暴露自己的服务,我们希望服务端这样暴露服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ServerTest {
public static void main(String[] args) {
// 定义一次暴露
Exporter<IService> exporter = new Exporter<>();
// 设置要暴露的服务接口,对应Client端
exporter.setInterfaceClazz(IService.class);
// 设置服务接口的具体实现类
exporter.setRef(new IServiceImpl());
// 暴露服务
exporter.export();
}

// IService接口的实现
private static class IServiceImpl implements IService {
@Override
public String say(String name) {
return "from rpc " + name;
}
}
}

Server端需要根据Client端传过来的参数确定需要调用哪个服务的哪个方法,所以当我们调用exporter.setInterfaceClazz(IService.class);时,就使用反射的方法,找到这个接口中所有的方法签名,并缓存在Map中。当我们调用export的时候,实际上底层肯定是开启了一个端口去监听网络,如果收到了网络请求,那么就先反序列化,然后从请求中拿到需要调用的方法的类和名称,以及方法参数的值,然后去调用本地IServiceImpl中的具体方法,得到结果封装成一个类,然后序列化后通过传输层放回。

这就是Client端和Server端需要做的最基本的工作了。

可以看到,我们至少需要四个层次:Exporter&Reference是最上层的Config层,Proxy代理层用来代理接口并封装底层的网络传输,Transport层用来封装网络传输,Serialization用来封装序列化和反序列化的相关操作。

3.协议设计

当我们在传输数据之前,需要写入额外的信息到数据包中,也就是所谓的协议头,这个协议头需要我们自己来定义。协议头中放入了一些基本的协议信息,例如MagicNumber、协议版本、传输数据类型、数据长度、请求Id等。之后会看到我自己设计的网络协议。

总结:

最终我按照这条思路进行了简单的实现,下章会具体讲解。

通过看dubbo和motan两个框架的源码,我们弄懂了一个Rpc框架的内部原理,并对基本功能做了一个简单梳理。但是我们还差很多很多的功能以及面临很多的问题:

  • 请求重发怎么做
  • 请求失败后的策略怎么抽象
  • Cluster怎么封装,负载均衡怎么做
  • 在Transport层一个Client对一个Server是一个连接还是多个连接,是长连接还是短连接
  • 各种设置如何从最上层传递到最下层,不同层的设置怎么区分
  • 缓存怎么做
  • 异步怎么做
  • 服务注册发现怎么做
  • 生命周期如何管理

这些问题都会在之后的博客中一步一步解决。

GitHub项目地址:susu