环形IO模型:io_uring

全链路异步化的最终目标

全链路异步化的最终目标,如下图所示:

  • 应用层:编程模型的异步化

  • 框架层:IO线程的异步化

  • OS层:IO模型的异步化

图片

一:应用层:编程模型的异步化

随着云原生时代的到来,底层的组件编程越来越响应式、流化,从命令式编程转换到响应式编程,在非常多的场景,是大势所趋。

二:框架层:IO线程的异步化

选择具有异步回调功能的异步线程模型,如Reactor线程模型。

三:OS层:IO模型的异步化

目前的一个最大难题,是IO模型的异步化。注意,Netty 底层的IO模型,一般用的是select或者epoll,是同步IO,不是异步IO。

第二层:线程模型的异步化

首先来看线程模型的异步化。

Reactor模式

NIO是基于事件机制的,有一个叫做Selector的选择器,阻塞获取关注的事件列表。获取到事件列表后,可以通过分发器,进行真正的数据操作。

图片

上图是Doug Lea在讲解NIO时候的一张图,指明了最简单的Reactor模型的基本元素。

  • Acceptor 处理client的连接,并绑定具体的事件处理器

  • Event 具体发生的事件

  • Handler 执行具体事件的处理者。比如处理读写事件

  • Reactor 将具体的事件分配给Handler

我们可以对上面的模型进行近一步细化,下面这张图同样是Doug Lea的ppt中的。

它把Reactor部分分为mainReactor和subReactor两部分。mainReactor负责监听处理新的连接,然后将后续的事件处理交给subReactor,subReactor对事件处理的方式,也由阻塞模式变成了多线程处理,引入了任务队列的模式。

图片

这两个线程模型,非常重要。

第三层:OS中IO模型的异步化

目前的一个最大难题,是IO模型的异步化。注意,Netty 底层的IO模型,咱们一般用的是select或者 epoll,是同步IO,不是异步IO。

IO模型层的异步化

  • 阻塞式IO (bio)

  • 非阻塞式IO

  • IO复用 (nio)

  • 信号驱动式IO

  • 异步IO(aio)

1.阻塞IO模型

图片

如上图,是典型的BIO模型,每当有一个连接到来,经过协调器的处理,就开启一个对应的线程进行接管。

如果连接有1000条,那就需要1000个线程。线程资源是非常昂贵的,除了占用大量的内存,还会占用非常多的CPU调度时间,所以BIO在连接非常多的情况下,效率会变得非常低。

就单个阻塞IO来说,它的效率并不比NIO慢。但是当服务的连接增多,考虑到整个服务器的资源调度和资源利用率等因素,NIO就有了显著的效果,NIO非常适合高并发场景。

2.非阻塞IO模型

其实,在处理IO动作时,有大部分时间是在等待。比如,socket连接要花费很长时间进行连接操作,在完成连接的这段时间内,它并没有占用额外的系统资源,但它只能阻塞等待在线程中。这种情况下,系统资源并不能被合理的利用。

Java的NIO,在Linux上底层是使用epoll实现的。epoll是一个高性能的多路复用I/O工具,改进了select和poll等工具的一些功能。在网络编程中,对epoll概念的一些理解,几乎是面试中必问的问题。

epoll的数据结构是直接在内核上进行支持的。通过epoll_create和epoll_ctl等函数的操作,可以构造描述符(fd)相关的事件组合(event)。

这里有两个比较重要的概念:

  • fd 每条连接、每个文件,都对应着一个描述符,比如端口号。内核在定位到这些连接的时候,就是通过fd进行寻址的。

  • event 当fd对应的资源,有状态或者数据变动,就会更新epoll_item结构。在没有事件变更的时候,epoll就阻塞等待,也不会占用系统资源;一旦有新的事件到来,epoll就会被激活,将事件通知到应用方

相对于select,epoll有哪些改进?

  • epoll不再需要像select一样对fd集合进行轮询,也不需要在调用时将fd集合在用户态和内核态进行交换

  • 应用程序获得就绪fd的事件复杂度,epoll时O(1),select是O(n)

  • select最大支持约1024个fd,epoll支持65535个

  • select使用轮询模式检测就绪事件,epoll采用通知方式,更加高效

为啥需要IO模型异步化

