网络通信优化之通信协议:如何优化RPC网络通信?
RPC 通信是大型服务框架的核心
我们经常讨论微服务,首要应该了解的就是微服务的核心到底是什么,这样我们在做技术选型时,才能更准确地把握需求。
就我个人理解,我认为微服务的核心是远程通信和服务治理。远程通信提供了服务之间通信的桥梁,服务治理则提供了服务的后勤保障。所以,我们在做技术选型时,更多要考虑的是这两个核心的需求。
服务的拆分增加了通信的成本,特别是在一些抢购或者促销的业务场景中,如果服务之间存在方法调用,比如,抢购成功之后需要调用订单系统、支付系统、券包系统等,这种远程通信就很容易成为系统的瓶颈。所以,在满足一定的服务治理需求的前提下,对远程通信的性能需求就是技术选型的主要影响因素。
目前,很多微服务框架中的服务通信是基于 RPC 通信实现的,在没有进行组件扩展的前提下,SpringCloud 是基于 Feign 组件实现的 RPC 通信(基于 Http+Json 序列化实现), Dubbo 是基于 SPI 扩展了很多 RPC 通信框架,包括 RMI、Dubbo、Hessian 等 RPC 通信框架(默认是 Dubbo+Hessian 序列化)。不同的业务场景下,RPC 通信的选择和优化标准也不同。
我们部门在选择微服务框架时,选择了 Dubbo。当时的选择标准就是RPC 通信可以支持抢购类的高并发,在这个业务场景中,请求的特点是瞬时高峰、请求量大和传入、传出参数数据包较小。而 Dubbo 中的 Dubbo 协议就很好地支持了这个请求。
无论从响应时间还是吞吐量上来看,单一 TCP 长连接+Protobuf 序列化实现的 RPC 通信框架都有着非常明显的优势。
在高并发场景下,我们选择后端服务框架或者中间件部门自行设计服务框架时,RPC 通信是重点优化的对象。
什么是 RPC 通信
无论是微服务、SOA、还是 RPC 架构,它们都是分布式服务架构,都需要实现服务之间的互相通信,我们通常把这种通信统称为 RPC 通信。
RPC(Remote Process Call),即远程服务调用,是通过网络请求远程计算机程序服务的通信技术。RPC 框架封装好了底层网络通信、序列化等技术,我们只需要在项目中引入各个服务的接口包,就可以实现在代码中调用 RPC 服务同调用本地方法一样。正因为这种方便、透明的远程调用,RPC 被广泛应用于当下企业级以及互联网项目中,是实现分布式系统的核心。
RMI(Remote Method Invocation)是 JDK 中最先实现了 RPC 通信的框架之一,RMI的实现对建立分布式 Java 应用程序至关重要,是 Java 体系非常重要的底层技术,很多开源的 RPC 通信框架也是基于 RMI 实现原理设计出来的,包括 Dubbo 框架中也接入了RMI 框架。接下来我们就一起了解下 RMI 的实现原理,看看它存在哪些性能瓶颈有待优化。
RMI:JDK 自带的 RPC 通信框架
目前 RMI 已经很成熟地应用在了 EJB 以及 Spring 框架中,是纯 Java 网络分布式应用系统的核心解决方案。RMI 实现了一台虚拟机应用对远程方法的调用可以同对本地方法的调用一样,RMI 帮我们封装好了其中关于远程通信的内容。
RMI 的实现原理
RMI 远程代理对象是 RMI 中最核心的组件,除了对象本身所在的虚拟机,其它虚拟机也可以调用此对象的方法。而且这些虚拟机可以不在同一个主机上,通过远程代理对象,远程应用可以用网络协议与服务进行通信。
RMI 在高并发场景下的性能瓶颈
Java 默认序列化
RMI 的序列化采用的是 Java 默认的序列化方式,性能并不是很好,而且其它语言框架也暂时不支持 Java 序列化。
TCP 短连接
由于 RMI 是基于 TCP 短连接实现,在高并发情况下,大量请求会带来大量连接的创建和销毁,这对于系统来说无疑是非常消耗性能的。
阻塞式网络 I/O
网络通信存在 I/O 瓶颈,如果在 Socket 编程中使用传统的 I/O 模型,在高并发场景下基于短连接实现的网络通信就很容易产生 I/O 阻塞,性能将会大打折扣。
一个高并发场景下的 RPC 通信优化路径
SpringCloud 的 RPC 通信和 RMI 通信的性能瓶颈就非常相似。SpringCloud 是基于 Http通信协议(短连接)和 Json 序列化实现的,在高并发场景下并没有优势。
RPC 通信包括了建立通信、实现报文、传输协议以及传输数据编解码等操作,接下来我们就从每一层的优化出发,逐步实现整体的性能优化。
1. 选择合适的通信协议
要实现不同机器间的网络通信,我们先要了解计算机系统网络通信的基本原理。网络通信是两台设备之间实现数据流交换的过程,是基于网络传输协议和传输数据的编解码来实现的。其中网络传输协议有 TCP、UDP 协议,这两个协议都是基于 Socket 编程接口之上,为某类应用场景而扩展出的传输协议。
基于 TCP 协议实现的 Socket 通信是有连接的,而传输数据是要通过三次握手来实现数据传输的可靠性,且传输数据是没有边界的,采用的是字节流模式。
基于 UDP 协议实现的 Socket 通信,客户端不需要建立连接,只需要创建一个套接字发送数据报给服务端,这样就不能保证数据报一定会达到服务端,所以在传输数据方面,基于UDP 协议实现的Socket 通信具有不可靠性。UDP 发送的数据采用的是数据报模式,每个UDP 的数据报都有一个长度,该长度将与数据一起发送到服务端。
通过对比,我们可以得出优化方法:为了保证数据传输的可靠性,通常情况下我们会采用TCP 协议。如果在局域网且对数据传输的可靠性没有要求的情况下,我们也可以考虑使用UDP 协议,毕竟这种协议的效率要比 TCP 协议高。
2. 使用单一长连接
如果是基于 TCP 协议实现 Socket 通信,我们还能做哪些优化呢?
服务之间的通信不同于客户端与服务端之间的通信。客户端与服务端由于客户端数量多,基于短连接实现请求可以避免长时间地占用连接,导致系统资源浪费。
但服务之间的通信,连接的消费端不会像客户端那么多,但消费端向服务端请求的数量却一样多,我们基于长连接实现,就可以省去大量的 TCP 建立和关闭连接的操作,从而减少系统的性能消耗,节省时间。
3. 优化 Socket 通信
建立两台机器的网络通信,我们一般使用 Java 的 Socket 编程实现一个 TCP 连接。传统的 Socket 通信主要存在 I/O 阻塞、线程模型缺陷以及内存拷贝等问题。我们可以使用比较成 熟的通信框架,比如 Netty。Netty4 对 Socket 通信编程做了很多方面的优化,具体见下方。
实现非阻塞 I/O:多路复用器 Selector 实现了非阻塞 I/O 通信。
高效的 Reactor 线程模型:Netty 使用了主从 Reactor 多线程模型,服务端接收客户端请求连接是用了一个主线程,这个主线程用于客户端的连接请求操作,一旦连接建立成功,将会监听 I/O 事件,监听到事件后会创建一个链路请求。
链路请求将会注册到负责 I/O 操作的 I/O 工作线程上,由 I/O 工作线程负责后续的 I/O 操作。利用这种线程模型,可以解决在高负载、高并发的情况下,由于单个 NIO 线程无法监听海量客户端和满足大量 I/O 操作造成的问题。
串行设计:服务端在接收消息之后,存在着编码、解码、读取和发送等链路操作。如果这些操作都是基于并行去实现,无疑会导致严重的锁竞争,进而导致系统的性能下降。为了提升性能,Netty 采用了串行无锁化完成链路操作,Netty 提供了 Pipeline 实现链路的各个操作在运行期间不进行线程切换。
零拷贝:一个数据从内存发送到网络中,存在着两次拷贝动作,先是从用户空间拷贝到内核空间,再是从内核空间拷贝到网络 I/O 中。而 NIO 提供的ByteBuffer 可以使用 Direct Buffers 模式,直接开辟一个非堆物理内存,不需要进行字节缓冲区的二次拷贝,可以直接将数据写入到内核空间。
除了以上这些优化,我们还可以针对套接字编程提供的一些 TCP 参数配置项,提高网络吞吐量,Netty 可以基于 ChannelOption 来设置这些参数。
TCP_NODELAY:TCP_NODELAY 选项是用来控制是否开启 Nagle 算法。Nagle 算法通过缓存的方式将小的数据包组成一个大的数据包,从而避免大量的小数据包发送阻塞网络,提高网络传输的效率。我们可以关闭该算法,优化对于时延敏感的应用场景。
SO_RCVBUF 和 SO_SNDBUF:可以根据场景调整套接字发送缓冲区和接收缓冲区的大小。
SO_BACKLOG:backlog 参数指定了客户端连接请求缓冲队列的大小。服务端处理客户端连接请求是按顺序处理的,所以同一时间只能处理一个客户端连接,当有多个客户端进来的时候,服务端就会将不能处理的客户端连接请求放在队列中等待处理。
SO_KEEPALIVE:当设置该选项以后,连接会检查长时间没有发送数据的客户端的连接状态,检测到客户端断开连接后,服务端将回收该连接。我们可以将该时间设置得短一些,来提高回收连接的效率。
4. 量身定做报文格式
接下来就是实现报文,我们需要设计一套报文,用于描述具体的校验、操作、传输数据等内容。为了提高传输的效率,我们可以根据自己的业务和架构来考虑设计,尽量实现报体小、满足功能、易解析等特性。
5. 编码、解码
序列化编码和解码的过程,对于实现一个好的网络通信协议来说, 兼容优秀的序列化框架是非常重要的。如果只是单纯的数据对象传输,我们可以选择性能相对较好的 Protobuf 序列化,有利于提高网络通信的性能。
6. 调整 Linux 的 TCP 参数设置选项
如果 RPC 是基于 TCP 短连接实现的,我们可以通过修改 Linux TCP 配置项来优化网络通信。
我们可以通过 sysctl -a | grep net.xxx 命令运行查看 Linux 系统默认的的 TCP 参数设置, 如果需要修改某项配置,可以通过编辑 vim/etc/sysctl.conf,加入需要修改的配置项, 并通过 sysctl -p 命令运行生效修改后的配置项设置。通常我们会通过修改以下几个配置项来提高网络吞吐量和降低延时。