网络通信和IO(5):你真的了解RPC吗 /面向服务架构SOA / RPC和HTTP的区别 /RPC和HTTP的应用场景区别 / RPC通信细节 / RPC调用问题解析

开文:面向服务架构SOA

任何大型网站的发展都伴随着网站架构的演进。网站架构一般最初是单应用设计,然后逐渐经历面向对象设计和模块化设计的架构,最终发展到面向服务的服务化架构。

在单应用设计架构体系当中,我们关注的是方法和实体;而在面向服务的服务化架构中,我们则关注的是服务和API。

传统应用开发中会有 研发成本高,运维效率低等挑战。

研发成本高主要体现在:

代码重复率高:在实际项目分工时,开发都是每人负责几个功能,即使开发之间存在功能重叠,往往也是选择自己实现,而不是类库共享。

需求变更困难:代码重复率高后,已有功能变更和新需求加入时都会非常困难。所有重复开发的功能都需要重新修改和测试,很容易出现修改不一致或者被遗漏。

无法满足新业务的快速迭代和交互等问题。

运维效率低主要体现在:

测试、部署成本高:业务运行在一个进程中,因此系统中任何程序的改变,都需要对整个系统重新测试并部署

可伸缩性差:水平扩展只能基于整个系统进行扩展,无法针对某一功能模块按需扩展

可靠性差:某个应用BUG,会导致整个进程宕机,影响其他应用

所以

为了解决单体架构面临的挑战,会对系统进行拆分、解耦、独立、分层。将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心;同时将公共API抽取出来,作为独立的公共服务供其他调用者消费,以实现服务的共享和重用,降低开发和运维成本。

既然系统都是由成千上万大大小小的服务组成,各服务部署在不同的机器上,由不同的团队负责。这时就会遇到两个问题:

要搭建一个新服务,免不了需要依赖他人的服务,而现在他人的服务都在远端,怎么调用?

其他团队要使用我们的新服务,我们的服务该怎么发布以便他人调用?

服务调用

由于各服务部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都要写一坨网络通信相关的代码,不仅复杂而且极易出错。

如果有一种方式能让我们像调用本地服务一样调用远程服务,并且让调用者对网络通信这些细节透明,那么将大大提高生产力,比如服务消费方在调用本地 的一个接口时,实质上调用的是远端的服务。

这种方式其实就是RPC,如今rpc在各大互联网公司中被广泛使用,如阿里巴巴的hsf、dubbo(开源)、Facebook的thrift(开源)、Google grpc(开源)、Twitter的finagle(开源)等。

当然,也可以通过HTTP + JSON的方式来进行服务间的调用,本质上HTTP的方式也是属于RPC的一种实现,但是HTTP的方式并不会像调用本地服务一样那么直观,同时也有一定的性能问题。

服务的调用类型,主要有三种:

同步服务调用:最常见、简单的服务调用,即同步等待服务方返回。为了防止服务端长时间不返回应答消息导致客户端线程被挂死,用户线程等待的时候需要设置超时时间。

异步服务调用:异步服务调用有两种实现方式:一种是只通过Future来实现,还有一种是通过构造Listener对象并将其添加到Future中,用于服务端应答的异步回调。通过Future方式时,线程会阻塞在get结果的操作上;而使用Listener的方式是监听器异步的获取执行结果。

并行服务调用:提升服务调用的并行度,降低时延。比如在结算时,会分别调用短信通知服务,订单详细服务,经验增长服务等等。这些服务即可通过并行服务调用来降低端到端的时间,最后只需对执行结果进行汇聚即可。并行服务最常见的技术实现方案是使用Fork/Join框架实现子任务的并行执行和结果汇聚。

RPC介绍

RPC(Remote Procedure Call)即远程过程调用,RPC常存在于分布式系统中,比如说现在有两台服务器A, B,一个在A服务器上的应用想要调用B服务器上的应用提供的某个方法,由于两个方法不在一个内存空间,不能直接调用,需要通过网络表达调用的语义和传达调用的数据,由此衍生了RPC。

RPC主要是基于TCP协议的,而HTTP服务主要是基于HTTP协议的,HTTP协议是应用层协议,而TCP是传输层协议,那么HTTP是在TCP之上的。所以效率来看的话,RPC是要更胜一筹的。

RPC架构

一个完整的RPC架构里面包含了四个核心的组件,如下:
网络通信和IO(5):你真的了解RPC吗 /面向服务架构SOA / RPC和HTTP的区别 /RPC和HTTP的应用场景区别 / RPC通信细节 / RPC调用问题解析

客户端(Client):服务的调用方

服务端(Server):服务的提供者

客户端存根(Client Stub):存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。

服务端存根(Server Stub):接收客户端发送过来的消息,将消息解包,并调用本地的方法。

RPC调用流程

网络通信和IO(5):你真的了解RPC吗 /面向服务架构SOA / RPC和HTTP的区别 /RPC和HTTP的应用场景区别 / RPC通信细节 / RPC调用问题解析

