RPC框架更适配哪种网络IO模型?

RPC是解决进程间通信的一种方式。一次RPC调用,本质就是服务消费者与服务提供者间的一次网络信息交换的过程。

服务调用者通过网络IO发送一条请求消息,服务提供者接收并解析,处理完相关的业务逻辑之后,再发送一条响应消息给服务调用者,服务调用者接收并解析响应消息,处理完相关的响应逻辑,一次RPC调用便结束了。

网络通信是整个RPC调用流程的基础

常见的网络IO模型

常见的网络IO模型分为四种:同步阻塞IO(BIO)、同步非阻塞IO(NIO)、IO多路复用和异步非阻塞IO(AIO)。在这四种IO模型中,只有AIO为异步IO,其他都是同步IO。

最常用的就是同步阻塞IO和IO多路复用。

阻塞IO(blocking IO)

同步阻塞IO是最简单、最常见的IO模型,在Linux中,默认情况下所有的socket都是blocking的。

操作流程

首先,应用进程发起IO系统调用后,应用进程被阻塞,转到内核空间处理。之后内核开始等待数据,等待到数据之后,再将内核中的数据拷贝到用户内存中,整个IO处理完毕后返回进程。最后应用的进程解除阻塞状态,运行业务逻辑。

系统内核处理IO操作分为两个阶段——等待数据拷贝数据。而在这两个阶段中,应用进程中IO操作的线程会一直都处于阻塞状态,如果是基于Java多线程开发,那么每一个IO操作都要占用线程,直至IO操作结束。

IO多路复用(IO multiplexing)

多路复用IO是在高并发场景中使用最为广泛的一种IO模型,如Java的NIO、Redis、Nginx的底层实现就是此类IO模型的应用,经典的Reactor模式也是基于此类IO模型。

什么是IO多路复用?具体流程

多个网络连接的IO可以注册到一个复用器(selector) 上,当用户进程调用了select,那么整个进程会被阻塞。同时,内核会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核中拷贝到用户进程。

用户进程发起了select调用,进程会被阻塞,当发现该select负责的socket有准备好的数据时才返回,之后才发起一次read,整个流程要比阻塞IO要复杂,似乎也更浪费性能。但最大优势在于,一个线程内可以同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

为什么阻塞IO和IO多路复用最为常用?

实际在网络IO的应用上,需要的是系统内核的支持以及编程语言的支持

在系统内核的支持上,现在大多数系统内核都会支持阻塞IO、非阻塞IO和IO多路复用,但像信号驱动IO、异步IO,只有高版本的Linux系统内核才会支持。

在编程语言上,无论C++还是Java,在高性能的网络编程框架的编写上,大多数都是基于Reactor模式,其中最为典型的便是Java的Netty框架,而Reactor模式是基于IO多路复用。当然,在非高并发场景下,同步阻塞IO是最为常见的。

在这四种常用的IO模型中,应用最多的、系统内核与编程语言支持最为完善的,便是阻塞IO和IO多路复用。这两种IO模型,已经可以满足绝大多数网络IO的应用场景。

高并发场景:IO多路复用;

非高并发场景:异步阻塞IO。

RPC框架更适配哪种网络IO模型?

RPC调用在大多数的情况下,是一个高并发调用的场景,在网络通信的处理上,会选择IO多路复用的方式。开发语言的网络通信框架的选型上,最优的选择是基于Reactor模式实现的框架,如Java语言,首选的框架便是Netty框架(Java还有很多其他NIO框架,但目前Netty应用得最为广泛),并且在Linux环境下,也要开启epoll来提升系统性能(Windows环境下是无法开启epoll的,因为系统内核不支持)。

零拷贝。在应用的过程中,同样非常重要的。

什么是零拷贝?

系统内核处理IO操作分为两个阶段——等待数据拷贝数据。等待数据,就是系统内核在等待网卡接收到数据后,把数据写到内核中;而拷贝数据,就是系统内核在获取到数据后,将数据拷贝到用户进程的空间中。以下是具体流程:

image-20241027215357078

应用进程的每一次写操作,都会把数据写到用户空间的缓冲区中,再由CPU将数据拷贝到系统内核的缓冲区中,之后再由DMA将这份数据拷贝到网卡中,最后由网卡发送出去。

一次写操作数据要拷贝两次才能通过网卡发送出去,而用户进程的读操作则是将整个流程反过来,数据同样会拷贝两次才能让应用程序读取到数据。

应用进程的一次完整的读写操作,都需要在用户空间与内核空间中来回拷贝,并且每一次拷贝,都需要CPU进行一次上下文切换(由用户进程切换到系统内核,或由系统内核切换到用户进程)。这样十分浪费CPU和性能。

为了减少进程间的数据拷贝,提高数据传输的效率,就会用到零拷贝(Zero-copy)技术。

所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,都可以通过一种方式,让应用进程向用户空间写入或者读取数据,就如同直接向内核空间写入或者读取数据一样,再通过DMA将内核中的数据拷贝到网卡,或将网卡中的数据copy到内核。

用户空间与内核空间都将数据写到一个地方虚拟内存

image-20241027220111521

零拷贝有两种解决方式,分别是 mmap+write 方式sendfile 方式,mmap+write方式的核心原理就是通过虚拟内存来解决的。

这是操作系统层面上的零拷贝,主要目标是避免用户空间与内核空间之间的数据拷贝操作,可以提升CPU的利用率,减少了CPU在用户空间与内核空间之间的上下文切换,从而提升了网络通信效率与应用程序的整体性能。

Netty零拷贝

Netty的零拷贝与操作系统的零拷贝是有些区别的,Netty的零拷贝偏向于用户空间中对数据操作的优化,这对处理TCP传输中的拆包粘包问题有着重要的意义,对应用程序处理请求数据与返回数据也有重要的意义。

那收到消息后,对数据包的分割和合并,是在用户空间完成,这里也会存在数据的拷贝操作,是用户空间内部内存中的拷贝处理操作。Netty的零拷贝就是为了解决这个问题,在用户空间对数据操作进行优化

Netty是怎么对数据操作进行优化?

  • Netty 提供了 CompositeByteBuf 类,它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免了各个 ByteBuf 之间的拷贝。
  • ByteBuf 支持 slice 操作,因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝。
  • 通过 wrap 操作,可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象, 进而避免拷贝操作。

Netty框架中很多内部的ChannelHandler实现类,都是通过CompositeByteBuf、slice、wrap操作来处理TCP传输中的拆包与粘包问题的。

Netty有没有解决用户空间与内核空间之间的数据拷贝问题的方法呢?

Netty 的 ByteBuffer 可以采用 Direct Buffers,使用堆外直接内存进行Socket的读写操作,最终的效果与虚拟内存所实现的效果是一样的。

Netty 还提供 FileRegion 中包装 NIO 的 FileChannel.transferTo() 方法实现了零拷贝,这与Linux 中的 sendfile 方式在原理上是一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不进大厂不改名1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值