系列文章目录
第一章 系统架构的演进
第二章 REST风格架构
文章目录
- 系列文章目录
- 前言
- 一、进程间的通信
- 普通管道(Pipe)或者具名管道(Named Pipe)
- 信号(Signal)
- 信号量(Semaphore)
- 消息队列(Message Queue)
- 共享内存(Shared Memory)
- 套接字接口(Socket)
- 二、RPC协议需要解决的基本问题
- 三、Rest设计风格
- 什么是Rest?
- REST的由来
- 什么是web
- REST中的几个概念
- RESTful 的系统
- 总结
前言
远程服务调用(Remote Service Call, RSC)是一种分布式系统架构中的关键技术,它允许运行在一个网络环境中的不同进程或服务之间进行直接的方法调用,即使这些进程或服务分布在不同的硬件设备或者操作系统上。这一机制是构建微服务架构、云服务集成、以及分布式应用的基础。
关于RPC这种技术,笔者本人也只是从字面上去理解它,尽管实际开发中也经常用到,比如rest远程调用,涉及到的协议http。web Service远程调用,涉及到的协议是SOAP。但我对于RPC 本身解决什么问题、怎么去解决这些问题、为什么要这样解决都或多或少存在认知模糊,一句话就是会用现成的RPC技术,各类支持库和框架,我也是信手拈来,随便看看也能上马应用到项目中去,但对于其本质仍然不了解。本篇文章记录了笔者的学习探索过程,部分内容来源于专业书籍和网络搜索。
一、进程间的通信
先来了解下同一台计算机中的不同进程间该如何通信。我们知道,由于操作系统为每个进程分配独立的内存空间,因此进程间无法直接访问对方的内存区域,需要借助特定的通信机制来实现数据传输和同步。比如说我们的微服务,即使多个服务部署在同一台服务器,但是每个服务的启动运行都是一个独立的进程,端口号也不同。进程里的用户空间都是独立的,无法直接访问。
任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
进程间通信大概有如下几种方式
普通管道(Pipe)或者具名管道(Named Pipe)
管道类似于两个进程间的桥梁,可通过管道在进程间传递少量的字符流或字节流。普通管道(匿名管道)只用于有亲缘关系进程(由一个进程启动的另外一个进程)间的通信,具名管道摆脱了普通管道没有名字的限制,除具有管道所有的功能外,它还允许无亲缘关系进程间的通信。管道典型的应用就是命令行中的|操作符,比如:
ps -ef | grep java
这个命令是列出linux或者unix服务器上正在运行的包含java字符串的进程。
ps与grep都有独立的进程,以上命令就通过管道操作符|将ps命令的标准输出连接到grep命令的标准输入上。
信号(Signal)
信号是一种比较原始的进程间通信方式,用于通知接收进程某个事件已经发生,常用于中断或异常处理,如终止进程、挂起进程等。譬如:
kill -9 pid
以上就是由 Shell 进程向指定 PID 的进程发送 SIGKILL 信号。
信号量(Semaphore)
信号量用于两个进程之间同步协作手段,它相当于操作系统提供的一个特殊变量,程序可以在上面进行wait()和notify()操作。
消息队列(Message Queue)
上面的三种方式只适合传递少量信息,POSIX 标准中定义了消息队列用于进程间数据量较多的通信。进程可以向队列添加消息,被赋予读权限的进程则可以从队列消费消息。消息队列克服了信号承载信息量少,管道只能用于无格式字节流以及缓冲区大小受限等缺点,但实时性相对受限。
共享内存(Shared Memory)
允许多个进程访问同一块公共的内存空间,这是效率最高的进程间通信形式。原本每个进程的内存地址空间都是相互隔离的,但操作系统提供了让进程主动创建、映射、分离、控制某一块内存的程序接口。当一块内存被多进程共享时,各个进程往往会与其它通信机制,譬如信号量结合使用,来达到进程间同步及互斥的协调操作。
套接字接口(Socket)
消息队列和共享内存只适合单机多进程间的通信,套接字接口是更为普适的进程间通信机制,可用于不同机器之间的进程通信。套接字(Socket)起初是由 UNIX 系统的 BSD 分支开发出来的,现在已经移植到所有主流的操作系统上。出于效率考虑,当仅限于本机进程间通信时,套接字接口是被优化过的,不会经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等操作,只是简单地将应用层数据从一个进程拷贝到另一个进程,这种进程间通信方式有个专名的名称:UNIX Domain Socket,又叫做 IPC Socket。
二、RPC协议需要解决的基本问题
-
如何表示数据:这里数据包括了传递给方法的参数,以及方法执行后的返回值。无论是将参数传递给另外一个进程,还是从另外一个进程中取回执行结果,都涉及到它们应该如何表示。进程内的方法调用,使用程序语言预置的和程序员自定义的数据类型,就很容易解决数据表示问题,远程方法调用则完全可能面临交互双方各自使用不同程序语言的情况;即使只支持一种程序语言的 RPC 协议,在不同硬件指令集、不同操作系统下,同样的数据类型也完全可能有不一样表现细节,譬如数据宽度、字节序的差异等等。有效的做法是将交互双方所涉及的数据转换为某种事先约定好的中立数据流格式来进行传输,将数据流转换回不同语言中对应的数据类型来进行使用,这个过程说起来拗口,但相信大家一定很熟悉,就是序列化与反序列化。每种 RPC 协议都应该要有对应的序列化协议,譬如:
ONC RPC 的External Data Representation (XDR)
CORBA 的Common Data Representation(CDR)
Java RMI 的Java Object Serialization Stream Protocol
gRPC 的Protocol Buffers
Web Service 的XML Serialization
众多轻量级 RPC 支持的JSON Serialization
…… -
如何传递数据:准确地说,是指如何通过网络,在两个服务的 Endpoint 之间相互操作、交换数据。这里“交换数据”通常指的是应用层协议,实际传输一般是基于标准的 TCP、UDP 等标准的传输层协议来完成的。两个服务交互不是只扔个序列化数据流来表示参数和结果就行的,许多在此之外信息,譬如异常、超时、安全、认证、授权、事务,等等,都可能产生双方需要交换信息的需求。在计算机科学中,专门有一个名称“Wire Protocol”来用于表示这种两个 Endpoint 之间交换这类数据的行为,常见的 Wire Protocol 有:
Java RMI 的Java Remote Message Protocol(JRMP,也支持RMI-IIOP)
CORBA 的Internet Inter ORB Protocol(IIOP,是 GIOP 协议在 IP 协议上的实现版本)
DDS 的Real Time Publish Subscribe Protocol(RTPS)
Web Service 的Simple Object Access Protocol(SOAP)
如果要求足够简单,双方都是 HTTP Endpoint,直接使用 HTTP 协议也是可以的(如 JSON-RPC)
…… -
如何确定方法:这在本地方法调用中并不是太大的问题,编译器或者解释器会根据语言规范,将调用的方法签名转换为进程空间中子过程入口位置的指针。不过一旦要考虑不同语言,事情又立刻麻烦起来,每门语言的方法签名都可能有所差别,所以“如何表示同一个方法”,“如何找到对应的方法”还是得弄个跨语言的统一的标准才行。这个标准做起来可以非常简单,譬如直接给程序的每个方法都规定一个唯一的、在任何机器上都绝不重复的编号,调用时压根不管它什么方法签名是如何定义的,直接传这个编号就能找到对应的方法。这种听起既粗鲁又寒碜的办法,还真的就是 DCE/RPC 当初准备的解决方案。虽然最终 DCE 还是弄出了一套语言无关的接口描述语言(Interface Description Language,IDL),成为此后许多 RPC 参考或依赖的基础(如 CORBA 的 OMG IDL),但那个唯一的绝不重复的编码方案UUID(Universally Unique Identifier)却也被保留且广为流传开来,今天已广泛应用于程序开发的方方面面。类似地,用于表示方法的协议还有:
Android 的Android Interface Definition Language(AIDL)
CORBA 的OMG Interface Definition Language(OMG IDL)
Web Service 的Web Service Description Language(WSDL)
JSON-RPC 的JSON Web Service Protocol(JSON-WSP)
……
以上 RPC 中的三个基本问题,全部都可以在本地方法调用过程中找到相对应的操作。RPC 的想法始于本地方法调用,尽管早已不再追求实现成与本地方法调用完全一致,但其设计思路仍然带有本地方法调用的深刻烙印,抓住两者间的联系来类比,对我们更深刻地理解 RPC 的本质会很有好处。
三、Rest设计风格
什么是Rest?
百度百科定义如下:
REST即表述性状态传递(英文:Representational State Transfer,简称REST)是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。它是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。
- 我们更多的将REST称为表述性状态转移。
- 所谓的表述性状态转移,是对什么的表述?——资源
- REST省略了主语Resource(资源),全称是 Resource Representational State Transfer,即资源表述性状态转移。通俗来讲就是:资源在网络中以某种表现形式进行状态转移。
- 如果一个架构符合REST原则,就称它为RESTful架构。
REST的由来
首先简单了解一下作者——Roy Thomas Fielding
- HTTP/1.0协议专家组成员
- HTTP/1.1协议专家组负责人
- Apache HTTP服务器的核心开发者
- Apache软件基金会合作创始人
什么是web
想要理解Rest,需要知道什么是web,因为Rest就是在web基础上提出来的一种架构风格。
同样的百度百科的一段话:
web(World Wide Web)即全球广域网,也称为万维网,它是一种基于超文本和HTTP的、全球性的、动态交互的、跨平台的分布式图形信息系统。是建立在Internet上的一种网络服务,为浏览者在Internet上查找和浏览信息提供了图形化的、易于访问的直观界面,其中的文档及超级链接将Internet上的信息节点组织成一个互为关联的网状结构。
维基百科
万维网(英语:World Wide Web),亦作“WWW”、“Web”,是一个由许多互相链接的超文本组成的系统,通过互联网访问。
万维网并不等同互联网,万维网只是互联网所能提供的服务其中之一,是靠着互联网运行的一项服务。
互联网和万维网用语经常被使用且没有太多区别。然而,两者是不一样的。互联网是电脑网络互相连接的全球系统。相较之下,万维网是全球收集的文件和其他资源,通过超链接和URIs连接。万维网资源通常使用HTTP访问,这是互联网通信协议的一种。
万维网的核心部分是由三个标准构成的:
统一资源标识符(URI),这是一个统一的为资源定位的系统。
超文本传送协议(HTTP),它负责规定客户端和服务器怎样互相交流。
超文本标记语言(HTML),作用是定义超文本文档的结构和格式。
web是一个由许多相互链接的超文本组成的系统,它使用URI来定位系统中的每一个资源,并通过HTTP协议进行数据的交互。
更抽象的说,Web是一个分布式信息系统,为超文本文件和其他对象(资源)提供访问接口和访问机制。
理解了什么是web,我们便可以更好地理解什么是REST了。作为web自身的架构风格,我们直接给出结论:REST本质上是一种分布式超媒体系统的应用层解决方案,它为资源互通和资源管理的分离提出了一系列架构约束和原则,得到一个功能强、性能好、适宜通信的以网络为基础的应用软件架构。
REST中的几个概念
-
资源(Resource):比如你现在正在阅读一篇名为《REST 设计风格》的文章,这篇文章的内容本身(你可以将其理解为其蕴含的信息、数据)我们称之为“资源”。无论你是购买的书籍、是在浏览器看的网页、是打印出来看的文稿、是在电脑屏幕上阅读抑或是手机上浏览,尽管呈现的样子各不相同,但其中的信息是不变的,你所阅读的仍是同一份“资源”。
-
表征(Representation):当你通过电脑浏览器阅读此文章时,浏览器向服务端发出请求“我需要这个资源的 HTML 格式”,服务端向浏览器返回的这个 HTML 就被称之为“表征”,你可能通过其他方式拿到本文的 PDF、Markdown、RSS 等其他形式的版本,它们也同样是一个资源的多种表征。可见“表征”这个概念是指信息与用户交互时的表示形式,这与我们软件分层架构中常说的“表示层”(Presentation Layer)的语义其实是一致的。
-
状态(State):当你读完了这篇文章,想看后面是什么内容时,你向服务器发出请求“给我下一篇文章”。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应,这类在特定语境中才能产生的上下文信息即被称为“状态”。我们所说的有状态(Stateful)抑或是无状态(Stateless),都是只相对于服务端来说的,服务器要完成“取下一篇”的请求,要么自己记住用户的状态:这个用户现在阅读的是哪一篇文章,这称为有状态;要么客户端来记住状态,在请求的时候明确告诉服务器:我正在阅读某某文章,现在要读它的下一篇,这称为无状态。
-
转移(Transfer):无论状态是由服务端还是客户端来提供的,“取下一篇文章”这个行为逻辑必然只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务器通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。
RESTful 的系统
满足REST风格的系统应该满足以下六大原则:
-
资源(Resources): 在RESTful系统中,每个URL都代表一个唯一的资源。资源可以是服务器上的文件、数据库中的记录或者任何可以通过网络访问的数据对象。例如,/users/123 可能代表ID为123的用户资源。
-
统一接口(Uniform Interface): REST架构的核心就是其统一的接口。它要求系统架构遵循一组标准的、预定义的操作,主要包括HTTP方法(GET, POST, PUT, DELETE等),以及如何通过HTTP状态码传达结果信息。这些规则使得客户端和服务器之间的交互更为简洁且可预测。
-
无状态(Stateless): 服务器不存储关于客户端上下文的信息,每次请求都是独立的。会话状态(如认证信息、页面浏览历史)应该由客户端负责维护,并在每次请求时附带发送给服务器。
-
可缓存(Cacheable): 利用HTTP协议的缓存机制,客户端可以缓存响应,减少不必要的网络请求,提高效率。通过HTTP响应头,服务器可以指示哪些响应是可以被缓存的。
-
分层系统(Layered System): REST架构允许存在中间层(如代理服务器、负载均衡器),而无需客户端了解或关心这些中间层的存在。每一层只需知道与之直接相邻的层次,从而简化了系统的复杂性。
-
按需代码(Code-On-Demand,可选): 这一原则是指服务器可以向客户端发送执行代码(通常是JavaScript),动态地增强客户端的功能。但这不是必须的,许多RESTful服务不需要此特性。
总结
关于REST风格架构,笔者学习总结到了如下几个观点:
-
面向资源的编程思想只适合做 CRUD,面向过程、面向对象编程才能处理真正复杂的业务逻辑
- 面向过程编程时,为什么要以算法和处理过程为中心,输入数据,输出结果?当然是为了符合计算机世界中主流的交互方式。
- 面向对象编程时,为什么要将数据和行为统一起来、封装成对象?当然是为了符合现实世界的主流的交互方式。
- 面向资源编程时,为什么要将数据(资源)作为抽象的主体,把行为看作是统一的接口?当然是为了符合网络世界的主流的交互方式。
-
REST 与 HTTP 完全绑定,不适合应用于要求高性能传输的场景中
-
带宽和延迟:HTTP协议本身包含较多的开销,比如请求头、响应头等,这些在高频率、低延迟需求的场景下可能会成为瓶颈。
-
连接建立:每次HTTP请求都需要建立TCP连接(虽然有HTTP Keep-Alive机制减少连接建立的开销),对于需要频繁交互的场景,这会增加额外的延迟。
-
无状态性:虽然无状态性使得服务端更易于扩展,但每次请求都需携带所有上下文信息,可能增加数据传输量。
-
协议选择:对于那些对实时性、吞吐量、低延迟有严格要求的应用,如在线游戏、金融交易、实时音视频通信等,REST+HTTP的组合可能不是最佳选择。这些场景可能更适合采用更轻量级的协议或二进制协议(如WebSocket、protobuf配合gRPC、MQTT等),它们能够提供更低的延迟、更高的数据压缩效率和更优的二进制编码性能。
-
-
REST 不利于事务支持
如果“事务”指的是数据库那种的狭义的刚性 ACID 事务,那除非完全不持有状态,否则分布式系统本身与此就是有矛盾的(CAP 不可兼得),这是分布式的问题而不是 REST 的问题。如果“事务”是指通过服务协议或架构,在分布式服务中,获得对多个数据同时提交的统一协调能力(2PC/3PC),譬如WS-AtomicTransaction、WS-Coordination这样的功能性协议,这 REST 确实不支持,假如你已经理解了这样做的代价,仍决定要这样做的话,Web Service 是比较好的选择。如果“事务”只是指希望保障数据的最终一致性,说明你已经放弃刚性事务了,这才是分布式系统中的正常交互方式,使用 REST 肯定不会有什么阻碍,谈不上“不利于”。
- REST 没有传输可靠性支持
REST(Representational State Transfer)本身作为一种架构风格,并不直接涉及传输层的可靠性保证。REST关注的是如何设计分布式系统中的组件,以便它们可以以一种统一且可预测的方式相互通信,特别是通过HTTP等应用层协议。关于传输的可靠性,实际上是底层传输协议的责任,而非REST架构直接提供的功能。
为什么这么说?
- 层次分离:REST架构遵循网络分层原则,其中传输可靠性通常由传输层(如TCP/IP模型中的TCP协议)处理。TCP协议提供了数据包的确认、重传和排序等机制,确保数据的可靠传输,而HTTP协议构建于TCP之上,享受这些可靠性保障。
- 无状态性:REST的一个核心原则是无状态性,服务器不保存关于客户端的上下文信息,每个请求都是独立的。这要求每个请求都携带完成操作所需的所有信息,但并不直接涉及传输可靠性。
- HTTP的错误处理:虽然HTTP协议(REST常用协议)提供了一些机制来处理请求失败的情况,如通过状态码(如4xx和5xx系列)来表示客户端或服务器错误,但这更多是关于请求处理的结果反馈,而非传输过程的可靠性保证。
- 可选的可靠性增强机制:对于需要更高可靠性的RESTful服务,开发者可以采用应用层的解决方案,如幂等性设计、事务补偿逻辑、重试策略、消息队列等,来增强最终的业务操作可靠性,但这已经超出了REST架构本身的范畴。
因此,说REST没有传输可靠性支持,是因为它作为一个架构风格,专注于资源表述和操作,而传输可靠性是由 其下层的网络协议(如TCP)以及上层的应用设计策略共同提供的。
- REST 缺乏对资源进行“部分”和“批量”的处理能力
REST 开创了面向资源的服务风格,却肯定仍并不完美。以 HTTP 协议为基础给 REST 带来了极大的便捷(不需要额外协议,不需要重复解决一堆基础网络问题,等等),但也是 HTTP 本身成了束缚 REST 的无形牢笼。譬如你仅仅想获得某个用户的姓名,RPC 风格中可以设计一个“getUsernameById”的服务,返回一个字符串,尽管这种服务的通用性实在称不上“设计”二字,但确实可以工作;而 REST 风格中你将向服务端请求整个用户对象,然后丢弃掉返回的结果中该用户除用户名外的其他属性,这便是一种“过度获取”(Overfetching)。