RPC调用问题解析

1、Call ID映射(调用)

在远程调用的过程中,我们怎么告诉服务端,我们要调用funA,而不是funB、funC呢?

在本地调用中,函数体是直接通过函数指针来指定的,调用funA,编译器就自动帮我们调用它相应的函数指针,但是在远程调用中,函数指针不行了,因为两个进程的地址空间完全不一样了

所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。

当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。

网络通信和IO(5):你真的了解RPC吗 /面向服务架构SOA / RPC和HTTP的区别 /RPC和HTTP的应用场景区别 / RPC通信细节 / RPC调用问题解析

2、序列化和反序列化(解析)

客户端怎么把参数值传给远程的函数呢?

在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)

这时候就需要客户端把参数先转成一个字节流(编码),传给服务端后,再把字节流转成自己能读取的格式(解码)。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。

为什么需要序列化?

  • 转换为字节流方便进行网络传输。

  • 实现跨平台、跨语言;如果是跨平台的序列化,则发送方序列化后,接收方可以用任何其支持的平台反序列化成相应的版本,比如 Java序列化后, 用.net、phython等反序列化。

3、网络传输(传输)

远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。

网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。

所以,要实现一个RPC框架,其实只需要把以上三点实现了就基本完成了。Call ID映射可以直接使用函数字符串,也可以使用整数ID。映射表一般就是一个哈希表。序列化反序列化可以自己写,也可以使用Protobuf或者FlatBuffers之类的。网络传输库可以自己写socket,或者用asio,ZeroMQ,Netty之类。

RPC通信细节

要让网络通信细节对使用者透明,我们需要对通信细节进行封装,我们先看下一个RPC调用的流程涉及到哪些通信细节:

1.服务消费方(client)调用以本地调用方式调用服务

2.client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体

3.client stub找到服务地址,并将消息发送到服务端

4.server stub收到消息后进行解码

5.server stub根据解码结果调用本地的服务

6.本地服务执行并将结果返回给server stub

7.server stub将返回结果打包成消息并发送至消费方

8.client stub接收到消息,并进行解码

9.服务消费方得到最终结果。

RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。有以下的一些技术细节:

如何做到透明化远程过程调用: 使用代理,代理分为两种:1. jdk 动态代理 2.字节码生成。

jdk代理的方式是对接口做代理,所以必须先定义接口;字节码生成方式一般使用的是cglib代理,cglib代理使用的是asm字节码框架,可以直接对类生成代理对象;虽然字节码生成的方式更加方便和高效,但是由于代码维护不易,一般还是采取jdk动态代理的方式。

**确认消息的数据结构:**通信的第一步就是要确认客户端和服务端相互通信的消息结构

客户端的请求消息一般需要包括以下内容:

  • 接口名称:用于确定调用哪个接口

  • 方法名:确定调用接口中哪个方法

  • 参数类型&参数值

  • 超时时间

  • requestID:请求唯一标识

服务器返回消息一般需要包括:

  • 返回值

  • 状态code

  • requestID

**序列化:**序列化类型包括基于文本和基于二进制方式。在分布式服务通信框架中,序列化方式应该包含以下特性:

  • 通用性:比如能否支持Map等复杂数据结构

  • 性能:包括时间复杂度和空间复杂度,通信框架被会公司全部服务使用,即使性能提升一点也会引起质变

  • 可扩展性:比如支持自动增加新的业务字段

  • 多语言支持:通过定义idl,生成方式为静态编译和动态编译

**通信:**通信框架需要支持同步(BIO)和异步(NIO)方式,一般底层使用Netty

**RequestID:**如果请求是异步的,对于客户端来说请求发出后线程即可向下执行。服务端处理完成后再以消息的形式发送给客户端。于是这里会出现以下两个问题:

  • 如何让当前线程“暂停”,等待到结果后,再向后执行

  • 如果多个线程同时进行远程方法调用,这时建立在client server之间的socket连接上会有很多双方发送的消息传递,前后顺序也可能是随机的,server处理完结果后将结果发送给client,client收到很多的消息,怎么知道哪个消息是原先哪个线程调用的?

这时即可通过唯一自增的一个RequestID来解决这两个问题:

在调用callback的get方法时,在get内部获取callback的锁,如果没有获取就等待

以RequestID为Key将callback对象存放在全局ConcurrentHashMap中,先通过RequestID获取callback对象,然后再获取callback的锁,获取之后再调用notify

流行的RPC框架

目前流行的开源RPC框架还是比较多的。下面简单介绍三种:

1、gRPC是Google最近公布的开源软件,基于最新的HTTP2.0协议,并支持常见的众多编程语言。我们知道HTTP2.0是基于二进制的HTTP协议升级版本,目前各大浏览器都在快马加鞭的加以支持。这个RPC框架是基于HTTP协议实现的,底层使用到了Netty框架的支持。

