RPC 与 Restful

2019-04-18

Java

撤没用的

什么是真正的技术?说实话这个定义太大了我是不敢说的。

我只说现象:很多人、尤其是职场从业5年以下的,往往觉得多用熟了一个框架,就觉得掌握了一门新技术。

职场上这样的人很多。还有很多为了技术而技术的,例如我小弟天天嚷嚷上Spring cloud,实际上现实场景根本不需要这个东西。

至于到底真正的技术是什么?

在我心里定义如下:被少数人能解决的能落地的技术场景结合扎实的数据结构和算法等基本功。

简而言之就是知识和实践相结合,并且被少数人掌握的。

什么是RPC

提到RPC(Remote Procedure Call),就躲不开提到分布式,这个促使RPC诞生的领域。

假设你有一个Calculator,以及它的实现类CalculatorImpl,那么单体应用时,要调用Calculator的add方法来执行一个加运算,你可以方法中直接使用,因为在同一个地址空间,或者说在同一块内存,这个称为本地函数调用。

现在,将系统改造为分布式应用,接口调用和实现分别在两个子系统内,

服务A里头并没有CalculatorImpl这个类,那它要怎样调用服务B的CalculatorImpl的add方法呢?可以模仿B/S架构的调用方式,在B服务暴露一个Restful接口,然后A服务通过调用这个Restful接口来间接调用CalculatorImpl的add方法。

这样,已经很接近RPC了,不过,像这种每次调用时,是不是都需要写一串发起http请求的代码呢?比如httpClient.sendRequest…之类的,能不能简单一下,像本地方法调用一样,去发起远程调用,让使用者感知不到远程调用的过程。

屏蔽的工作,可以使用代理模式解决,生成一个代理对象,而这个代理对象的内部,就是通过httpClient来实现RPC远程过程调用的。

这就是很多RPC框架要解决的问题和解决的思路,比如阿里的Dubbo。

总结一下,RPC要解决的两个问题:

  1. 解决分布式系统中,服务之间的调用问题。

  2. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。

RPC是一种技术的概念名词

RPC=Remote Produce Call 是一种技术的概念名词,HTTP是一种协议,RPC可以通过 HTTP 来实现,也可以通过Socket自己实现一套协议来实现.所以题目可以换一种理解,为何 RPC 还有除 HTTP 之外的实现法,有何必要,毕竟除了HTTP实现外,私有协议不具备通用性.

RPC框架好处

http接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;

优点就是简单、直接、开发方便。

如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了:

首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;

其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。

最后是安全性。

rpc是一种概念,http也是rpc实现的一种方式。

论复杂度,dubbo/hessian用起来是超级简单的。

至于为什么用dubbo/hessian,有几点:

一是调用简单,真正提供了类似于调用本地方法一样调用接口的功能 。

二是参数返回值简单明了 参数和返回值都是直接定义在jar包里的,不需要二次解析。

三是 轻量,没有多余的信息。

四是便于管理,基于dubbo的注册中心。

RPC能解耦服务

RPC:远程过程调用。RPC的核心并不在于使用什么协议。RPC的目的是让你在本地调用远程的方法,而对你来说这个调用是透明的,你并不知道这个调用的方法是部署哪里。

通过RPC能解耦服务,这才是使用RPC的真正目的。RPC的原理主要用到了动态代理模式,至于http协议,只是传输协议而已。简单的实现可以参考spring remoting,复杂的实现可以参考dubbo。

rpc=socket + 动态代理

服务器通讯原理就是一台socket服务器A,另一台socket客户端B,现在如果要通讯的话直接以流方式写入或读出。这样能实现通讯,但有个问题。如何知道更多信息?

比如需要发送流大小,编码,Ip等。这样就有了协议,协议就是规范,就是发送的流中携带了很多的内容。那回到刚刚的问题。发送的内容就是文本类型,客户端就得序列化,那么常用的就有json,xml之类,如果想把内容变得更小,那就有二进制了。把文本变成二进制传递。

说到 rpc 与http接口,不要太复杂了。rpc 协议更简单内容更小,那么来说效率是要高一点

rpc 是什么?就是socket 加动态代理。

总结

学技术应该是知其然知其所以然,我们得明白什么场景,或者什么业务需要它,它能解决其他技术不能解决或者不方便解决的问题。

RPC是一个软件结构概念,是构建分布式应用的理论基础。就好比为啥你家可以用到发电厂发出来的电?是因为电是可以传输的。至于用铜线还是用铁丝还是其他种类的导线,也就是用http还是用其他协议的问题了。这个要看什么场景,对性能要求怎么样。

