暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

架构修炼之道-RPC通信

看点有用的 2021-08-08
245

在当今互联网时代,对于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调用的原理和一次调用过程的耗时,在日常开发过程中经常会遇到请求超时的问题,通过了解原理可以快速的帮助我们定位到问题出现的环节,进而提升解决问题的时效。


文章转载自看点有用的,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论