远程调用的实现依赖于它所具备的基本组成结构。在本节接下来的内容中,我们将系统分析远程调用的组成结构,并进一步引出实现这一组成结构所需要具备的技术组件。
如果我们站在最高的层次来看远程调用的执行流程,那么就是一个服务的消费者向服务的提供者发起远程调用并获取返回结果的过程,如图3-1所示。
图3-1 远程调用基本模型
接下来,我们对图3-1进行展开。我们知道服务提供者需要暴露服务访问的入口,而服务消费者则会向服务提供者所暴露的访问入口发起请求,如图3-2所示。
图3-2 远程调用模型细化之一
从图3-2中可以看到,我们对服务消费者和服务提供者的组成结构做了细化,并提取了Channel、Protocol、Connector和Acceptor这四个技术组件。在这四个技术组件中,前两个属于公共组件,而后两个则面向服务的提供者和服务的消费者,分别用于发起请求和接收响应。
在具备了用于完成底层网络通信的技术组件之后,如图3-3所示,我们再来看如何从业务接口定义和使用的角度出发进一步对远程调用的组成结构进行扩充。
图3-3 远程调用模型细化之二
图3-3中出现了用于代表业务接口的API组件。同时,我们也看到了分别代表客户端和服务器的Client和Server组件。我们不难想象这些组件的定位和作用。而这里需要重点介绍的是Caller组件和Invoker组件。Caller组件位于服务消费者端,会根据API的定义信息发起请求。而Invoker组件位于服务提供者端,负责对Server执行具体的调用过程并返回结果。
最后,为了对远程调用过程进行更好的控制,我们还会引入两个技术组件,分别是Proxy和Processor。完整的远程调用组成结构如图3-4所示。
图3-4 远程调用组成结构
如图3-4所示,从命名上看,位于服务消费者端的Proxy组件充当了一种代理机制,确保服务消费者能够像调用本地方法一样调用远程服务。而位于服务提供者端的Processor组件的作用则是为远程调用执行过程添加各种辅助性支持,例如线程管理、超时控制等。
这样,我们对整个远程调用的演进过程做了详细的描述。通过对这些技术组件做进一步的梳理,我们会发现这些组件可以归为三大类,即客户端组件、服务端组件和公共组件。其中,客户端组件与职责包括如下组件。
❑ Client组件,负责导入远程接口代理实现。
❑ Proxy组件,负责对远程接口执行代理。
❑ Caller组件,负责执行远程调用。
❑ Connector组件,负责连接服务器。
服务端组件与职责包括如下组件。
❑ Server组件,负责导出远程接口。
❑ Invoker组件,负责调用服务端接口。
❑ Acceptor组件,负责接收网络请求。
❑ Processor组件,负责处理调用过程。
而客户端和服务端所共有的组件即公共组件如下。
❑ Protocol组件,负责执行网络传输。
❑ Channel组件,负责数据在通道中进行传输。
关于远程调用的组成结构介绍到这里就结束了。在这一组成结构的基础上,如果采用合适的编程语言和实现技术,原则上我们就可以自己动手实现一个远程调用框架。这就是接下来要讨论的核心技术。
远程调用包含一组固有的技术体系。在Dubbo、Spring Cloud、Spring Cloud Alibaba等主流的开源框架中,这些技术体系使用起来都非常简单,甚至你都没有意识到正在使用这些体系,因为框架为开发人员屏蔽了底层的实现细节。让我们一起来看一下。
1.网络通信
网络通信的涉及面很广,首先关注的是网络连接。而关于网络连接最基本的概念就是通常所说的长连接(也叫持久连接,Persistent Connection)和短连接(Short Connection)。当网络通信采用TCP(传输控制协议)时,在真正的读写操作之前,服务器与客户端之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接时就可以释放这个连接。连接的建立需要三次握手,而释放则需要四次握手,每个连接的建立都意味着需要消耗资源和时间,TCP握手协议如图3-5所示。
图3-5 TCP握手协议
当客户端向服务端发起连接请求,服务端接收请求时,然后双方就可以建立连接。服务端响应来自客户端的请求就需要完成一次读写过程,这时候双方都可以发起关闭操作,所以短连接一般只会在客户端/服务端间传递一次读写操作。也就是说TCP连接在数据包传输完成即会关闭。显然,短连接结构和管理比较简单,存在的连接都是有用的连接,不需要额外的控制手段。
长连接则不同,当客户端与服务端完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。这样当TCP连接建立后,就可以连续发送多个数据包,能够节省资源并降低时延。
长连接和短连接的产生在于客户端和服务端采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。例如在Dubbo框架中,考虑到性能和服务治理等因素,通常使用长连接进行通信。而对于Spring Cloud/Spring Cloud Alibaba而言,为了构建轻量级的微服务交互体系,使用的则是短连接。
关于网络通信还有一个值得分析的技术点是I/O(输入/输出)模型。最基本的I/O模型就是阻塞I/O(Blocking I/O, BI/O),要求客户端请求数与服务端线程数一一对应,显然服务端可以创建的线程数会成为系统的瓶颈。非阻塞I/O(Non-blocking I/O, NI/O)和I/O复用技术实际上也会在I/O上形成阻塞,真正在I/O上没有形成阻塞的是异步I/O(Asynchronized I/O, AI/O)。图3-6展示了各种I/O模型的工作特性。
图3-6 操作系统I/O模型的工作特性
2.序列化
所谓序列化(Serialization)就是将对象转化为字节数组,用于网络传输、数据持久化或其他用途,而反序列化(Deserialization)则是把从网络、磁盘等读取的字节数组还原成原始对象,以便后续业务逻辑操作。
序列化的方式有很多,常见的有文本和二进制两大类。XML(可扩展标示语言)和JSON(JS对象简谱)是文本类序列化方式的代表,而二进制实现的方案包括Google的Protocol Buffer和Facebook的Thrift等。对于一个序列化实现方案而言,以下几方面的需求特性可以帮助我们作出合适的选择。
序列化基本功能的关注点在于所支持的数据结构种类以及接口友好性。数据结构种类体现在对泛型和Map/List等复杂数据结构的支持,有些序列化工具并不内置这些复杂数据结构。接口友好性涉及是否需要定义中间语言(Intermediate Language, IL),正如Protocol Buffer需要.proto文件,Thrift需要.thrift文件,通过中间语言实现序列化在一定程度上增加了使用的复杂度。
另外,在分布式系统中,各个独立的分布式服务原则上都可以具备自身的技术体系,形成异构化系统,而异构系统实现交互就需要跨语言支持。Java自身的序列化机制无法支持多语言也是我们使用其他各种序列化技术的一个重要原因。像Protocol Buffer、Thrift以及Apache Avro都是跨语言序列化技术的代表。同时,我们也应该注意到,跨语言支持的实现与所支持的数据结构种类以及接口友好性存在一定的矛盾。要做到跨语言就需要兼容各种语言的数据结构特征,通常意味着要放弃Map/List等部分语言所不支持的复杂数据结构,而使用各种格式的中间语言的目的也正是在于能够通过中间语言生成各个语言版本的序列化代码。
性能可能是我们在序列化工具选择过程中最看重的一个指标。性能指标主要包括序列化之后码流大小、序列化/反序列化速度和CPU/内存资源占用。表3-1列举了目前主流的一些序列化技术,可以看到在序列化和反序列化时间维度上Alibaba的fastjson具有一定优势。而从空间维度上看,相较其他技术我们可以优先选择Protocol Buffer。
表3-1 序列化性能比较
兼容性(Compatibility)在序列化机制中体现的是版本概念。业务需求的变化势必导致分布式服务接口的演进,而接口的变动是否会影响使用该接口的消费方,是否也需要消费方随之变动成为在接口开发和维护过程中的一个痛点。在接口参数中新增字段、删除字段和调整字段顺序都是常见的接口调整需求,比方说Protocol Buffer就能实现前向兼容性,从而确保调整之后新接口、老接口都能继续可用。
3.传输协议
ISO/OSI网络模型分为7个层次,自上而下分别是应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。其中传输层实现端到端连接,会话层实现互联主机通信,表示层用于数据表示,应用层则直接面向应用程序。
架构的设计和实现通常会涉及传输层及以上各个层次的相关协议,通常所说的TCP就属于传输层,而HTTP则位于应用层。TCP面向连接、可靠的字节流服务,可以支持长连接和短连接。HTTP是一个无状态的面向连接的协议,基于TCP的客户端/服务端请求和应答标准,同样支持长连接和短连接,但HTTP的长连接和短连接本质上还是TCP的连接模式。
我们可以使用TCP和HTTP等公共协议作为基本的传输协议,但大部分框架内部往往会使用私有协议进行通信,这样做的主要目的在于提升性能,因为公共协议出于通用性考虑添加了很多辅助性功能,这些辅助性功能会消耗通信资源从而降低性能,设计私有协议可以确保协议尽量精简。另外,出于扩展性的考虑,具备高度定制化的私有协议也比公共协议更加容易实现扩展。当然,私有协议一般都会实现对公共协议的外部对接。
4.服务调用
服务调用存在两种基本方式,即同步调用模式和异步调用模式。其中服务同步调用如图3-7所示。
从图3-7中可以看到,同步调用的执行流程比较简单。在同步调用中,服务消费者在获取来自服务提供者的响应之前一直处于等待状态。而异步调用则不同,服务消费者一旦发送完请求之后就可以继续执行其他操作,直到服务提供者异步返回结果并通知服务消费者进行接收,服务异步调用如图3-8所示。
图3-7 服务同步调用
图3-8 服务异步调用
显然,使用异步调用的目的在于获取高性能。但是,异步调用的开发过程比较复杂,对开发人员的要求较高,所以很多远程调用框架提供了专门的异步转同步机制,即面向开发人员提供的是同步调用的API,而具体执行过程则使用的是异步机制。
除了同步调用和异步调用之外,还存在并行(Parallel)调用和泛化(Generic)调用等调用方法。这些调用方式并不常用,本书不做详细展开。