网络通信和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架构里面包含了四个核心的组件,如下:
客户端(Client):服务的调用方
服务端(Server):服务的提供者
客户端存根(Client Stub):存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
服务端存根(Server Stub):接收客户端发送过来的消息,将消息解包,并调用本地的方法。
RPC调用流程
RPC调用问题解析
1、Call ID映射(调用)
在远程调用的过程中,我们怎么告诉服务端,我们要调用funA,而不是funB、funC呢?
在本地调用中,函数体是直接通过函数指针来指定的,调用funA,编译器就自动帮我们调用它相应的函数指针,但是在远程调用中,函数指针不行了,因为两个进程的地址空间完全不一样了
所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。
当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
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等课程
参考文章:
https://blog.****.net/zhenghhgz/article/details/80595358
https://www.jianshu.com/p/b61695e6b473
https://blog.****.net/elricboa/article/details/78836416