架构修炼之道-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通信的主要内容,如果未能解决你的问题,请参考以下文章

程序员架构修炼之道:软件架构基本概念和思维

架构概述 -《卓越架构师修炼之道》

架构分析 -《卓越架构师修炼之道》

测试架构师修炼之道:5 测试策略实战攻略

程序员修炼之道:从小工到专家

C语言嵌入式系统编程修炼之道