一、What
1)小故事
张三和李四都在同一个家公司负责商品交易的模块,两个人平时开发甚是紧密。
🙋🏻♂️ 张三:“李四,我这边一个商品下单了,但是付款数额不对,你帮我查下支付有没有问题”
🙋🏻 李四:“张三,支付这边检验价格的时候有点问题,实付金额和预付金额对不上”
往常,他们相邻而坐,有什么问题也是 张口就问 。但是随着业务的增长,他们的工作被细化到不同的子模块中,而 分布式系统 的概念也恰逢其时地变得流行起来。"拆分系统"的计划迫在眉睫。于是张三自然而然地分配到了订单模块,而李四到了交易模块。相应,两个人的工位也需要分开,这个时候他们意识到应该没法像从前那样即呼即应了 。
这个时候该如何解决沟通的问题?
李四人搬走了,但是 联系方式 还在,张三需要李四协助的时候可以通过 电话 的方式直呼李四,虽然说通过电话的这种方式略显间接,但仍然保留了解问题的能力,只是沟通的形式变了。
通过阅读这个故事,我们总结一下其中几个核心概念
- 张三李四可以直接交谈。(直接调用)
- 拆分模块(分布式系统)
- 联系方式(接口定位)
- 利用电话沟通(远程通信)
- 打电话的过程(过程调用)
而这几个概念也正是 RPC 的核心所在。
2)概要
RPC (Remote Procedure Call) 是一种用于实现远程通信和分布式计算的协议。它允许在不同的计算机或进程之间进行通信,使得这些计算机或进程能够像调用本地函数一样调用位于远程计算机或进程上的函数或方法。
RPC 协议的基本原理是客户端调用远程服务器上的函数,并将函数参数传递给服务器。 服务器执行相应的函数逻辑,并将结果返回给客户端。
从客户端的角度来看,RPC 调用就像是调用本地函数一样,而不需要关心远程函数的实现和通信细节。
简单来说:从本质上讲,它使一台机器上的程序能够调用另一台机器上的子程序,而不会意识到它是远程的。
3)HTTP
1、HTPP 与 RPC 的先后
RPC是一个较早的抽象,它的思想在1970年代早期就已经存在,而具体实现上的一个里程碑是在1980年代。它代表了一种通用概念:从一个计算机进程中调用另一个进程的函数或过程,无论这两个进程是否在同一台机器上。
HTTP首次出现在1991年。它是由蒂姆·伯纳斯-李在1990年至1991年期间开发的,作为万维网(World Wide Web,简称WWW)项目的一部分。
因此从时间线上看,RPC 是早于 HTTP 出现的。
2、既生 RPC 何生 HTTP
有了RPC,理论上可以通过各种协议进行方法调用,但HTTP为万维网提供了一个标准化的、广泛支持的方式来交换信息和服务,它不仅限于方法调用,还包括数据的获取、提交、更新和删除等。
比方说 A 公司开发了一套数据管理系统,在 A 公司内部可以使用 RPC 协议 进行方法调用获取数据,而这个时候 B 公司想要集成 A 公司的能力,这个时候通过方法调用的方式就不大合适,就需要利用万维网上更加标准的协议来进行通信,也就是 HTTP 协议。
主要区别
RPC 与 REST 最大的区别就在于 RPC 提供了更好的抽象,甚至将网络传输细节彻底隐藏了,而 REST 没有。具体来说,REST 至少要求用于提供 URL 以及请求参数,而 RPC 隐藏了与网络传输的相关实现细节。另一方面,RPC 可以基于任何网络通信协议,而 REST 通常基于 HTTP(或者 HTTPS)协议。
- 设计目标
- RPC 协议是一种用于实现远程过程调用的协议,它主要是让客户端能够像调用本地函数一样调用远程服务器上的函数。更侧重于方法的调用和参数传递,通常用于构建分布式系统、微服务架构等场景,提供了更直接的远程函数调用能力。
- HTTP 协议是一种通用的应用层协议。它提供了一套标准的请求和响应语义,支持网页浏览、资源访问、API 调用等场景。HTTP 具有广泛的应用范围,可以用于浏览器与服务器之间的通信,也可以用于不同系统之间的数据交换。
- 传输协议
- RPC 协议可以基于多种传输协议实现,如 TCP、UDP、HTTP 等,通过自定义的协议栈和编码方式来进行数据传输。
- HTTP 协议基于 TCP/IP,使用统一的格式规范,如请求行、请求头、请求体等,以及常见的编码方式如 JSON、XML、Form 表单等。
- 通信模型:
- RPC 协议是一种点对点的通信模型,客户端和服务器之间建立直接的连接,进行函数调用和返回结果。
- HTTP 协议是一种客户端-服务器模型,客户端发送请求,服务器接收请求并返回响应,每次请求都需要建立新的连接。
3、性能比较
HTTP 协议和 RPC 协议在性能方面有一些差异,这些差异主要由以下几个因素决定:
- 通信开销:
- HTTP 协议通常使用文本格式(如 JSON、XML)进行数据传输,相对较为冗长。每次请求和响应都会包含大量的头部信息,增加了数据传输的开销。
- RPC 协议通常使用二进制序列化格式(如 Protocol Buffers、Thrift),相对更为紧凑。它通常专注于方法调用和参数传递,减少了不必要的开销。
- 连接复用:
- HTTP 协议在默认情况下使用短连接,即每个请求都需要建立一个新的连接。这对于频繁的请求会增加连接建立和关闭的开销。
- RPC 协议通常支持长连接,在一个连接上可以进行多次的方法调用。这样可以减少连接的建立和关闭次数,提高性能。
- 序列化和反序列化:
- HTTP 协议使用通用的文本格式进行数据传输,需要进行文本到对象的序列化和反序列化操作。这些操作可能会消耗一定的时间和计算资源。
- RPC 协议通常使用二进制序列化格式,可以更高效地进行序列化和反序列化,减少了转换的开销。
相对来说,RPC 协议通常在性能方面比 HTTP 协议更优秀。由于 RPC 协议的设计目标更加专注于方法调用和参数传递,它通常采用更紧凑的数据格式、支持长连接等机制,以提供更高的性能和效率。但在实际应用中,具体的性能差异需要根据具体情况进行评估和测试。
二、How
1)核心概念
- 客户端(Client): 发起远程函数调用的一方。负责将本地的函数或方法调用转化为网络请求,并将网络响应转化回本地的函数或方法调用的结果。
- 客户端存根(Client Stub): 客户端存根将函数调用及其参数编码、序列化后通过网络发送请求。
- 网络模块(Network): 用于传输远程调用讯息的媒介,可以是TCP/IP、HTTP或其他网络协议。
- 服务端骨架(Server Skeleton): 与客户端存根对应,负责将接收到的请求解包、反序列化后调用实际的服务服务过程。并将结果重新包装发送回客户端。
- 服务端(Server): 提供远程过程实际执行的地方,即实现了具体逻辑的服务或应用程序。
2)通信流程
从上面中我们认识了 RPC 的五大概念,可以通过一张时序图将五大概念串行起来,如下:
具体流程:
- Client 客户端 通过调用本地服务的方式调用需要消费的服务
- Client Stub 存根 接收到调用请求后负责将方法,入参等信息序列化(组装)成能够进行网络传输的消息体
- Client Stub 存根 找到远程的服务地址,并且将消息通过网络发送给服务端
- Server Stub 服务端骨架 收到消息后进行解码(反序列化操作)
- Server Stub 服务端骨架 根据解码结果调用本地的服务进行相关处理
- Server 服务端 执行具体业务逻辑并将处理结果返回给 Server Stub 服务端骨架
- Server Stub 服务端骨架 将返回结果重新打包成消息(序列化)并通过网络发送至消费方
- Client Stub 存根 接收到消息,并进行解码(反序列化);
- Client Stub 存根 将解码后的结果返回给 Client 客户端
3)实现架构
当我们认识到 RPC 的通信架构了,进一步就可以考虑如何实现 RPC 框架了!
其中 Client 客户端 和 Server 服务端 是关键角色,一个负责消费,一个负责提供。在 Client 客户端 我们要实现无感知地侵入,达到客户端完全不会意识到 Server 服务端 的存在,就需要用上 Proxy 代理 能力。当代理拦截到客户端调用的方法后,还需要将数据序列化后进行发送,这个时候 Serializer 模块 就不可少,当序列完数据就得通过网络将数据进行传输,因此便需要 Network 模块,但是在传输之前,我们需要找到远程服务的地址,因此 Registry 注册中心 也少不了。
1、五大模块
通过简单梳理我们大概整理了 5 大模块:
- 客户端(Client): 客户端模块,发起远程函数调用请求。
- 序列化模块(Serializer): 序列化模块,负责数据的序列化和反序列化。
- 网络(Network): 网络模块,用于传输远程调用信息。
- 注册中心(Registry): 注册中心模块,负责服务的注册与发现。
- 服务端(Server): 提供远程过程实际执行的地方,即实现了具体逻辑的服务或应用程序。
架构图如下:
2、技术分析
聊完理论谈实践,为了实现以上各个模块之间的串行能力,我们需要用到的技术能力如下:
1)动态代理
生成 Client Stub 存根 和 Server Stub 服务端骨架 的时候需要用到 Java 的动态代理技术,可以使用JDK提供的原生的动态代理机制,也可以使用开源的CGLib代理, Javassist字节码生成技术。
在构建RPC框架时,生成 Client Stub 存根 和 Server Stub 服务端骨架 是实现远程方法调用不可或缺的一环。这一过程往往需要动态代理技术来实现,在Java平台上,我们有几种选择来达到这个目的。
- 使用JDK提供的原生的动态代理机制,它主要是通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来完成的,适用于需要代理接口而非类的场合。
- 使用开源的CGLib库。CGLib能够在运行时对类进行扩展,通过继承的方式生成新的子类,它不需要接口就能实现动态代理。
- Javassist 提供了一种更加直接操作字节码的方式,通过这种字节码技术,可以在不打破原有类结构的情况下,新增或者改变类的行为。
2)序列化
序列化是数据处理的关键环节,它使得复杂数据结构能够转换成字节序列,以便于存储或网络传输,这一过程亦称作编码。相对地,反序列化将这些字节序列重新构建成原始对象,即解码过程。在分布式系统与数据交换频繁的应用中,序列化的效率至关重要。
当评估并选择合适的序列化框架时,效率、灵活性和生态支持是几个关键的考量因素。当前市场上的一些高效开源序列化库,比如Kryo、FastJson和Protobuf,各自都有其优势。
- Kryo 是一个小巧且快速的序列化库,它被广泛应用于需要高速序列化操作的场景,尤其是在游戏和高性能计算中。
- FastJson 以其速度快和使用简单闻名,特别适用于Web服务和轻量级应用,它的优势在于处理JSON格式数据的高效性。
- Protobuf,由Google开发,是一个以效率和兼容性为主要目标的结构化数据序列化方法。相较于传统的XML,它更小、更快、更简单,且拥有良好的跨平台性。
3)网络通信
为了提高系统处理并发请求的能力,传统的同步阻塞IO(BIO)模型并不适宜,因为它在等待数据读写过程中会导致线程阻塞,从而降低了并发处理的效率。因此,采用非阻塞IO(NIO)是解决高并发场景下性能问题的恰当选择。NIO支持异步IO操作,允许线程在处理其它任务时并行地等待IO操作完成,这大大提升了资源的利用率和系统的吞吐量。
在构建高并发网络应用时,我们有多种框架可以选择。Netty和Apache MINA是两个流行的高性能网络应用框架,它们都利用了Java NIO的优势。Netty特别被广泛用于其简单的API和稳定的性能,而MINA同样提供了一个易于使用和扩展的异步IO框架。根据应用的具体需求和开发团队的熟悉度,我们可以选取最适合的框架来构建我们的网络服务,以确保在高并发场景下应用的最佳网络性能和响应速度。
4)注册中心
服务注册与发现是核心组件,它用于服务间的动态定位。市面上有多种成熟的服务注册中心方案,包括Redis、ZooKeeper、Consul等。其中,
选择合适的注册中心时,我们不仅需要考虑技术特性,比如数据一致性、可用性和可扩展性,还要考虑与现有系统环境的兼容性,以及未来的维护和支持情况。每种方案都有其特定优势和场景适用性,例如Redis在处理高速缓存和消息队列方面表现卓越,Consul提供更现代的服务网络功能,如健康检查和服务网格集成,ZooKeeper凭借其强一致性的特点,经常被选用来处理服务注册和发现的问题,同时有效解决分布式系统中避免单点故障及处理分布式部署的挑战
4)常见 RPC 框架
1、gRPC
- 开源RPC框架,由Google主导开发。
- 使用HTTP/2作为传输协议,以Protocol Buffers作为其接口定义语言。
- 支持多种编程语言,适用于构建跨语言的服务。
2、Apache Thrift
- 由Facebook开发并贡献给Apache的RPC框架。
- 支持多种编程语言,允许定义数据类型和服务接口在一个统一的接口描述语言(IDL)文件中。
- 框架生成服务端和客户端代码,供不同语言编写的程序使用。
3、Apache Dubbo
- 高性能的Java RPC框架。
- 用于构建大规模分布式系统,支持多种通信协议。
- 提供灵活的服务治理和动态配置功能。
三、End
在本篇文章中,我们探究了 RPC 的核心概念和基本原理,了解到其如何使得跨网络的服务调用变得透明而无缝。了解RPC不仅仅是关于掌握一个技术的使用,更是理解分布式系统设计中关于模块化、服务解耦、和伸缩性设计的深层次思想。
随着微服务架构的兴起,RPC也随之进化,适应了更复杂的网络模式和更严格的性能需求。无论你是一个初级开发者还是高级开发者,RPC都应该是你日常开发过程中绕不开的一个话题。未来的网络计算充满了无限可能,而RPC无疑将继续在其中扮演着重要的角色。感谢您的阅读!