这里有一个很大的性能损耗点,同步IO中,线程的切换、 IO事件的轮询、IO操作, 都是需要进行 系统调用完成的。

系统调用的性能耗费在哪里?

首先,线程是很”贵”的资源,主要表现在:

  1. 线程的创建和销毁成本很高,线程的创建和销毁都需要通过重量级的系统调用去完成。

  2. 线程本身占用较大内存,像Java的线程的栈内存,一般至少分配512K~1M的空间,如果系统中的线程数过千,整个JVM的内存将被耗用1G。

  3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。过多的线程频繁切换带来的后果是,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统CPU sy值特别高(超过20%以上)的情况,导致系统几乎陷入不可用的状态。

在Linux的性能指标里,有ussy两个指标,使用top命令可以很方便的看到。

图片

us是用户进程的意思,而sy是在内核中所使用的cpu占比。如果进程在内核态和用户态切换的非常频繁,那么效率大部分就会浪费在切换之上。一次内核态和用户态切换的时间,普遍在  微秒  级别以上,可以说非常昂贵了。cpu的性能是固定的,在无用的东西上浪费越小,在真正业务上的处理就效率越高。

影响效率的有两个方面:

  1. 进程或者线程的数量,引起过多的上下文切换。

    进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,如果你的代码切换了线程,它必然伴随着一次用户态和内核态的切换。

  2. IO的编程模型,引起过多的系统态和内核态切换。

    比如同步阻塞等待的模型,需要经过数据接收、软中断的处理(内核态),然后唤醒用户线程(用户态),处理完毕之后再进入等待状态(内核态)。

注意:一次内核态和用户态切换的时间,普遍在 微秒 级别以上,可以说非常昂贵了。

IO模型的异步化的第一个目标:减少线程数量,减少线程切换系统调用带来  CPU 上下文切换的开销。

IO模型的异步化的第一个目标:减少IO系统调用,减少线程切换系统调用带来的带来  CPU 上下文切换开销。

线程模型和IO模型的概念误区

  • 需要要分层思考,就想 WEB应用架构要分层一样。

  • 线程模型和IO模型,要分开来看,不能混为一谈。

很多人把Reactor反应器,认为底层的IO模型是NIO,去看Netty源码,Netty反应器,支持各种IO模型,包括BIO。所以,一定要分层去看。

这里可以把线程模型和IO模型的,分为三层:应用层、框架层、 OS层。具体如下图所示:

图片

Netty的 Reactor 模式,对应到是:线程模型。不是对应到 IO模型。

在IO模型的层面,Tomcat 也用了 NIO,大家一定不要以为Tomcat还用BIO,还用 ,大部分的HTTPClient客户端组件,都用了NIO,都不会使用BIO模型的。

在线程模型的层面,很多的HTTPClient组件,要么没有使用 Reactor模型,要么是使用了Reactor反应性线程模型,但是我们的业务程序不用,咱们的业务程序,用的还是其同步阻塞线程模型的API代码。

图片

如何进行IO模型的异步化。

大家都知道BIO非常的低效,而网络编程中的IO多路复用普遍比较高效。Linux中,一直没有成熟的异步IO内核组件。现在,io_uring已经能够挑战NIO的,功能非常强大。

io_uring在2019加入了Linux内核,目前5.1+的内核,可以采用这个功能。

随着一步步的优化,系统调用这个大家伙,调用次数越来越少了。让我们先看看 linux 中的各种异步 IO,也就是 AIO。

1. glibc aio

官方地址:Perform I/O Operations in Parallel

glibc 是 GNU 发布的 libc 库,该库提供的异步 IO 被称为 glibc aio,在某些地方也被称为 posix aio。glibc aio 用多线程同步 IO 来模拟异步 IO,回调函数在一个单线程中执行。

该实现备受非议,存在一些难以忍受的缺陷和bug,极不推荐使用。详见:http://davmac.org/davpage/linux/async-io.html

2. libaio

linux kernel 2.6 版本引入了原生异步 IO 支持 — libaio,也被称为 native aio。

ibaio 与 glibc aio 的多线程伪异步不同,它真正的内核异步通知,是真正的异步IO。

虽然很真了,但是缺陷也很明显:libaio 仅支持 O_DIRECT 标志,也就是 Direct I/O,这意味着无法利用系统缓存,同时读写的的大小和偏移要以区块的方式对齐。

3. libeio

