正如本书第1章所介绍的,网络通信的“三态性”是分布式系统的基本特性,我们无法避免该特性,只能想办法利用和管理它,这就对设计和实现分布式系统提出了挑战。而Dubbo作为一款优秀的分布式服务框架,针对网络通信专门设计并实现了一套完整的技术组件。本节将介绍网络通信的基本概念以及Dubbo框架中网络通信问题的解决方案。
网络通信涉及的知识面很广,本节将对网络连接、I/O模型、序列化以及可靠性等基本概念展开讨论。
1.网络连接
当客户端向服务器端发起连接请求时,服务器端接收请求,双方就可以建立连接。服务器端响应来自客户端的请求需要完成一次读写过程,这时候双方都可以发起关闭操作,所以短连接一般只会在客户端和服务器端之间传递一次读写操作,也就是说在TCP连接建立后,数据包传输完成即关闭连接。短连接结构简单,管理起来比较容易,存在的连接都是有用的连接,不需要额外的控制手段。
长连接则不同,当客户端与服务器端完成一次读写操作之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。这样当连接建立后,就可以连续发送多个数据包,能够节省资源并降低时延。
长连接和短连接的产生与客户端和服务器端采取的关闭策略有关,在具体的应用场景中采用具体的策略,没有十全十美的选择,只有合适的选择。在RPC框架实现过程中,考虑到性能和服务治理等因素,通常使用长连接进行通信。
2.I/O模型
现代操作系统都包括内核空间和用户空间,内核空间主要存放内核代码和数据,是供系统进程使用的空间;而用户空间主要存放用户代码和数据,是供用户进程使用的空间。一般的I/O操作都分为两个阶段,包括内核空间和用户空间之间的数据传输过程,以套接口的输入操作为例,首先等待网络数据到来,当数据分组到来时,将其拷贝到内核空间的临时缓冲区中,然后将内核空间临时缓冲区中的数据拷贝到用户空间缓冲区中。围绕I/O操作的这两个阶段,存在几种主流的I/O模型。
最基本的I/O模型就是阻塞I/O,BIO要求客户端请求数与服务器端线程数一一对应,显然服务器端可以创建的线程数会成为系统的瓶颈。非阻塞I/O和I/O复用、信号驱动I/O技术实际上也会在I/O上形成阻塞,真正在I/O上没有形成阻塞的是异步I/O。各个I/O模型如图3-1所示,可以看到其中有些I/O模型从发起请求开始就一直处于阻塞状态,而有些则会采用一定的检查机制,直到发起请求的条件就绪时才会发生阻塞。
图3-1 操作系统I/O模型
结合图3-1,我们发现前4种I/O模型的主要区别是在第一阶段,因为它们在第二阶段都会阻塞等待数据由内核空间拷贝到用户空间;而异步I/O很明显与前面4种有所不同,它在第一阶段和第二阶段都不会阻塞。
3.序列化
序列化是网络通信中的另一个核心概念,我们已经在前文中明确了序列化的概念和作用。在现实开发过程中,存在一组现成的序列化方式,常见的包括文本和二进制两大类。XML和JSON是文本类序列化方式的代表,而二进制实现的方案包括使用Google的Protocol Buffer和Facebook的Thrift等工具。对于一个序列化实现方案而言,以下几方面的需求可以帮我们做出合适的选择。
从功能上讲,选择序列化的关注点在于所支持的数据结构种类以及接口友好性。数据结构种类主要关于该序列化工具是否支持泛型和Map/List等复杂数据结构,有些序列化工具并不内置这些复杂数据结构。而接口友好性涉及该序列化工具是否需要定义中间语言(Intermediate Language,IL),例如Protocol Buffer需要.proto文件、Thrift需要.thrift文件。通过中间语言实现序列化在一定程度上增加了使用该序列化工具的复杂度。同时,在分布式系统中,各个独立的分布式服务在原则上都可以建立自身的技术体系,形成异构系统,而异构系统实现交互就需要提供跨语言支持。Java自身的序列化机制无法支持多语言也是我们使用其他序列化技术的一个重要原因。像前面提到过的Protocol Buffer、Thrift以及Apache Avro都是跨语言序列化技术的代表。
另外,性能可能是我们在选择序列化方案的过程中最看重的一个指标。性能指标主要包括序列化之后的码流大小、序列化/反序列化速度和CPU/内存资源占用。表3-1列举了目前主流的一些序列化技术,可以看到在序列化和反序列化时间维度上,阿里巴巴的Fastjson具有一定优势,而从空间维度上看,相较于其他工具我们可以优先选择Protocol Buffer。
表3-1 序列化性能比较
4.可靠性
在发生网络闪断、超时等网络状态不稳定以及业务系统故障等问题时,网络之间的通信必须能够快速感知并修复。常见的网络通信保障手段包括链路有效性检测以及断线之后的重连处理。
从原理上讲,要确保通信链路的可靠性就必须对链路进行周期性的有效性检测,通用的做法就是心跳检测。心跳检测通常有两种实现方式:一种是在TCP层通过建立长连接在发送方和接收方之间传递心跳信息;另一种则是在应用层,心跳信息根据系统要求可能包含一定的业务逻辑。
发送方检测到通信链路中断时,会在事先约定好的重连间隔时间之后发起重连操作。如果重连失败,则周期性地使用该间隔时间进行重连直至重连成功或超时。
我们在第2章中介绍Dubbo整体架构的演进过程中提到了Remoting模块,该模块包含3个组件,分别是Exchange、Transport和Serialize。
从技术分层上讲,Remoting模块处于整个Dubbo框架的底层,是第4章将要介绍的服务发布和引用的基础。本章及后面介绍框架的相关内容的讲解思路是从框架的底层实现过程出发,推导出上层设计。
显然,Remoting模块的组件呈现的是一种对称结构,即Dubbo的生产者和消费者都依赖底层的网络通信。所以,我们也将分别从服务器端和客户端两个角度出发分析Dubbo中具体的网络通信过程。
而在Dubbo中,网络通信实际上委托给了第三方组件。Dubbo通过SPI机制提供了Netty、Mina等多种通信框架的集成。这部分内容相当于对Remoting模块的补充,从层次上讲应该属于Dubbo框架的最底层。