关于RPC不可不知的“坑”

RPC,全称是远程过程调用(Remote Procedure Call),是一种常用的Client-Server间的通信方式。早在上个世纪70年代这一概念就被提出来了,后来虽然经过不断地演进,但它的基本思想没有发生太大的变化,那就是屏蔽底层的网络细节,使得对远程服务的网络请求看起来就像是对一个相同进程内的函数调用一样。

关于RPC不可不知的“坑”

然而理想是丰满的,现实是骨感的。尽管RPC的愿景看起来很诱人,但是这一设想本身存在着一些根本性的缺陷,我们在进行技术选型的时候不可不知。

首先我们要明白,网络请求和本地调用之间有着本质的不同:

  • 对于本地函数调用来说,它的结果完全是由输入决定的,无论成功还是失败,都是可以预期的。但是网络请求则不同,它在执行过程中存在着大量的不确定性,比如请求或者响应有可能在传输过程中丢失,服务端的处理速度太慢,甚至服务不可能等等。这些情况往往超出了我们能够控制的范围。与此同时,网络问题又是非常常见的,所以我们必须通过例如重试等机制来加以应对。


  • 本地调用的结果无非有三种情况:正常返回、抛出异常或者永不返回(死循环)。但是对于网络请求来说,还存在着另一种可能的情况:由于网络超时而导致的空结果。在这种情况下,由于我们没有拿到返回结果,所以无法知道这个请求是否在服务端被处理了。


  • 如果我们重试一个失败的网络请求,则有可能它已经在服务端被处理过了,而它失败的原因仅仅是因为返回的响应在传输过程中丢失了。那么在这种情况下,重试将导致同一请求被执行多次。这时,我们就需要考虑请求的幂等问题了。本地的函数调用则不存在这种烦恼。


  • 多次执行同一本地调用的时间基本是相同的。但是网络请求的执行时间直接受到网络延时的影响。网络延时的波动会导致网络请求响应时间的巨大变化。当网络条件较差时,一个网络请求甚至需要数秒钟的时间。


  • 在本地调用中,可以直接在参数中传递一个对象在内存中的指针。但是在网络请求中,所有数据都必须转换成字节序列在网络中传输。对于基本的数据类型,比如整型或者字符串,问题还不大。但是对于比较大的对象,就需要付出比较大的代价。


  • 由于客户端和服务端有可能是用不同语言来实现的,所以RPC框架就必须把传输的数据类型在不同语言间做转换。然而,并不是所有的语言都有相同的数据类型,所以这一转换有时会比较困难。同样的问题在本地调用中就不会出现。

由于上述这些原因,使得RPC在本质上就无法做到和本地调用同样的效果。与RPC不同,同样是为了解决网络请求问题而诞生的REST则能够正视这些区别,基于网络本身的特点来设计协议(虽然有很多人热衷于用REST来实现RPC)。

所以,我们在决定是否要采用RPC的时候,一定要充分认识到上面提到的网络请求所固有的问题,不要被RPC美好的设想蒙蔽的双眼。

于此同时,我们也看到,近年来出现的一些新一代的RPC框架,已经开始越来越多地正视网络请求与本地调用之间所固有的不同,比如,Finagle和Rest.li使用future(promises)来封装可能失败的异步过程。gRPC支持streams,可在一次调用中包含多个请求和响应。RPC协议由于采用了二进制编码格式,所以相对于REST的JSON格式具有更高的性能。

综上所述,RPC和REST各有特点,在选择时需要综合考虑。一般来说,REST更适合用来实现对外的公共接口。而RPC则更聚焦于同一个组织内部的系统中的交互。

对这一话题,大家有什么看法呢?欢迎在留言中发表自己的观点。

您可能还会对下面的文章感兴趣: