RPC
介绍
RPC远程过程调用,就是将本地一个程序里的函数调用操作,延伸为不同机器、不同服务上的函数调用。
本地函数调用基于函数表和参数压栈完成,RPC其实就要模拟这种从字符串名称到远程函数体的对应查找以及通过网络传输函数的实参与返回值。
在具体操作方面,还需要IDL文件的辅助(就是protobuf中的.proto文件这种),数据的传输也是先序列化和反序列化,再通过传输层协议发送。
其实我初学RPC的时候,还在想他和MVC的那种API接口有什么区别。现在想想,虽然都是发送请求以处理数据并获得结果。但是,RPC 适合于需要频繁调用远程服务且期望调用过程尽可能透明的场景,单个远程函数的处理不太复杂,通过远程函数之间相互调用,协作完成;而 API 则更适合于需要定义清晰接口边界的情况,因为一个API往往集成了多个不同的技术栈,会在内部做一系列的数据传递和处理,一个API解决一个需求。
而RPC的好处也说明了这点,相比于单体较大的API,每个远程函数职责单一,可扩展性强,同时故障也只会发生在调用链路的某部分而不是整个API,可靠性会更高。
但RPC由于依赖网络进行多机器间的通信,随着网络和设备故障,导致的某节点宕机问题,会导致一系列依赖该节点上函数的结点出现问题。对于这些问题,需要做进一步的架构设计。这个后面再谈
编解码层
这个IDL文件可能有点陌生,但说到.proto文件就都知道了。一个与编程语言、平台无关的文件,定义了传输的数据格式。最后由编程语言各自基于此文件生成代码,代码包括数据结构体(或者class)的定义、构造函数以及这个结构体进行序列化和反序列化的方法。
为什么要用文件?因为这样比较方便制定协议规范。而代码生成则减少了程序员的开发负担。如果协议有变动,比如要加一个字段。按照普通编写协议的做法就要重新安排数据包的结构,然后对序列化和反序列化的代码做变更。而且这种协议变动是频繁的,程序员改来改去,焦头烂额还容易出bug。基于IDL文件生成代码,改协议就改下是人都看得懂的协议文件,然后调用工具重新gencode一下,就弄好了!程序员的福音啊。
由于按照同一个数据定义规范,以及对应的序列化反序列化方式,支持前后端以此进行通信、支持跨语言通信,与微服务的理念契合。Moreover,我记得protobuf还支持一个版本字段,新老服务可以各用各的RPC协议,兼容性好。
与此对应的便是编程语言自带的序列化反序列化算法,不能做到跨语言。而json这种虽然通用,但转换效率低下,不如IDL生成的二进制编解码算法。
下方是一个struct编码为二进制的例子。在右边可以看到字段的二进制中的属性、序号、以及长度,然后list也有特殊处理,会把list的size也存进去。
网络通信层
基于TCP/UDP,使用socket api进行网络连接与数据发收与事件分发。注意这里走的是传输层协议。相比较HTTP这种文本协议,RPC中用到的二进制协议在传输和编解码的性能会高很多。
RPC关键指标
稳定性
这里的稳定包括了RPC的caller和callee的稳定性。
- 对于caller,防止因为callee故障导致整条调用链路受影响。通过
熔断断掉链路,接下来caller的调用快速返回错误。或者超时控制,为了不让一次超时等待阻塞住之后以及其他的请求,timeout后直接返回错误。
-
对于callee,防止因为caller的请求频繁导致callee被压垮。
-
负载均衡:均匀调用某个服务集群,不让某个节点压力过大
-
请求重试:timeout时先不认为是下游节点故障,先retry两三下。这样也能解决长尾请求的情况,即同样的两次请求,可能响应耗时不一样。第一次如果超过预期时长了,会发第二次,如果第二次能快速获得响应,那我们还是减少了响应时长。
易用性
提供合理的默认参数与详细的开发文档,让用户快速使用、方便修改。同时也提供gencode与脚手架工具等,让用户快速搭建起可用可靠的框架。
扩展性
在上述机制策略的实现上,采用中间件、插件等形式,灵活注入,能保证服务稳定而不过多侵入业务代码。除此之外在各个层次都可以提供插件接口,进行进一步扩展。
观测性
能在运行时收集节点信息、请求处理信息等,方便运维人员用来分析以及异常定位。
性能
目标是高吞吐与低延迟。
可以采用如线程池、多路复用、高性能协议与网络库等提高性能。
RPC企业实践(Kitex)
TODO