在java中的最基本的就是RMI技术,它是java原生的应用层分布式技术。我们可以肯定的是在传输性能方面,RMI的性能是优于HTTP的。

那为啥很少用到这个技术?那是因为用这个有很多局限性,首先它要保证传输的两端都要要用java实现,且两边需要有相同的对象类型和代理接口,不需要容器,但是加大了编程的难度,在应用内部的各个子系统之间还是会看到他的身影,比如EJB就是基于rmi技术的。

这就与目前的bs架构的软件大相径庭。用http必须要服务端位于http容器里面,这样减少了网络传输方面的开发,只需要关注业务开发即可。所以在架构一个软件的时候,不能一定根据需求选定技术。

RPC vs Restful

其实这两者并不是一个维度的概念,总得来说RPC涉及的维度更广。

如果硬要比较,那么可以从RPC风格的url和Restful风格的url上进行比较。

比如你提供一个查询订单的接口,用RPC风格,你可能会这样写:

/queryOrder?orderId=123
用Restful风格呢?

Get
/order?orderId=123
再精炼一点,甚至可以这样:

Get
/order/123
RPC是面向过程,Restful是面向资源,并且使用了Http动词。从这个维度上看,Restful风格的url在表述的精简性、可读性上都要更好。

RPC vs RMI

严格来说这两者也不是一个维度的。

RMI是Java提供的一种访问远程对象的协议,是已经实现好了的,可以直接用了。

而RPC呢?人家只是一种编程模型,并没有规定你具体要怎样实现,你甚至都可以在你的RPC框架里面使用RMI来实现数据的传输,比如Dubbo:Dubbo - rmi协议

RPC没那么简单

要实现一个RPC不算难,难的是实现一个高性能高可靠的RPC框架。

比如,既然是分布式了,那么一个服务可能有多个实例,你在调用时,要如何获取这些实例的地址呢?

这时候就需要一个服务注册中心,比如在Dubbo里头,就可以使用Zookeeper作为注册中心,在调用时,从Zookeeper获取服务的实例列表,再从中选择一个进行调用。

那么选哪个调用好呢?这时候就需要负载均衡了,于是你又得考虑如何实现复杂均衡,比如Dubbo就提供了好几种负载均衡策略。

这还没完,总不能每次调用时都去注册中心查询实例列表吧,这样效率多低呀,于是又有了缓存,有了缓存,就要考虑缓存的更新问题,blablabla……

你以为就这样结束了,没呢,还有这些:

客户端总不能每次调用完都干等着服务端返回数据吧,于是就要支持异步调用;
服务端的接口修改了,老的接口还有人在用,怎么办?总不能让他们都改了吧?这就需要版本控制了;
服务端总不能每次接到请求都马上启动一个线程去处理吧?于是就需要线程池;
服务端关闭时,还没处理完的请求怎么办?是直接结束呢,还是等全部请求处理完再关闭呢?
……
如此种种,都是一个优秀的RPC框架需要考虑的问题。

比较

JSON-RPC比较符合直观,格式也相对宽松;

REST最近正流行,有自己的一套设计规范。

REST面对的疑问跟当年刚开始流行面向对象时的情况是一样的。

它适合很多情况,但并不适合所有情况。

最差的结果就是盲目跟风,又对REST的概念和理念一知半解,最后搞出一个半吊子的怪胎,还自我标榜用了流行的RESTful API。

REST是一种设计风格,它的很多思维方式与RPC是完全冲突的。

RPC的思想是把本地函数映射到API,也就是说一个API对应的是一个function,我本地有一个getAllUsers,远程也能通过某种约定的协议来调用这个getAllUsers。至于这个协议是Socket、是HTTP还是别的什么并不重要;

RPC中的主体都是动作,是个动词,表示我要做什么。

而REST则不然,它的URL主体是资源,是个名词。而且也仅支持HTTP协议,规定了使用HTTP Method表达本次要做的动作,类型一般也不超过那四五种。这些动作表达了对资源仅有的几种转化方式。

这种设计思路是反程序员直觉的,因为在本地业务代码中仍然是一个个的函数,是动作,但表现在接口形式上则完全是资源的形式。

就像面向对象的「万物皆对象」理论在习惯了纯粹面向过程开发的程序员眼里显得十分别扭一样:我的代码本来就是按顺序、循环、分支这么运行的啊,为啥非得在很明确的结构上封装一层一层的基类子类接口,还要故意给两个函数起同一个名字,调用时才选择用哪一个呢?

使用「万物皆资源」的思想编写实际项目中的API接口时,最常见的问题就是「这玩意到底是个什么资源?………………算了,我就直接写吧,不管什么风格了」

