当然我在扯淡

谈谈RPC

RPC(Remote Procedure Call)字面意思就是远程过程调用,RPC框架的作用就是将远程过程调用封装成本地过程调用。所以Rpc-Framework要封装的就是以下这些东西。

在美团实习用到了Thrift,Thrift作为一个跨语言RPC,美团内部通过jar包将其接入Spring,屏蔽了复杂的Thrift连接操作,而且RPC框架可以很非常方便的将一个单机服务转变为分布式架构的。
不由得让我想起了刚来美团时接手的一个纯C写的分段式语音传输系统,Socket通信加上各种内存复制着实让人恶心。用Java重构后,加上Thrift框架,仅仅两个实习生就多快好省的搞好了这个分布式系统。RPC的强大可见一斑。

原理

实现远程函数调用,无非就是讲函数参数通过Socket传过去,在服务器上执行完之后将结果通过Socket传输到客户端。所以RPC本质就是实现了这么个事情。
参数传输之后,如何放服务器知道调用哪个类的哪个参数,Java反射特性用来解决这个问题最好不过了。

Code

下面贴一段最简单的RPC框架,麻雀虽小,五脏俱全.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class RpcFramework{
public static void servicePublish(Object interfaceImplClass, int port) throws IOException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(port));
while(true){
Socket socket = serverSocket.accept();
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());

String methodName = inputStream.readUTF();
Class<?>[] argTypes = (Class<?>[])inputStream.readObject();
Object[] args = (Object[])inputStream.readObject();

Method method = interfaceImplClass.getClass().getMethod(methodName, argTypes);
Object result = method.invoke(interfaceImplClass, args);

outputStream.writeObject(result);

outputStream.close();
inputStream.close();
socket.close();
}
}

public static Object clientProxy(Class interfaceClass, String hostname, int port){
return Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[] {interfaceClass},
new InvocationHandler(){
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(hostname, port));
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
outputStream.writeUTF(method.getName());
outputStream.writeObject(method.getParameterTypes());
outputStream.writeObject(args);
Object result = inputStream.readObject();
outputStream.close();
inputStream.close();
return result;
}
});
}
}

客户端和服务端的例子就不放了,看懂代码之后很简单。
了解动态代理的的人知道,动态代理相比静态代理的区别就是动态生成代理类。[JDK动态代理]http://shiwenhao.gq/2018/05/20/JDK%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86/#more

动态代理

所以这里当客户端使用代理类执行方法时,内部执行的是如下方法

1
2
invoke(proxy, method, args)
//proxy是生成的代理类本身, method就是要执行的方法,args则是执行方法的参数

看上面这个小的RPC框架,RPC主要包含如下几个模块,服务治理,序列化,网络IO下面来看一下我理解中的如何去扩展这些模块。

模块

序列化

序列化在RPC框架中的作用就是讲RPC request序列化之后从Consumer传输到Provider,同时将RPC response序列化之后从Provider传输到Consumer.
在如上代码中并没有过多的体现这个过程。

客户端直接将Rpc Request分成了三部分

  1. Method 的 name
  2. Method 的 参数类型
  3. Object[] arguments 函数参数

在 ObjectOutputStream.writeUTF()中直接传输了字符串,在ObjectOutputStream.writeObject()中传输了序列化之中的类,所以序列化默认为Java自带的序列化机制,在如上代码中直接ObjectOutputStream.writeObject(Method)是不可取的,因为Method类没有实现Serizalibe接口.

序列化的性能主要从两方面考虑,

  1. 序列化消耗时间
  2. 序列化之后的文件大小

所以一般情况向二进制的序列化机制可以吊打json或者xml序列化方式,据说在美团内部Thrift甚至被用来传输图片数据。

网络IO

网络IO谈起来就很多了,从最初的BIO模型,这种 线程数量:连接数量为1:1 的模型实在不适合用于这种负载很大的系统。除了BIO,在Java中还有NIO,在Linux系统中使用了epoll作为其底层实现,然而一直存在一个比较严重的bug,epoll空轮询导致CPU负载100%。在JDK1.7中,引入了AIO,真正的异步IO。然而NIO和AIO的编程过于复杂,像我这种菜鸡也就是照着模板代码跑一跑,出bug的时候怕是只能删库跑路了…

所以现在高性能的IO框架一般选择Netty,Netty作为一种事件驱动的异步IO框架,满足我这等屌丝的性能需求是没问题的,否则只能是代码写的烂了。
建议Netty看看<<Netty权威指南>>和<<Netty实战>>两本书。

服务治理

服务治理要分好多方面,包括但不限于 服务注册,服务发现,服务依赖关系分析,服务分组路由,服务调用链路追踪等。
在上面的代码中,基本没有体现服务治理的内容,而是靠函数参数手工传入,这在面向服务化的分布式系统中,无法实现动态添加或者下线机器的功能,现在需要一个服务注册和发现中心。
这个中心必须是分布式式的,不能在一台机器宕机后整个服务崩掉。在许多RPC框架中一般选择Zookeeper,Zookeeper使用CAB协议保证了一致性,而且命名空间可以很好的满足服务注册发现的要求,唯一缺点的在于Zookeeper使用Java实现,性能有些羸弱。

在Zookeeper上如何实现服务注册和发现呢,由于Zookeeper有不同的命名空间,所以可以实现下图这样一颗服务树。(Zookeeper在Dubbo中的应用)

Spring集成

其实Spring集成并不是必须的,但是如果扣字的话,上面的那些模块也不是必须的。集成Spring的目的主要在于如今的Java开发大部分都是基于Spring(Spring Boot),或者在Spring上进行二次开发。将Rpc框架与Spring无缝集成,可以使开发人员的编程界面保持一致,使用更为方便。

集成Spring主要就是在Bean的生命周期中,利用一些部分进行扩展,另外一点就是FactoryBean。FactroyBean和BeanFactory的区别一定要清楚,通过FactoryBean可以得到任何想得到的类,而不是FactoryBean本身。

Naive Rpc

在我第一次接触到Rpc框架的时候,总觉的Rpc是一个非常复杂的东西。经过我的学习发现Rpc确实是一个复杂的系统,有许多可以扩展的地方,还有许多坑点。例如如果保证Rpc Request和Rpc Response一一对应。

没有手动造过轮子,底层的许多东西可能都不会接触到,不要想当然的认为自己已经懂了。
一个自己正在写的轮子NaiveRpc,欢迎交流讨论。