2.凤凰架构:构建可靠的大型分布式系统 --- 访问远程服务
Posted enlyhua
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2.凤凰架构:构建可靠的大型分布式系统 --- 访问远程服务相关的知识,希望对你有一定的参考价值。
第2章 访问远程服务
远程服务将计算机的工作范围从单机扩展至网络,从本地延伸至远程,是构建分布式系统的首要基础。
2.1 远程服务调用
2.1.1 进程间通信
举例,一个正常的本地调用需要完成以下几个工作:
1.传递方法参数
2.确定方法版本
3.执行被调方法
4.返回执行结果
如果被调的方法不在本地进程。至少存在2个问题。首先,第一步和第四步所做的传递参数、传回参数都依赖于栈内存,如果Caller和Callee分属不同的进程,
就不会拥有相同的栈内存,此时将参数在Caller进程的内存中压栈,对于Callee进程的执行毫无意义。其次,第二步的方法版本选择依赖于语言规则,如果Caller
和Callee不是同一种语言实现的程序,方法版本选择将是一项模糊的不可知行为。
进程间的通信:
1.管道/具名管道
管道类似于两个进程间的桥梁,可通过管道在进程间传递少量的字符流或字节流。普通管道只用于具有亲缘关系的进程(由一个进程启动的另外一个进程)间
的通信,具名管道摆脱了普通管道没有名字的限制,除具有普通管道的所有功能外,它还允许无亲缘关系的进程间的通信。
2.信号
信号用于通知目标进程有某种事件发生。除了进程间通信外,进程还可以给进程自身发送信号。
3.信号量
信号量用于在两个进程之间同步协作的手段,它相当于操作系统提供的一个特殊变量,程序可以在上面进行wait()和notify()操作。
4.消息队列
以上3种方式只适合传递少量消息,POSIX标准中定义了可用于进程间数据量较多的通信的消息队列。进程可以向消息队列添加消息,被赋予读权限的进程
还可以从消息队列消费消息。消息队列客服了信号承载信息量少、管道只能用于无格式字节流以及缓冲区大小受限制等缺点,但实时性相对受限。
5.共享内存
允许多个进程访问同一块公共内存空间,这是效率最高的进程间通信形式。原本每个进程的内存地址空间都是互相隔离的,但操作系统提供了让进程主动
创建、映射、分离、控制某一块内存的程序接口。当一块内存倍多进程共享时,各个进程往往会与其他通信机制,譬如与信号量结合使用,来达到进程间同步及
互斥的协调操作。
6.本地套接字接口
消息队列与共享内存只适合单机多进程间的通信,套接字接口则是更普适的进程间通信机制,可用于不同机器之间的进程通信。出于效率考虑,当仅限于
本机进程间通信时,套接字接口是被优化过的,不会经过网络协议栈,不需要打包拆包,计算校验和,维护序号和应答等操作,只是简单的将应用层数据从一个
进程复制到另外一个进程,这种进程间通信方式即为本地套接字接口,又叫做IPC Socket。
2.1.2 通信的成本
套接字接口的通信方式(IPC Socket),不仅适用于本地相同机器的不同进程间的通信,由于Socket是网络栈的统一接口,它也能支持基于网络的跨进程间通信。
这样做的好处是,由于Socket是各个操作系统都提供的标准接口,完全有可能把远程方法调用的通信细节隐藏在操作系统底层,从应用层面上来看可以做到远程调用与
本地的进程间通信在编码上完全一致。
网络进行分布式运算的八宗罪:
1.网络是可靠的;
2.延迟是不存在的;
3.带宽是无限的;
4.网络是安全的;
5.拓扑结构是一成不变的;
6.总会有一个管理员;
7.不必考虑传输成本;
8.网络都是同质化的。
"RPC 应该是一种高层次的或者说语言层次的特征,而不是像IPC那样,是低层次的或者说系统层次的特征。"
定义:
远程服务调用是指位于互不重合的内存地址空间中的两个程序,在语言层面上,以同步的方式使用带宽有限的信道来传输程序控制信息。
2.1.3 三个基本问题
1.如何表示数据
这里的数据包括传递给方法的参数以及方法执行后的返回值,无论是将参数传递给另外一个进程,还是从另外一个进程中取回执行结果,都涉及数据的表示问题。
对于进程内的方法调用,使用程序语言预置和程序员自定义的数据类型,就很容易解决数据表示问题;对于远程调用,则完全有可能面临交互双方各自使用不同程序
语言的情况,即使只支持一种程序语言的RPC协议,在不同硬件指令集、不同操作系统下,同样的数据类型也完全有不一样的表示细节,譬如数据宽度,字节序的差异
等。有效的做法是将交互双方所涉及的数据转换为某种事先约定的中立数据流格式来进行传输,将数据流转换回不同语言对应的数据类型来使用。这个过程说来拗口,
但相信大家一定很熟悉,就是序列化和反序列化。每种RPC协议都应用要有对应的序列化协议,譬如:
1.ONC RPC的外部数据表示(XDR)
2.CORBA的通用数据表示(CDR)
3.gPRC的Protocol Buffers
4.Web Service 的XML序列化
5.众多轻量级RPC支持的JSON序列化
2.如何传递数据
如何传递数据,准确的说,是指如何通过网络,在两个服务的 Endpoint 之间互相操作,交换数据。这里"交换数据"通常是指应用层协议,实际传输一般是
基于tcp,udp等标准的传输层协议来完成的。两个服务交互不是只扔个序列化数据流表示参数和结果就行,许多在此之外的信息,譬如异常、超时、安全、认证、
授权、事务等,都可能产生双方需要交换的信息的需求。在计算机领域中,专门有一个名词"Wire Protocol"来表示这种两个Endpoint之间交换这些数据的行为,
常见的Wire Protocol 如下:
1.Java RMI 的Java远程消息交换协议(JRMP)
2.CORBA 的互联网ORB间协议(IIOP,是GIOP协议在IP协议上的实现版本)
3.DDS的实时发布订阅协议(RTPS)
4.Web Service 的简单对象访问协议(SOAP)
5.如果要求够简单,双方都是http Endpoint,直接使用HTTP协议也是可以的(如JSON-RPC)
3.如何表示方法
确定表示方法在本地方法调用中并不是太大的问题,编译器或者解释器会根据语言规范,将调用的方法签名转换为进程空间中子过程入口位置的指针。不过一旦
要考虑不同的语言,事情就变得复杂了,每种语言的方法签名都可能有差别,所以"如何表示同一个方法" "如何找到对应的方法" 还是需要一个统一的跨语言的标准
才行。这个标准可以非常简单,譬如直接给程序的每个方法都规定一个唯一的,在任何机器上都绝不重复的编号,调用时压根不管它是什么方法,签名是如何定义的,
直接传递这个编号就能找到对应的方法。这粗暴的方法,还真的就是 DCE/RPC 当初准备的解决方案。类似的,用于表示方法的协议还有:
1.android 的 Android 接口定义语言(AIDL)
2.CORBA 的 OMG 接口定义语言(OMG IDL)
3.Web Service 的Web 服务描述语言(WSDL)
4.JSON-RPC 的 JSON Web 服务协议(JSON-WSP)
2.1.4 统一的RPC
虽然 DEC/RPC 与 ONC RPC 都有很浓厚的UNIX痕迹,但是它们并没有真正在UNIX系统意外大规模流行过,而且它们还有一个大问题:支持持传递值而不支持
传递对象。这2种RPC都是面向C语言设计的,根本就没有对象的概念。然而20世纪90年代,正好是面向对象编程(OOP)风头正盛的年代,所以在1991年,对象管理
组织(OMG)发布了跨进程的,面向异构语言的,支持面向对象的服务调用协议:CORBA 1.0。
但无奈CORBA本身设计的实在太多余繁琐,废话代码太多了。这也间接导致了W3C Web Service的出现。
1999年末,W3C发布了 SOAP 1.0标准的发布,意味着一种被称为"Web Service"的全新的RPC协议的诞生。Web Service 采用xml作为远程过程调用的序列化,
接口描述,服务发现等所有编码的载体。但从技术角度看,它设计的并不那么优秀,甚至可以说是有显著缺陷的。对于开发者而言,web service 的一大缺点就是它过于
严格的数据和接口定义所带来的性能问题。xml作为一门描述性语言本身信息密度就相对低下(都不用和二进制比,和json或者yaml对比一下就知道了)。web service
又是跨语言的,这使得一个简单的字段,为了在不同的语言中不会产生歧义,要以xml严谨描述的话,往往需要比原本存储的这个字段值多出十几倍,几十倍乃至上百倍的
空间。包含大量冗余信息,性能奇差。
如果只是需要客户端,传输性能差就算了,又不是不能用。既然选择了xml,获得了自描述能力,本来就没打算把性能放在第一位,但web service还有一个缺点:
贪婪。是指它希望在一套协议上解决所有分布式计算中遇到的问题,这促使web service 生出了整个家族的协议,除它本身包括的soap,wsdl,uddi协议外,还有
一大堆以 WS-* 命名的,用于解决事务、一致性、事件、通知、业务描述、安全、防重放等子功能的协议,让开发者负担非常中。
简单,普适,高性能这3点,似乎真的很难满足。
2.1.5 分裂的RPC
由于一直没有一个同时满足以上三点的"完美RPC协议"出现,所以远程服务器调用这个领域,逐渐进入群雄混战,距离统一越来越远。现在,相继出现了RMI,Thrift,
Dubbo,gPRC,Motan1/2,Finagle,brpc,.NET REmoting,Arvo,JSON-RPC 2.0。这些RPC功能,特点不尽相同,有的是某种语言私有,有的支持跨语言,有的运行
在应用层http协议之上的,有的直接运行在tcp/udp协议之上的,但并不存在哪一种是"最完美的RPC"。今时今日,任何一款具有生命力的RPC框架,都不再去追求大而全
的"完美",而是以某个具有针对性的特点作为主要的发展对象,如:
1.朝着面向对象发展
代表为 RMI、.NET Remoting。这种方式的别名叫做分布式对象。
2.朝着性能发展
代表为 gRPC和Thrift。决定RPC性能的主要因素有2个:序列化效率和信息密度。序列化效率很好理解,序列化输出结果的容量越小,速度越快,效率自然
高;信息密度则取决于协议中有效负载所占传输数据的比例大小,使用传输协议的层次越高,信息密度就越低,SOAP使用xml拙劣的性能表现就是前车之鉴。gRPC
和Thrift都有自己优秀的专有序列化器,而传输协议方面,gPRC是基于http/2的,支持多路复用和Header头压缩,Thrift则直接基于传输层tcp协议来实现的,
省去了应用层协议的额外开销。
3.朝着简化发展
代表为JSON-RPC,要说功能最强,速度最快可能还有争议,但选功能最弱,速度慢的,JSON-RPC肯定是候选人之一。牺牲了功能和效率,换来的是协议的
简单轻便,接口与格式都更为通用。
开发者们终于认可了不同的RPC框架所提供的特性或多或少是矛盾的,很难有一种框架能满足所有。若要朝着面向对象发展,就注定不会太简单,建Stub,Skeleton
就很烦了,即使由IDL生成也很麻烦;功能多起来,协议就会更复杂,效率一般也会受到影响;要简单易用,那很多事情就必须遵守约定而不是自行配置;要注重效率,那就
需要采用二进制的序列化器和较低层的传输协议,支持的语言范围容易受限。
到了最近几年,RPC框架明显有向更高层次(不仅负责远程调用服务,还管理远程服务)与插件化方向发展的趋势,不再追求独立的解决RPC的全部3个问题(表示数据,
传递数据,表示方法),而是将一部分功能设计成扩展点,让用户自己选择。框架聚焦于提供核心的,更高层次的能力,譬如提供负载均衡,服务注册,可观察性等方面的
支持。
2.2 REST设计风格
rest 和 rpc 本质上并不是同一类型的东西。rest和rpc在思想上的差异的核心是抽象的目标不一样,即面向过程的编程思想与面向资源的编程思想两者之间的
差别。rest和rpc在概念上的不同是指rest并不是一种远程服务调用协议,甚至它就不是一种协议。尽管有一些指导原则,但实际上并不受任何强制的约束。
2.2.1 理解REST
超文本:一种能够对操作进行判断和响应的文本(或声音,或图像)。互联网中的一段文件可以点击,可以触发脚本执行,可以调用服务端。
尝试从"超文本"或"超媒体"的含义来理解什么是"表征状态转移":
1.资源(Resource)
比如你正在读一篇<<REST设计风格>>的文章,这篇文章的内容称之为"资源"。
2.表征(Representation)
当你通过浏览器阅读此文章时,浏览器会向服务端发出"我"需要这资源的html格式的请求,服务端向浏览器返回的这个html就被称为"表征"。你也可以
通过其他方式拿到文本的pdf,MarkDown,RSS等其他形式的版本,它们同样是一个资源的多种表征。可见"表征"是指信息与用户交互时的表示形式。
3.状态(State)
当你读完这篇文章,想看后面是什么内容时,你向服务端发出"给我下一篇文章"的请求。但是"下一篇"是个相对的概念,必须依赖"你当前正在阅读的文章
是哪一篇"才能正确回应,这类在特定语境中产生的上下文信息被称为"状态"。我们所说的有状态(Stateful)抑或是无状态(Stateless),都只是相对于服务
端来说的,服务端要完成"取下一篇"的请求,要么自己记住用户的状态,如这个用户现在阅读的是哪一篇文章,这称为有状态;要么由客户端来记住状态,在请求
的时候明确告诉服务端,如我正在阅读某某文章,现在要读它的下一篇,这称为无状态。
4.转移(Transfer)
无论状态是由服务端还是客户端来提供,"取下一篇文章"这个行为逻辑只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务端通过某种
方式,把"用户当前阅读的文章"转变成"下一篇文章",这就被称为"表征状态转移"。
5.统一接口(Uniform Interface)
上面说的服务端"通过某种方式"让表征状态转移,那具体是什么方式呢?如果你真的用浏览器阅读本文电子版本的话,滚动到结尾处,右下角有一篇文章的
URI超链接地址,这是服务端渲染这篇文章的时候就预置好的,点击它让页面跳转到下一篇,这就是所谓的"某种方式"的其中一种。URI的含义是,统一资源标识
符,是一个名词,如何能够表达出"转移"动作的含义呢?答案是http协议中已经提前约定好了一套"统一接口",它包括get,post,head,put,delete,
trace,options7种基本操作,任何一个支持http协议的服务器都会遵守这套规定,对特定的URI采取这些操作,服务器就会触发相应的表征状态转移。
6.超文本驱动(Hypertext Driven)
尽管表征状态转移是由浏览器主动向服务器发出请求所导致的,该请求导致了"在浏览器屏幕上显示出了下一篇文章的内容"的结果。但是,我们都清楚这
不可能真的是浏览器的主动意图,浏览器是根据用户输入的URI地址来找到网站首页,读取服务器给与的首页超文本内容后,浏览器再通过超文本内容的链接
来导航到这篇文章的,阅读刚结束时,也是通过超文本内部的链接再导航到下一篇。浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都
不可能预置于浏览器代码中的,而是由服务器发出的请求响应信息(超文本)来驱动的。这点和其他客户端的软件有十分本质的区别,那些软件中往往是预置于
程序代码中的,有专门的页面控制器来驱动页面的状态转移。
7.自描述消息(Self-Descriptive Message)
由于资源的表征可能存在多种不同的形态,在消息中应当有明确的消息来告诉客户端该消息的类型以及如何处理这条消息。一种被广泛采用的自描述方法是在
名为"Content-Type"的http header中标识出互联网媒体类型(MIME type),譬如"Content-Type : application/json; charset=utf-8" 说明
该资源会以json的格式来返回,请使用utf-8字符集进行处理。
2.2.2 RESTful的系统
一套理想的、完全满足 REST 风格的系统应该满足以下六大原则:
1.客户端与服务端分离(Client-Server)
将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提高用户界面的跨平台的可移植性,也越来越受到广大开发者的认可,以前完全基于
服务端控制和渲染(如JSF这类)框架的实际用户已甚少,而在服务端进行界面控制(Controller),通过服务端或者客户端的模板渲染引擎来进行界面渲染的
框架(如Struts,SrpingMVC这类)也受到了颇大的冲击。这点和rest可能关系不大,前端技术(如ES规范,到语言实现,再到前端框架等)在近年来的高速
发展,使得前端表达能力大幅度加强才是真正的幕后推手。由于前端的日渐强势,现在还流行起前端反过来驱动服务端进行渲染的SSR(Server-Side Rendering)技术,在Serverless,SEO 等场景中已经占领了一席之地。
2.无状态(Stateless)
无状态是rest的一条核心原则,部分开发者在做服务接口规划时,觉得rest风格的服务怎么设计都感觉特别别扭,很可能的一个原因是服务端持有比较
重的状态。rest希望服务端不用负责维护状态,每一次从客户端发送的请求中,应包括所有必要的上下文信息,会话信息也由客户端负责保存维护,服务端只
依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。客户端承担状态维护职责以后,会产生新的一些问题,譬如身份认证,授权等可信问题,
它们都应有针对性的解决方案。
但必须承认,目前大多数系统都达不到这个要求,且越复杂,越大型的系统越是如此。服务端无状态可以在分布式计算中获得非常高价值的回报,但大型系统
的上下文状态数量完全可能膨胀到客户端无法承受的程度,在服务端的内存,会话,数据库或者缓存等地方持有一定的状态成为一种事实上的存在,并且长期存在,被广泛使用的主流方案。
3.可缓存(Cacheability)
无状态服务虽然提升了系统的可见性、可靠性和可伸缩性,但降低了系统的网络性,"降低网络性"的通俗解释是某个功能使用有状态的设计时只需要一次
(或少量)请求就能完成,使用无状态的设计时可能会需要多次请求,或者在请求中带有额外冗余的信息。为了缓解这个矛盾,rest系统软件系统能够像万维网
一样,允许客户端和中间的通信传递着(如代理)将部分服务端的应答缓存起来。当然,为了缓存能够正常的运作,服务端的应答中必须直接或者间接的表明本身
是否可以进行缓存,可以缓存多长时间,以避免客户端在将来进行请求时得到过期的数据。
4.分层系统(Layered System)
这里所指的分层并不是表示层,服务层,持久层这种意义上的分层,而是指客户端一般不需要知道是否直接连接到了最终的服务器,抑或是连接到路径上的
中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存,伸缩和安全策略的部署。该原则典型的应用就是CDN。
5.统一接口(Uniform Interface)
这是rest的另外一条核心原则,rest希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源,而不是抽象系统该有哪些行为
(服务)上。这条原则你可以类比为计算机中对文件管理的操作来理解,管理文件会涉及到 创建,修改,删除,移动等操作,这些操作都是可数的,而且对所有
的文件都是固定的,统一的。如果面向资源来设计系统,同样会具有类似的操作特征,由于rest并没有设计新的协议,所以这些操作都借用了http协议中固有
的操作命令来完成。
统一接口也是rest最容易陷入争论的地方,基于网络的软件系统,到底是面向资源合适,还是面向服务更合适,这个问题恐怕在相当长的时间内都不会有
定论。但是,有一个更清晰的基本结论是:面向资源的抽象程度更高。抽象程度带来的坏处是距离人类的思维方式往往会更远,而好处是通用程度往往会更好。
举个例子,对于几乎任何系统都有登录和注销功能,如果你理解成登录是login()服务,注销对应的是logout()服务,这样2个独立的服务,这是"符合人类
思维"的;如果你理解为登录是 PUT Session,注销是DELETE Session,这样你只需要设计一种"Session 资源"即可满足需求,甚至以后对Session
的其他需求,如查询用户的信息,就是GET Session而已,其他操作如修改用户信息等也都可以被这套设计囊括在内,这便是"抽象程度更高"带来的好处。
如果想在架构设计中合理恰当的利用统一接口,Fielding建议系统应该能做到每次请求中都包含资源ID,所有操作均通过ID来进行;建议每个资源都
应该是自描述的消息;建议通过超文本来驱动应用状态的转移。
6.按需代码(Code-On-Demand)
按需代码被 Fielding 列为一条可选的原则。它是指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务端发送到客户端的技术。按需代码
赋予了客户端无需事先知道所有来自服务端的消息应该如何处理,如何运行的宽容度。举例,以前的Java Applet技术,今天的WebAssembly等都属于典型的
按需代码,蕴含着具体执行逻辑的代码是存放在服务端的,只有当客户端请求了某个Java Applet之后,代码才会被传输并在客户端机器中运行,结束后通常
也会随即在客户端中销毁。将按需代码列为可选的并非它特别难以达到,更多的是出于必要性和性价比的实际考虑。
REST 提出以资源为主体的服务设计风格,可以带来不好好处:
1.降低服务接口的学习成本
统一接口是rest的重要标志,它将对资源的标准操作都映射到标准的http方法上去,这些方法对每个资源的用法都是一致的,语义都是类似的,不需要
刻意学习,更不需要有诸如IDL之类的协议存在。
2.资源天然具有集合与层次结构
以方法为中心抽象的接口,由于方法是动词,逻辑上决定了每个接口都是互相独立的;但以资源为中心抽象的接口,由于资源是名词,天然就可以产生
集合与层次结构。相信你不需要专门阅读接口说明书,就能轻易推断获取用户icyfenix的购物车中的第二本书的rest接口为:
GET /users/icyfenix/cart/2
3.REST绑定与HTTP协议
面向资源编程不是必须构筑在http之上,但rest是,这是缺点,也是优点。因为http本来就是面向资源设计的网络协议,纯粹只用http(而不是SOAP
over HTTP 那样再构筑协议)带来的好处是无需考虑RPC中的Wire Protocol 问题,rest 将复用http 协议中已经定义的概念和相关基础设施支持来
解决问题。坏处是自然是,当你想去考虑那些http不支持的特性时,便会束手无策。
2.2.3 RMM
服务有多么REST的Richardson成熟度模型:
a) 第0级:完全不REST
b) 第1级:开始引入资源的概念
c) 第2级:引入统一接口,映射到http协议的方法上
d) 第3级:超媒体控制
举例:
医生预约系统:作为一名病人,我想要从系统中得知指定日期内我熟悉的医生是否具有空闲时间,以便于我向该医生预约就诊。
第0级:
医院开放了一个 /appointmentService 的web api,传入日期、医生姓名等参数,可以得到该时间段内医生的空闲时间,如下:
POST /appointmentService?action=query HTTP/1.1
date:"2020-03-04",
doctor:"mjones"
然后服务器回应:
HTTP/1.1 200 OK
start:"14:00","end":"14:50", doctor:"mjones"
start:"16:00","end":"16:50", doctor:"mjones"
觉得14:00比较合适,进行预约确认:
POST /appointmentService/action=confirm HTTP/1.1
appointment:date:"2020-03-04", start:"14:00", doctor:"mjones",
patient:name:icyfenix, age:30, ...
上面是非常直观的RPC风格的服务设计。
第1级:
第0级是RPC风格,如果需要永远不会变化,那它完全可以良好的工作下去。但是,如果你不想为预约医生之外的其他操作、获取空闲时间之外的其他信息
去编写额外的方法、或者改动现有的方法的接口,那还是应该考虑一下如何使用REST来抽象资源。
通往rest的第一步便是引入资源的概念,在api中的基本体现是围绕资源而不是过程来设计服务,说的直白一点,可以理解为服务的Endpoint应该是一个
名词而不是动词。此外,每次请求中都应该包含资源的ID,所有操作均通过资源的ID进行。如,获取医生指定的时间的空闲档期:
POST /doctors/mjones HTTP/1.1
date:"2020-03-04"
然后服务器传回一组包含ID信息的档期清单,注意,ID是资源的唯一编号,有ID即代表"医生的档期"被视为一种资源:
HTTP/1.1 200 ok
id:1234, start:"14:00", end:"14:50", doctor:"mjones",
id:5678, start:"16:00", end:"16:50", doctor:"mjones"
笔者还是觉得16点比较合适:
POST /schedules/1234 HTTP/1.1
name:"icyfenix", age:30m ...
比起第0级,第1级的特征是引入了资源,通过资源ID作为主要线索与服务交互,但第1级至少还有3个问题没有解决:
1.只处理了查询和预约,如果临时想要换时间,要调整预约,或者病突然好了,想删除预约,这都需要提供新的服务接口;
2.处理结果响应时,只能依靠结果中的code、message这些字段做分支判断,每一套服务都要设计可能发生错误的code,这很难全面,而且也不
利于对某些通用的错误进行统一处理;
3.没有考虑认证授权等安全方面的内容,譬如要求只有登录的用户才允许查询医生档期,某些医生可能只对VIP开放,需要特定级别的病人才能预约等
第2级:
第1级遗留的3个问题都可以通过引入统一接口来解决。http协议的7个标准方法是经过精心设计的,只要架构师的抽象能力够用,它们几乎涵盖资源可能
遇到的所有操作场景。rest的具体做法是:
a) 把不同业务需求抽象为对资源的增加、修改、删除等操作来解决第一个问题;
b) 使用http协议的Status Code,它可以涵盖大多数资源操作可能出现的异常,也可以自定义扩展,以此来解决第二个问题;
c) 依靠 HTTP Header 中携带的额外认证、授权信息来解决第三个问题,在5.3节中介绍。
按这个思路,获取医生档期,如下:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
响应:
HTTP/1.1 200 ok
id:1234, start:"14:00", end:"14:50", doctor:"mjones",
id:5678, start:"16:00", end:"16:50", doctor:"mjones"
预约:
POST /schedules/1234 HTTP/1.1
name:icyfenix, age:30, ...
预约成功:
HTTP/1.1 201 Created
Successful confirmation of appointment
失败:
HTTP/1.1 409 Conflict
doctor not available
第3级:
第2级是目前绝大多数系统所达到的rest级别,但仍不是完美的,至少还存在一个问题:你是如何知道预约mjnoes医生的档期需要
访问"/schedules/1234" 这个服务的Endpoint的?也许无法第一时间理解我为什么有这样的疑问,这当然是程序代码写的。但rest 并不认同这种
已经烙在程序员脑海中许久的想法。RMM中的超文本控制、Fielding论文中提到的HATEOAS和现在提的比较多的"超文本驱动",所希望的是除了第一个
请求是由你在浏览器地址输入驱动之外,其他的请求都应该能够自己描述清楚后续可能发生的状态迁移,由超文本自身驱动。所以,当你输入查询指令之后:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
服务器回传的响应信息应该包括如何预约档期、如何了解医生信息等可能的后续操作:
HTTP/1.1 200 ok
schedules: [
id:1234,start:"14:00",end:"14:50",doctor:"mjones",
links:[
rel:"confirm schedule", href:"/schedules/1234"
]
,
id:5678,start:"16:00",end:"16:50",doctor:"mjones",
links:[
rel:"confirm schedule", href:"/schedules/5678"
]
,
],
links: [
rel:"doctor info", href:"/doctors/mjnoes/info"
]
如果做到了第3级,那么服务端的API和客户端也是完全解耦的,此时如果你要调整服务数量,或者对同一个服务做API升级时将会变得非常简单。
2.2.4 不足与争议
以下是rest能否在实践中真正良好应用的部分争议问题:
1.面向资源的编程思想只适合做crud,面向过程、面向对象编程才能处理真正复杂的业务逻辑
针对那些比较抽象的场景,如果真不能把http方法映射为资源的所有操作,rest也并非刻板的教条,用户是可以使用自定义的方法的,按照Google推荐
的rest api风格,自定义方法应该放在资源路径末尾,嵌入冒号加自定义动词的后缀。譬如,可以把删除操作映射到标准的DELETE方法上,如果还要提供一个
恢复删除API,那它可能会被设计为:
POST /user/user_id/cart/book_id:undelete
面向资源的编程思想与另外两种主流编程思想只是抽象问题时所处的立场不同,只有选择不同,没有搞下之分:
1.面向过程编程时,为什么要以算法和处理过程为中心,输入数据,输出结果?当然是为了符合计算机世界中主流的交互方式。
2.面向对象编程时,为什么要将数据和行为统一起来、封装成对象?当然是为了符合现实世界的主流交互模式。
3.面向资源编程时,为什么要将数据(资源)作为抽象主体,把行为看做统一的接口?当然是为了符合网络世界的主流交互模式。
2.REST与HTTP完全绑定,不适合应用于要求高性能传输的场景中
面向资源编程与协议无关,但rest的确依赖于http协议的标准方法、状态码、协议头等各个方面。http不是传输层协议,它是应用层协议,如果仅将
http用于传输是不恰当的。对于需要直接控制传输,如二进制细节、编码形式、报文格式、连接方式等细节的场景,rest确实不合适。
3.REST不利于事务支持
这个问题首先要看你怎么看待"事务"这个概念。如果事务指的是数据库那种狭义的刚性ACID事务,那除非完全不持有状态,否则分布式系统本身与此就是
有矛盾的(CAP不兼容),这是分布式的问题而不是rest的问题。
如果事务指的是通过服务协议或者框架,在分布式服务中,获得对多个数据同时提交的统一协调能力(2PC/3PC),譬如WS-AtomicTransaction这样的
功能性协议,rest是不支持的,假如你了解了这样做的代价,仍要坚持这样做的话,Web Service 是比较好的选择。如果"事务"只是希望保证数据的最终
一致性,说明你已经放弃刚性事务了,这才是分布式系统中正常的交互方式,使用rest肯定不会有什么障碍,更谈不上"不利于"。
4.REST没有传输可靠性支持
是的,并没有。在http中发送一个请求,通常会收到一个响应,如 HTTP/1.1 200 OK 或者 HTTP/1.1 404 Not Found等。但如果你没有收到任何
响应,那就无法确定消息是没有发出去,还是没有从服务器返回,这其中关键的差别在于服务端是否被触发了某些处理?应对传输可靠性最简单粗暴的做法是把
消息再重发一遍。这种简单处理能够成立的前提条件是服务具有幂等性,即服务被重复执行多次的效果与执行一次是相等的。HTTP协议要求GET、PUT和DELETE
应具有幂等性,我们把REST服务映射到这些方法时,也应当保证幂等性。对于POST重复提交,浏览器会出现相应的警告,如Chrome中"确认重新提交表单"的
提示,对于服务端,就应该做预校验,如果发现可能重复,则返回 HTTP/1.1 425 Too Early。另外,web service 中有 WS-ReliableMessaging
功能协议用于支持消息可靠投递。类似的,由于rest没有采用额外的Wire Protocol,所以除了事务、可靠传输这些功能外,一定还可以在WS-*协议中找到
很多REST不支持的特性。
5.REST缺乏对资源进行"部分"和"批量"处理能力
以http协议为基础给rest带来了极大的的便捷,但也使http本身成了束缚rest的无形存在。
目前,一种理论上比较优秀的可以解决以上几类问题的方案是 GraphQL,它由Facebook 提出并开源的一种面向资源API的数据查询语言,如同SQL一样,挂了个
"查询语言"的名字,但其实CURD都做。比起依赖于HTTP无协议的REST,GraphQL 可以说是另一种有协议的、更彻底的面向资源的服务方式。然后凡是都有2面性,离开
了HTTP,它又面临几乎所有RPC框架所遇到的那个如何推广交互接口的问题。
以上是关于2.凤凰架构:构建可靠的大型分布式系统 --- 访问远程服务的主要内容,如果未能解决你的问题,请参考以下文章
17.凤凰架构:构建可靠的大型分布式系统 --- 技术演示工程实践
16.凤凰架构:构建可靠的大型分布式系统 --- 向微服务迈进