比如,login和logout应该怎么REST化?
比如,多条件复合搜索在GET里写不下怎么办?
比如,大量资源的删除难道要写几千个DELETE?
其实在理解了REST后,这些都不是什么无解的难题,只是思维方式要转换一下:

login和logout其实只是对session资源的创建和删除;
search本身就是个资源,使用POST创建,如果不需持久化,可以直接在Response中返回结果,如果需要(如翻页、长期缓存等),直接保存搜索结果并303跳转到资源地址就行了;
id多到连url都写不下的请求,应该创建task,用GET返回task状态甚至执行进度;
……等等等。

如果只是规定了一种规范,却不理解它表相下面的思维方式,实施中又按照自己的理解随意变动,那结果肯定是混乱不堪的。

当然,API怎么写是开发者的自由。但如果一个API在url里放一堆动词、资源设计混乱、各种乱用HTTP Method和Status Code,还自称RESTful API的话,那就像你养了一条狗,还管它叫猫一样。

这种混搭产物,不如叫它REFU吧。

(Remove Extension From Url:从url里去掉文件扩展名)

前面说了半天REST的理念和不懂REST造成的问题,但是,这并不代表REST比RPC更「高等」,更不是说不理解REST的人是落伍的。

所谓代码风格、接口形式、各种林林总总的格式规定,其实都是为了在团队内部形成共识、防止个人习惯差异引起的混乱。JSON-RPC当然也是有规范的,但相比REST实在宽松太多了。

如果一个开发团队规定必须在url里写action,所有请求都是POST,可以吗?当然也没问题,只是不要拿出去标榜自己写的是RESTful API就行。

规范最终还是为了开发者和软件产品服务的,如果它能带来便利、减少混乱,就值得用;反之,如果带来的麻烦比解决的还多,那就犯不上纯粹跟风追流行了。

RESTful API在很多实际项目中并不使用。因此真的做了项目,你可能会发现只能用HTTP+JSON来定义接口,无法严格遵守REST风格。

为什么说不实际呢?因为这个风格太理想化了,比方说:

REST要求要将接口以资源的形式呈现。但实际上,很多时候都不太可能将一些业务逻辑看作资源。即使强制这么干了,也会非常非常别扭。登录就是登录,而不是“创建一个session”;播放音乐就是播放,而不是“创建一个播放状态“。
我们之所以要定义接口,本身的动机是做一个抽象,把复杂性隐藏起来,而绝对不是把内部的实现细节给暴露出去。REST却反其道而行之,要求实现应该是“资源”并且这个实现细节要暴露在接口的形式上。

但一个好的接口设计就应该是简单、直观的,能够完全隐藏内部细节的,不管底层是不是资源,资源的组合还是别的什么架构。此外,让业务逻辑与接口表现一致,对系统的长期维护和演进都有极大的好处。
REST只提供了增删改查的基本语义,其他的语义基本上不管。比如批量添加,批量删除,修改一个资源的一部分字段。区分“物理删除”和“标记删除”等等。复杂的查询更加不显示,对于像筛选这类的场景,REST明显就是个渣。这里要表扬一下GraphQL(但GraphQL有其他的问题,在此不展开)
REST建议用HTTP的status code做错误码,以便于“统一”,实际上这非常难统一。各种业务的含义五花八门,抽象层次高低不齐,根本就无法满足需要。比如一个404到底是代表这个接口找不到,还是代表一个资源找不到。400表达请求有问题,但是我想提示用户“你登录手机号输入的格式不对“,还是“你登录手机号已经被占用了“。既然201表示“created”,为啥deleted和updated没有对应的status code,只能用200或者204(no content)?错误处理是web系统里最麻烦的,最需要细心细致的地方。REST风格在这里只能添乱。
web请求参数可能散布在url path、querystring、body、header。服务器端处理对此完全没有什么章法。客户端和服务器端的研发之间还是要做约定。
在url path上的变量会对很多其他的工作带来不良影响。
比如监控,本来url可以作为一个接口的key统计次数/延迟,结果url里出了个变量,所以自动收集nginx的access log,自动做监控项目增加就没法弄了。
再比如,想对接口做流量控制的计数,本来url可以做key,因为有变量,就得多费点事才行。
现实中接口要处理的真正的问题,REST基本上也没怎么管。比如认证、授权、流控、数据缓存(http的etag还起了点作用)、超时控制、数据压缩……。
REST有很多好的工具可以便利的生成对应的代码和文档,也容易形成规范。但问题是REST在实际的项目中并没有解决很多问题,也在很多时候不合用,因此产生的代码和文档也就没什么用,必须经过二次加工才能真的用起来。因此可以基于REST+你的业务场景定义一个你自己的规范。
REST的本意是基于一个架构的假设(资源化),定义了一组风格,并基于这个风格形成约定、工具和支持。思路不错。但是因为他的架构假设就是有问题的,因此后续一系列东西都建立在了一个不稳固的基础之上。同时,REST并没有解决太多的实际问题。

