刷题外传之深入浅出 RPC

无论是 Leetcode 和层出不穷的 Online Judge, 核心是帮助大家锻炼 Coding 和解算法题的能力。

未来的趋势是,技术面试以及入职之后的 Ramp-up, 对大家的技术能力要求更高更广。因此我们推出一个新的系列——刷题外传,深入浅出的介绍那些刷题中无法涉及的、在面试和工作中却非常重要的系统知识。

本地调用的局限

很多朋友写起 Leetcode 如占瓜切菜,拿出 Eclipse/IntelliJ 建一个 Solution 类就可以码算法,然后 Main 函数里面放几个 Test case, 跑通了心里的满足感不要不要的。然后大家就觉得,写程序挺简单嘛,写几个类,大不了在 import 几个系统库,调用来调用去,语法没错,逻辑搞对,编译通过了就搞定啦。

真正进入工作了,你耳边想起各种新的词汇,什么 Web Service, RPC, Rest, blabla, 感觉生活不复当初一个 IDE 搞定一切那样美好,你的心中想着,怎么才能 import 这些乱七八糟的 service ? 怎么才能调用它们呢?

OK, 进入正题。初学者、或者大部分学生写过的程序都属于本地调用。

image

看上面的例子,从 main 到 PrintValue 的调用就是一个典型的本地调用。

当然复杂的例子可以很多,你可以调用各种系统库、第三方库中的类和函数,只要被调用的方法只在本地完成所有实现,那么就是本地调用。

本地调用的假设是所有实现都存在于本地,但这个假设在生产环境中基本不现实。

在真实的场景中,我们喜欢把常用的相对独立的功能单元做成一个服务 (service), 服务的提供者和服务的调用者往往部署在不同的机器上,有不同的团队管理。

这样做的好处很明显:

  • 服务的开发者不需要关心谁调用了它的服务;低耦合;
  • 服务的开发者只需要关心怎样最优化实现自己的服务,且更容易 Scale; 高内聚;
  • 服务的使用者(调用者)不需要关心服务的具体实现;
  • 服务的使用者(调用者)只需要处理好自己的调用链中的逻辑;

RPC

既然跨机器、跨服务的调用有样的好处,怎样实现呢?答案有很多种,RPC (Remote Procedure Call), 提供了一种很有意思的思路。

想象一下跨机器间的调用:底层基础一定是网络编程,机器 A 向机器 B 发送消息,另外机器 B 收到消息后,根据某种协议来做一些事情,比如:

  • 比如进行一个方法的调用
  • 比如对某个资源进行操作

RPC 属于前者,也就是跨机器进行方法的调用。

image

如图所示,Client / Server routines 可以想象成你的代码。他们通过 Client / Service Stub 和网络协议进行消息传递,看起来好复杂。

为什么设计成这样?Client 的代码直接调用 Service 端的代码不行么?

还是那句话,如果方法的调用者和实现者都在本机,当然可以直接调用(本地调用)。但在网络中传递消息本质上是传输 byte codes, 发消息和收消息的两台机器都需要按同样的规则 序列化/反序列化 消息内容,类似于编码和解码的过程。

换句话说,假设机器 A 需要调用机器 B 上的一个函数 “function_b”. 那么机器 A 需要把调用的 context (包括函数名,参数信息,调用服务地址,e.g 机器 B 的地址和端口)打包序列化成 byte codes, 通过网络协议传到机器 B, 机器 B 再反序列化获得 context, 执行程序实现,把结果再序列化发回给机器 A.

如果每次 RPC 调用都需要手写这么复杂的序列化、反序列化的过程,那可麻烦死了。

考虑到序列化、反序列化的过程都是类似的,能否把他们抽象出来呢?

没错,这就是上图中 Client / Service Stub 的作用。他们封装了 RPC 调用的序列化以及网络调用的部分,所以调用者只需要像调用本地方法一样调用 Client Stub 上的方法,消息就会顺着 Framework 来到另一端进行处理,结果依同样的路径返回调用者。

RPC 的优雅之处在于其良好的封装性,Client 用类似于本地调用的方式进行了跨机器的操作,代码简洁。测试更是非常直观,Mock 一下 Client Stub 怎么方便怎么测试,真的有助于写出高质量的代码。

进阶 RPC

相信读到此处的读者对于 RPC 已经有了不错的理解。那么为什么圈子里流行着各种各样的 RPC Framework 呢,比如最早的 XML-based RPC, Facebook 开源的 Thrift, Google 开源的 gRPC. 大家都在 re-invert wheels 么?

性能是最大的动力。还是如上图所示,RPC 封装的是系列化和网络传输的部分。先说序列化,XML 是最原始的系列化手段,但是其体积大,传输效率低。所以 Thrift 和 gRPC 都实现了自己的 binary 序列化协议,提升了序列化的效率。

换句话说,传递同样的 Context, 使用 XML 序列化产生的消息大小要超过 Thrift/gRPC.

网络协议也是一个优化的重点。Thrift/gRPC 都是在 TCP 的基础上实现自家的应用层协议,效率高。(如果你还搞不清楚 TCP / HTTP 之间的区别,等包子的系列文章吧)。

此外,很多 RPC Framework 也支持 Stream 操作,即 Client / Server 端可以连续发送几条消息给对方,然后等待回复。在很多需要高性能的场景下,Streaming RPC 是比较不错的选择。

尾声

RPC 是一种非常常见的服务调用方式,这篇文章提供一个简单的切入点,帮助大家理解 RPC 的原理和基本应用。粗糙之处,也请大家指正。
转自:包子铺里聊IT