架构修炼之道-RPC通信
Posted 看点有用的
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了架构修炼之道-RPC通信相关的知识,希望对你有一定的参考价值。
在当今互联网时代,对于RPC相信大家都不会陌生,大部分互联网公司都在使用开源的或者公司自研的RPC框架,其中很多优秀的框架被广泛使用,例如国内的dubbo和国外的gRPC等,今天就简单聊一下RPC是如何实现通信的。
一.简介
RPC(Remote ProcedureCall),即远程过程调用,通过网络在跨进程的两台服务器之间传输信息,使得调用远程服务就像本地调用系统内部方法一样方便。RPC基本的调用过程如下图所示:
从上图可以看出,RPC跨越了传输层和应用层,首先由客户端发起一个RPC请求,本地调用client stub负责将调用的接口、方法和参数按照事先约定好的协议进行序列化,然后由RPC框架的RPCRuntime实例通过socket传输到远程服务器;而远程服务器端RPCRuntime实例收到请求后再通过server stub进行反序列化,发起最终的server method调用。上述过程就是一次完整的RPC调用过程。
二.RPC通信的原理
RPC实现通信主要分为4步,动态代理、反射、序列化和网络编程,接着我们从这4个步骤简单阐述一下RPC通信的实现原理。
2.1 动态代理
代理是指做一件事的时候,不用亲自去做,而是找一个代理,通过和代理沟通让其去处理各种事情。在JDK中有一个重要的类java.lang.reflect.Proxy,该类即可生成代理类,它的主要方法是newProxyInstance,源码如下:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
它有三个参数,第一个参数是类加载器对象,可以加载代理类到JVM方法区;第二个参数是接口数组,即代理类要实现的接口,可以实现多个接口;第三个参数是调用具体的处理器类实例,包含了具体要处理的逻辑。
该方法主要实现分为三步,首先创建代理类,接着可以得到代理类含有参数的构造函数,最后创建代理对象,即可调用处理器实例。这样可以根据传入的不同接口获取不同的业务处理逻辑对象,而且是在运行过程中实现的,从而达到了动态代理的目的。
2.2 反射
反射是指程序在运行时可以访问、检测和修改它本身状态或者行为的一种能力,即程序在运行的时候能够观察并修改自己的行为。
在2.1节中,提到了调用具体的处理器类InvocationHandler,该类是一个接口,只有一个invoke()方法,源码如下:
publicinterface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
在RPC调用的过程中,只要传入远程服务名和方法名,通过调用invoke()方法即可反射自动定位到需要被调用的方法,再传入入参,即可进行RPC调用。
2.3 序列化
序列化的目的是将内存中的数据体转化为字节流,反序列化则是将字节流转化为内存的数据结构。在网络传输前都需要先进行序列化,而且要考虑序列化之后内容的大小以及对CPU的影响,这些因素都会影响一次RPC调用的时间。每一次RPC的调用都会伴随着频繁的序列化和反序列化操作。
2.4 网络编程
在前三个步骤中,通过动态代理解决了代理逻辑代码和业务隔离的问题,通过反射实现了自动定位到具体的远程方法,通过序列化为网络传输做好了准备,最后只差一个通道把内容通过网络发送出去,即可完成RPC的一次通信。
RPC框架一般以TCP协议为基础,通过可靠的通信来保障每一次调用的顺利进行。不过现在大多数RPC框架底层的网络通信都是使用Netty将基础的网络代码进行封装,因此我们在使用RPC框架的时候就不会注意到这些内容了。
通过以上四个步骤就可以实现RPC的通信。
三.一次RPC调用的耗时
在传统通信中,衡量通信系统性能时一般包含两个指标,有效性和可靠性,而在RPC调用时,一般会衡量一次调用过程的耗时时长,进而来评估RPC调用的性能。
一次正常的RPC调用的耗时如下图所示:
一次正常的调用的耗时主要包括调用端RPC框架执行时间、网络发送时间、服务端RPC框架执行时间和服务端业务代码时间。接着简要阐述这四个过程的耗时。
a.调用端调用的时候RPC框架会先拦截业务请求,同时将对象序列化,在收到响应时会反序列化。这时框架耗时主要与CPU和JVM运行情况有关,序列化耗时主要和网络传输对象复杂程度有关。
b.网络时间就是数据包在网络传输过程中的时间,包括请求+响应,耗时主要与数据包大小和网络情况相关。
c. 服务端主要是队列等待时间,包括请求拦截+反序列化、响应的序列化。同时如果RPC框架使用了队列则有可能有一定的等待时间。
d.服务端业务代码处理业务逻辑的时间,通常是监控系统收集的服务端耗时。
执行以上过程的时间就是一次RPC调用时的耗时。
四.小结
本文主要简单阐述了RPC调用的原理和一次调用过程的耗时,在日常开发过程中经常会遇到请求超时的问题,通过了解原理可以快速的帮助我们定位到问题出现的环节,进而提升解决问题的时效。
以上是关于架构修炼之道-RPC通信的主要内容,如果未能解决你的问题,请参考以下文章