是,的确,有些时候,用REST完成CRUD已经能完成任务了。此时,用REST没有什么不好的。但是,现实当中,真正的业务领域一般都会比资源的CRUD复杂的多。这时REST“基本上没解决太多实际问题”的缺点就会体现出来。我所见到的大多数情况,是会形成一种REST-like形式的接口,像REST却又不限于REST。

为了REST,我看到了太多的人在争执到底是POST还是PUT,到底用querystring还是body,到底用200还是201,到底一个单词应该用单数还是复数,到底一个请求参数应该放在url path的中间一段还是最后一段…… 真正要做的事情本身反而没人关心。而一旦把争论给一个“REST专家”看,他的回答八成是“其实你还是不懂REST”…

我觉得人生不能这么糟蹋,你觉得呢?

附一个现实当中接口的开发的方式

你可以总是从REST开始,如果你要开发的东西能被自然而然的想成是一个资源。然后通过相关的工具自动生成一些代码,把这个原型和你的合作者讨论一下。这是我能想到的REST能做的一件很好的事情——快速实验。

然后如果你想认真的往下做,就可以彻底忘记REST这件事。开始自己定义业务接口,尽量不要在url里加变量。尽量只用GET和POST,减少一些沟通上的混乱。对于每个接口,好好定义可能发生的业务错误,并与PM一起协商怎么处理这些错误。认真的考虑认证、授权、流控等机制,当你开发的是和钱相关的业务尤其要留意。

最后,本文并不是说“绝对不要用REST”,而是:如果你在实际工作中用REST有了困惑,不知道某个情况下REST此时的最佳实践是什么时,不要追求“真正的REST会怎么做”,不要被REST限制住。

如果看过REST最初的那篇论文Architectural Styles and the Design of Network-based Software Architectures就会发现,当时想设计的目标是解决互联网级别的信息共享和互操作问题。而我们的大量开发者工作的主要目标是“为业务系统实现一个满足功能(比如登录,交易……)/非功能需求(比如认证,性能,可扩展性……)的接口“。并且设计接口时会区分“给第三方用的开放接口”、“给UI开发定制的接口“和“内部使用的接口”等。这些接口的设计目标都和REST当初制定的目标有差别。其中最接近的,是“开放接口”。因此可以看到有些开放接口用REST实现还是很不错的,比如github的接口,AWS S3的接口等。

但是其他两类接口与REST关注的点完全不一样。比如面向UI的接口的就要满足UI需要。此时资源不资源不太重要,而是尽量用少的roundtrip去返回这个界面需要的所有数据。接口是按照加载的优先级,而不是“资源”做切分。比如第一屏的显示要尽量一个接口先给出来,后续异步加载的数据可以用其他接口慢慢出。为UI提供的接口往往被划归为“大前端“的一部分。

而内部的接口,越接近DB的,越容易用表来mapping到“资源“,但是内部的接口需要考虑到数据整合的需要。比如底层的用户数据分为A、B、C三类,但这3个数据因为服务隔离不能直接在DB做join。需求要按照A的某个条件做排序分页,但要注入B和C的数据。这时就需要B和C提供batch读取和app注入的相关逻辑。此外还有复杂的查询条件,可动态改变的输出字段等要求。REST的“资源”概念在这里能帮上的忙有限。这也是GrpahQL尝试解决的问题。

再有一类问题是用接口实现分布式一致性的业务问题。比如下单+支付+扣库存+加积分问题。这时接口的形式并不重要,而能够支持实现SAGA或者TCC才是最关键的。而整个业务对外的感觉实际上是创建一个“事务”。早期一本叫做Resftul Web Services的书描述Restful接口做这个事情的方案是:

调用接口创建一个事务的资源
拿着事务资源的id,调用步骤1接口,步骤2接口……
拿着事务资源的id,调用事务的commit接口
这种形式不仅臃肿,还把怎么做这件事的内部细节完全暴露到了调用方,造成了耦合。而我们一般常见的做法就是一个接口POST /doSomething,然后接口实现方内部维护事务,维护commit,rollback等细节。有的时候还需要添加一些异步回调。

简单总结下,写接口的目标各自不同。而REST的目标是“实现互联网级别的信息共享系统”,这个目标和大部分开发者要实现的目标完全不同,这就不难解释为何照搬REST去做另一个领域的事情可能会非常别扭。