Last Surprise Lyn
  1. 1 Last Surprise Lyn
  2. 2 One Last You Jen Bird
  3. 3 Warcry mpi
  4. 4 Life Will Change Lyn
  5. 5 かかってこいよ NakamuraEmi
  6. 6 Flower Of Life 发热巫女
  7. 7 Time Bomb Veela
  8. 8 Hypocrite Nush
  9. 9 BREAK IN TO BREAK OUT Lyn
  10. 10 Libertus Chen-U
2018-09-24 15:41:13

浅谈RPC

最近学习计算机网络的时候才了解到RPC这样一个重要概念,想起来之前工作中维护的一个浏览器拓展的工作机制也是建立在该原理之上的,同时RPC也是构建分布式应用的重要理论基础,有必要体系化的总结一下。

基本概念

RPC的全称是Remote Procedure Call(远程过程调用),简单来说,就是指计算机A上的进程想要调用计算机B上的应用提供的函数或者方法,由于不在同一内存空间,需要通过网络表示调用的语义以及传达调用相关的数据。整体流程大致可拆解为如下步骤:

  1. client以本地调用的形式调用远程服务
  2. client stub接收到本地调用后负责处理方法和参数等必要数据,编码为能在网络中传输的特定格式
  3. client stub找到远程服务地址,发送编码调用消息
  4. server stub接收到调用消息后解码
  5. server stub调用本地服务
  6. 调用完成后将结果返回给server stub
  7. server stub同样的编码调用结果,并发送给client stub
  8. client stub接收到调用结果后解码,返回给本地调用的client

至此就完成了一次RPC调用,可以看到流程还是很通俗易懂的,RPC本质上就是将以上过程封装,让远程函数的调用者感知不到中间步骤的存在,如同本地调用一样。

解决痛点

那么RPC主要解决了什么问题呢?这就要从应用的拓展性说起了。对于一个普通的访问量不大的应用来说,基本上是没有使用RPC的必要的,比如我这个个人博客,日访问量也就3位数,也没有复杂的服务,那么部署上就可以很简单,一台服务器上起个docker配合nginx转发就可以了,这就是典型的单机结构。

如果访问量上来了,一台老破小服务器撑不住了,就可以考虑上集群,做负载均衡减轻单台服务器压力。简单来说就是把单机结构拓展为多份,每一份称为节点并且都提供相同服务,在这些节点前增加一个调度者角色,用来根据每个节点的压力合理分配请求,这个节点就是负载均衡服务器了。

好的,现在这种部署方式对于大多数应用来说都是能胜任了。但总是有一些更特殊的情况,比如说应用拓展到很大规模之后,有各种各样的服务,视频模块,音乐模块,文章模块等等。用户对这些服务的访问并不是平均的,可能视频模块访问量高出其他模块很多倍,那让其他模块和这个高频模块共同承担压力是不是不太合理,有没有可能把这个视频模块单独分离出去在新的服务器上部署?

这就是分布式结构了。将一个完整的系统,按照其服务拆分为单独的子系统并分别部署。好处是显而易见的,系统之间的低耦合,进而带来的高拓展性和高复用性,比如一套用户系统拆分为单独的服务后,可以在所有产品上都实现复用。

那么分布式之后,问题又来了,拆分开来不在一个进程里了,不能像单机结构一样直接调用函数了,独立的服务如何通信呢?这就是RPC要做的事情啦。那么可能又有人要问了,不同服务器之间的通信,http不就可以实现吗?太有道理了,http确实可以实现,但可实现和可以投入生产环境之间还是有很大距离的。

首先要明白,分布式之后,虽然在开发者看来所有的服务都独立出来了,但对于用户而言,还是一个完整的应用。这意味着什么?对于用户来说,所有的服务还是处在同一进程里,也就是要做到极高的性能优化,RPC一定要快。虽然RPC可以采用http实现,但为了追求更高的性能,通常都会采用自定义tcp协议。下图为两者的结构。

http相对更慢的原因,一是在其上多了一层封装,二是http1.1的报文中包含了太多RPC需求中不需要的信息,增大了传输负担。当然http2一定程度上优化了这一点,gRPC的传输协议就是基于http2的。

总的来说,RPC是在传输层和应用层上进行的面向服务的封装,针对服务的可用性和效率做了优化,相比于单纯的http对于web服务的普适性,自然在针对性场景下有着更优的性能。

实现关键点

前面也说过了,RPC对于开发者而言是封装了调用细节的,也就是对于开发者而言和本地过程调用没有差别。那么先看一下本地调用一个函数会经历哪些过程,以一个乘法函数为例:

function multiply(l, r) {
    return l * r;
}
let l = 1;
let r = 2;
let res = multiply(l, r);

Re要将这段本地调用转化为RPC,一步步看其关键点。

Call ID映射

本地调用multiply会在作用域中根据函数指针找到其声明,但由于RPC是两个独立的进程,所以必须对每个方法都标识唯一ID识别,这个ID在所有进程中都是唯一的。进而,两端都要分别维护一份ID和函数的双向映射,客户端要调用multiply时,会先去查找映射对应Call ID,然后传给服务端。

参数序列化

现在知道该调用哪个函数了,那么如何让服务端知道该函数需要的参数呢?毕竟不同的服务可能都使用的不是同一种语言,参数需要兼顾通用性和在网络中传输方便的特点,那么自然想到二进制字节流这种最底层的表达方式了。序列化就是将参数按照某种规则转化为二进制字节流的手段。

目前有许多成熟的RPC序列化方案,选择方案时主要考虑以下几点:

  • 支持复杂的数据结构
  • 高性能
  • 可拓展

传输协议

参数和函数标识都有了,接下来就需要将这些信息传输到远程服务器了。只要能实现这一任务的协议都能被选做传输协议。为了性能考虑,大部分RPC的传输协议都是针对性优化的自定义TCP协议,但其实UDP,HTTP等都可以。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「HTTP」「工程化」 标签。