2、Thrift是Facebook的一个开源项目,主要是一个跨语言的服务开发框架。它有一个代码生成器来对它所定义的IDL定义文件自动生成服务代码框架。用户只要在其之前进行二次开发就行,对于底层的RPC通讯等都是透明的。不过这个对于用户来说的话需要学习特定领域语言这个特性,还是有一定成本的。

3、Dubbo是阿里集团开源的一个极为出名的RPC框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是及其鲜明的特色。同样 的远程接口是基于Java Interface,并且依托于spring框架方便开发。可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致。

RPC和HTTP的应用场景区别

RPC应用场景

RPC主要是用在大型企业里面,因为大型企业里面系统繁多,业务线复杂,而且效率优势非常重要的一块,这个时候RPC的优势就比较明显了。

实际的开发当中是这么做的,项目一般使用maven来管理。

比如我们有一个处理订单的系统服务,先声明它的所有的接口(这里就是具体指Java中的interface),然后将整个项目打包为一个jar包,服务端这边引入这个二方库,然后实现相应的功能,客户端这边也只需要引入这个二方库即可调用了。

为什么这么做?主要是为了减少客户端这边的jar包大小,因为每一次打包发布的时候,jar包太多总是会影响效率。另外也是将客户端和服务端解耦,提高代码的可移植性。

HTTP场景

其实在很久以前,我对于企业开发的模式一直定性为HTTP接口开发,也就是我们常说的RESTful风格的服务接口。

的确,对于在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。

我们记得之前本科实习在公司做后台开发的时候,主要就是进行接口的开发,还要写一大份接口文档,严格地标明输入输出是什么?说清楚每一个接口的请求方法,以及请求参数需要注意的事项等。比如下面这个例子:

POST http://www.httpexample.com/restful/buyer/info/share

接口可能返回一个JSON字符串或者是XML文档。然后客户端再去处理这个返回的信息,从而可以比较快速地进行开发。但是对于大型企业来说,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。

RPC和HTTP的区别

RPC服务和HTTP服务还是存在很多的不同点的,一般来说,RPC服务主要是针对大型企业的,而HTTP服务主要是针对小企业的,因为RPC效率更高,而HTTP服务开发迭代会更快。

总之,选用什么样的框架不是按照市场上流行什么而决定的,而是要对整个项目进行完整地评估,从而在仔细比较两种开发框架对于整个项目的影响,最后再决定什么才是最适合这个项目的。一定不要为了使用RPC而每个项目都用RPC,要因地制宜,具体情况具体分析。

建议是对外开放的API推荐采用RESTful,是否严格按照规范是一个要权衡的问题。要综合成本、稳定性、易用性、业务场景等等多种因素。内部调用推荐采用RPC方式。当然不能一概而论,还要看具体的业务场景。

区分HTTP和RPC的异同,让大家更容易根据自己的实际情况选择更适合的方案。

传输协议

RPC:可以基于TCP协议,也可以基于HTTP协议

HTTP:基于HTTP协议

传输效率

RPC:使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率

HTTP:如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理

性能消耗

主要在于序列化和反序列化的耗时

RPC:可以基于thrift实现高效的二进制传输

HTTP:大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能

负载均衡

RPC:基本都自带了负载均衡策略

HTTP:需要配置Nginx,HAProxy来实现

服务治理(下游服务新增,重启,下线时如何不影响上游调用者)

RPC:能做到自动通知,不影响上游

HTTP:需要事先通知,修改Nginx/HAProxy配置

总结

RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。

如果图片都是 HTTPS 连接并且在同一个域名下,那么浏览器在 SSL 握手之后会和服务器商量能不能用 HTTP2,如果能的话就使用 Multiplexing 功能在这个连接上进行多路传输。不过也未必会所有挂在这个域名的资源都会使用一个 TCP 连接去获取,但是可以确定的是 Multiplexing 很可能会被用到。

如果发现用不了 HTTP2 呢?或者用不了 HTTPS(现实中的 HTTP2 都是在 HTTPS 上实现的,所以也就是只能使用 HTTP/1.1)。

那浏览器就会在一个 HOST 上建立多个 TCP 连接,连接数量的最大限制取决于浏览器设置,这些连接会在空闲的时候被浏览器用来发送新的请求,如果所有的连接都正在发送请求呢?那其他的请求就只能等等了。

公众号内有完整网络与IO系列文章,回复关键字“资源”,可获得价值5000元架构师课程、面试题、以及大数据、AI等课程
网络通信和IO(5):你真的了解RPC吗 /面向服务架构SOA / RPC和HTTP的区别 /RPC和HTTP的应用场景区别 / RPC通信细节 / RPC调用问题解析

参考文章:

https://blog.****.net/zhenghhgz/article/details/80595358

https://www.jianshu.com/p/b61695e6b473

https://blog.****.net/elricboa/article/details/78836416