由于上面两个都不靠谱,所以 Marc Lehmann 又开发了一个 AIO 库 — libeio。

与 glibc aio 的思路一样,也是在用户空间用多线程同步模拟异步 IO,但是 libeio 实现的更高效,代码也更稳定,著名的 node.js 早期版本就是用 libev 和 libeio 驱动的(新版本在 libuv 中移除了 libev 和 libeio)。

libeio 提供全套异步文件操作的接口,让用户能写出完全非阻塞的程序,但 libeio 也不属于真正的异步IO。

libeio 项目地址:https://github.com/kindy/libeio

4. io_uring

接下来就是 linux kernel 5.1 版本引入的 io_uring 了。

io_uring 类似于 Windows 世界的 IOCP,但是还没有达到对应的地位,目前来看正式使用 io_uring 的产品基本没有,目前还是没有一个成熟的基础框架与其匹配,至于 Netty 对 io_uring 的封装,看下来的总体感受是:Netty 为了维持编程模型统一,完全没有发挥出 io_uring 的长处。

io_uring (用户环形IO)

前面讲到,NIO依然有大量的系统调用,那就是Epoll的epoll_ctl。另外,获取到网络事件之后,还需要把socket的数据进行存取,这也是一次系统调用。虽然相对于BIO来说,上下文切换次数已经减少很多,但它仍然花费了比较多的时间在切换之上。

IO只负责对发生在fd描述符上的事件进行通知。事件的获取和通知部分是非阻塞的,但收到通知之后的操作,却是阻塞的。即使使用多线程去处理这些事件,它依然是阻塞的。

如果能把这些系统调用都放在操作系统里完成,那么就可以节省下这些系统调用的时间,io_uring就是干这个的。

从io_uring的名字uring可以看出来,该机制的核心即userring:其申请了一块用户态和内核态共享的内存作为环形数组,并在共享内存中通过ringBuf环形队列的方式来实现内核态和用户态的通信。

缩略语 英语 中文 解析
SQ Submission Queue 提交队列 一整块连续的内存空间存储的环形队列。用于存放将执行操作的数据。
CQ Completion Queue 完成队列 一整块连续的内存空间存储的环形队列。用于存放完成操作返回的结果。
SQE Submission Queue Entry 提交队列项 提交队列中的一项。
CQE Completion Queue Entry 完成队列项 完成队列中的一项。
Ring Ring 比如 SQ Ring,就是“提交队列信息”的意思。包含队列数据、队列大小、丢失项等等信息。

io_uring 的环形队列长成啥样?

前面讲到,io_uring 中,应用程序可以使用两个队列来和 Kernel 进行通信:

  • Submission Queue(SQ)

  • Completion Queue(CQ) 。

而这两个队列中的保存的主要是指针或者编号(index),真正的IO请求,保存在一个基于数组结构的环形队列中,这个环形队列的结构如下图:

图片

这块内存共分为三个区域,分别是 SQ,CQ,SQEs。

SQEs是一个环形数组,保存实际的IO请求,之所以采用了一个额外数组保存 SQEs,是为了方便通过 RingBuffer 提交内存上不连续的请求。两个队列 SQ 和 CQ 中每个节点,保存的并不是IO请求,保存的都是 SQEs 数组的偏移量,实际的请求只保存在 SQEs 数组中。一个 SQE 条目的结构,主要包含以下的内容:

  • Opcode:描述要进行的系统调用的 IO 操作码。如果是读,操作码IORING_OP_READV。

  • Flags:修饰符,可以通过任何请求传递

  • Fd:要读取的文件描述符

  • Address:对于我们的readv调用,它创建了一个缓冲区(或向量)数组来读入数据。因此,address字段包含了该数组的地址。

  • Length:Address 缓冲区 向量数组的长度。

  • User Data:通常这是一个指针,指向一些结构体,其中保存了请求的元数据,来识别应用的请求。当请求从CQ 队列中出来时,并不能保证IO结果与请求SQEs的顺序相同。如果一定保证有序的就会降低性能,  就违背了异步API的初衷。因此,我们需要一些东西来识别我们发出的请求。User Data这可以达到这个目的。

CQE包含

  • Result:readv系统调用的返回值。如果成功,就会有读取的字节数;  否则它将有一个错误代码。

  • User Data:在SQE中传递的指针。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值