关键词:
文章目录
IO调用
操作系统一次IO
过程:
应用程序发起的一次IO
操作包含两个阶段:
IO
调用:应用程序进程向操作系统内核发起调用。IO
执行:操作系统内核完成IO
操作。
操作系统内核完成IO
操作还包括两个过程:
- 准备数据阶段:内核等待
I/O
设备准备好数据 - 拷贝数据阶段:将数据从内核缓冲区拷贝到用户进程缓冲区
其实IO
就是把进程的内部数据转移到外部设备,或者把外部设备的数据迁移到进程内部。外部设备一般指硬盘、socket
通讯的网卡。一个完整的IO
过程包括以下几个步骤:
- 应用程序进程向操作系统发起
IO
调用请求 - 操作系统准备数据,把
IO
外部设备的数据,加载到内核缓冲区 - 操作系统拷贝数据,即将内核缓冲区的数据,拷贝到用户进程缓冲区
五大IO模型
阻塞IO模型
假设应用程序的进程发起IO
调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次IO
操作,称之为阻塞IO
。
阻塞IO
的缺点就是:如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能,可以使用非阻塞IO
优化。
非阻塞IO模型
如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。这就是非阻塞IO
,流程图如下:
非阻塞IO的流程如下:
- 应用进程向操作系统内核,发起
recvfrom
读取数据。 - 操作系统内核数据没有准备好,立即返回
EWOULDBLOCK
错误码(就是EAGAIN
)。 - 应用程序进程轮询调用,继续向操作系统内核发起
recvfrom
读取数据。 - 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
- 完成调用,返回成功提示。
非阻塞IO
模型,简称NIO(Non-Blocking IO)
。它相对于阻塞IO
,虽然大幅提升了性能,但是它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU
资源。可以考虑IO
复用模型,去解决这个问题。
IO多路复用模型
既然NIO
无效的轮询会导致CPU
资源消耗,我们等到内核数据准备好了,主动通知应用进程再去进行系统调用,那不就好了嘛?
在这之前,我们先来复习下,什么是文件描述符fd(File Descriptor)
,它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
IO
复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select
、poll
、epoll
函数),它们可以同时监控多个fd
的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom
系统调用。
IO
多路复用之select
:应用进程通过调用select
函数,可以同时监控多个fd
,在select
函数监控的fd
中,只要有任何一个数据状态准备就绪了,select
函数就会返回可读状态,这时应用进程再发起recvfrom
请求去读取数据。
非阻塞IO
模型(NIO
)中,需要N
(
N
>
=
1
N >= 1
N>=1)次轮询系统调用,然而借助select
的IO
多路复用模型,只需要发起一次询问就够了,大大优化了性能。
但是呢,select
有几个缺点:
- 监听的
IO
最大连接数有限,在Linux
系统上一般为1024
。 select
函数返回后,是通过遍历fdset
,找到就绪的描述符fd
。(仅知道有I/O
事件发生,却不知是哪几个流,所以遍历所有流)
因为存在连接数限制,所以后来又提出了poll
。与select
相比,poll
解决了连接数限制问题。但是呢,select
和poll
一样,还是需要通过遍历文件描述符来获取已经就绪的socket
。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。因此经典的多路复用模型epoll
诞生。
IO
多路复用之epoll
:为了解决select/poll
存在的问题,多路复用模型epoll
诞生,它采用事件驱动来实现,流程图如下:
epoll
先通过epoll_ctl()
来注册一个fd
(文件描述符),一旦基于某个fd
就绪时,内核会采用回调机制,迅速激活这个fd
,当进程调用epoll_wait()
时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是epoll
的亮点。
select | poll | epoll | |
---|---|---|---|
底层数据结构 | 数组 | 链表 | 红黑树和双链表 |
内核实现及工作效率 | 采用轮询方式检测就绪事件,时间复杂度O(n) | 采用轮询方式检测就绪事件,时间复杂度O(n) | 采用回调方式检测就绪事件,时间复杂度O(1) |
最大连接数 | 1024 | 无限制 | 无限制 |
工作模式 | LT | LT | LT和ET(高效) |
fd数据拷贝 | 每次调用select,需要将fd数据从用户空间拷贝至内核空间 | 每次调用poll,需要将fd数据从用户空间拷贝至内核空间 | 使用内存映射(mmap),不需要从用户空间频繁拷贝fd数据至内和空间,降低拷贝的资源消耗 |
事件集合 | 通过3个参数分别传入感兴趣的可读、可写、异常等事件。内核通过对这些参数的在线修改来反馈其中的就绪事件,这使得用户每次调用select都要重置这3个参数 | 统一处理所有事件类型,因此只需要一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents参数反馈其中就绪的事件 | 内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无需反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件 |
epoll
明显优化了IO
的执行效率,但在进程调用epoll_wait()
时,仍然可能被阻塞。能不能酱紫:不用我老是去问你数据是否准备就绪,等我发出请求后,你数据准备好了通知我就行了,这就诞生了信号驱动IO
模型。
IO模型之信号驱动模型
信号驱动IO
不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用sigaction
的时候建立一个SIGIO
的信号),然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过SIGIO
信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用recvfrom
,去读取数据。
信号驱动IO
模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。但是你细看上面的流程图,发现数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下,不管是BIO
,还是NIO
,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。还有没有优化方案呢?AIO
(真正的异步IO
)!
IO 模型之异步IO(AIO)
前面讲的BIO
,NIO
和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此都不算是真正的异步。AIO
实现了IO
全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO
操作执行完毕。
异步IO
的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似思想的业务场景:比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。
- 同步阻塞(
blocking-IO
)简称BIO
- 同步非阻塞(
non-blocking-IO
)简称NIO
- 异步非阻塞(
asynchronous-non-blocking-IO
)简称AIO
一个经典生活的例子:
- 小明去吃同仁四季的椰子鸡,就这样在那里排队,等了一小时,然后才开始吃火锅。(
BIO
) - 小红也去同仁四季的椰子鸡,她一看要等挺久的,于是去逛会商场,每次逛一下,就跑回来看看,是不是轮到她了。于是最后她既购了物,又吃上椰子鸡了。(
NIO
) - 小华一样,去吃椰子鸡,由于他是高级会员,所以店长说,你去商场随便逛会吧,等下有位置,我立马打电话给你。于是小华不用干巴巴坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了美味的椰子鸡(
AIO
)
高性能网络框架
要理解网络框架有哪些,必须要清楚网络框架完成了哪些事情。
大致描述下这个请求处理的流程:
- 远端的机器
A
发送了一个HTTP
请求到服务器B
,此时服务器B
网卡接收到数据并产生一个IO
可读事件; - 我们以同步
IO
为例,此时内核将该可读事件通知到应用程序的listen
线程; listen
线程将任务甩给handler
线程,由handler
将数据从内核读缓冲区拷贝到用户空间读缓冲区;- 请求数据包在应用程序内部进行计算和处理并封装响应包;
handler
线程等待可写事件的到来;- 当这个连接可写时将数据从用户态写缓冲区拷贝到内核缓冲区,并通过网卡发送出去;
备注:上述例子是以同步
IO
为例,并且将线程中的角色分为Listen
线程、Handler
线程、Worker
线程,分别完成不同的工作,后续会详细展开。
所以我们可以知道,要完成一个数据交互,涉及了几大块内容:
IO
事件监听- 数据拷贝
- 数据处理和计算
我们在网络通信中目前划分两种体系结构,分别为:thread-based architecture
(基于线程的架构)、event-driven architecture
(事件驱动模型),事件驱动体系结构是目前比较广泛使用的一种。这种方式会定义一系列的事件处理器来响应事件的发生,并且将服务端接受连接与对事件的处理分离。其中,事件是一种状态的改变。比如,tcp
中socket
的new incoming connection
、ready for read
、ready for write
。Reactor
模式和Proactor
模式都是事件驱动模型的实现方式。
thread-based architecture 基于线程的架构
在早期并发数不多的场景中,有一种One Request One Thread
的架构模式。通俗的说就是:多线程并发模式,一个连接一个线程,服务器每当收到客户端的一个请求, 便开启一个独立的线程来处理。该模式下每次接收一个新请求就创建一个处理线程,线程虽然消耗资源并不多,但是成千上万请求打过来,性能也是扛不住的。
这是一种比较原始的架构,思路也非常清晰,创建多个线程来提供处理能力,这种模式一定程度上极大地提高了服务器的吞吐量,由于在不同线程中,之前的请求在read
阻塞以后,不会影响到后续的请求。但是,仅适用于于并发量不大的场景,因为:
- 线程需要占用一定的内存资源
- 创建和销毁线程也需一定的代价
- 操作系统在切换线程也需要一定的开销
- 线程处理
I/O
,在等待输入或输出的这段时间处于空闲的状态,同样也会造成CPU
资源的浪费
如果连接数太高,系统将无法承受,不再展开讨论此模型。
event-driven architecture 事件驱动模型
当前流行的是基于事件驱动的IO
复用模型,相比多线程模型优势很明显。
在此我们先理解一下什么是Event-Drive-Model
事件驱动模型:事件驱动编程是一种编程范式,程序的执行流由外部事件来决定,它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。
通俗来说就是:有一个循环装置在一直等待各种事件的到来,并将到达的事件放到队列中,再由一个分拣装置来调用对应的处理装置来响应。
Reactor反应堆模式
反应堆模式是一种思想,形式却有很多种。反应堆的本质是什么呢?从本质上理解,无论什么网络框架都要完成两部分操作:
IO
操作:数据包的读取和写入CPU
操作:数据请求的处理和封装
所以上述这些问题由谁来做以及多少线程来做,就衍生出了很多形式,所以不要被表面现象迷惑,出现必有原因,追溯之后我们才能真正掌握它。
反应堆模式根据处理IO
环节和处理数据环节的数量差异分为如下几种:
- 单
Reactor
线程 - 单
Reactor
线程和线程池 - 多
Reactor
线程和线程池
我们来看看这三种常见模式的特点、原理、优缺点、应用场景等。
单Reactor线程模式
这种模式最为简洁,主要是针对于I/O
操作而言,一个线程完成了连接的监听listen()
、接收新连接accept()
、处理连接connect()
、读取数据read()
、写入数据write()
全套工作。由于只使用了一个线程,对于多核利用率偏低,但是编程简单。是不是觉得这个种单线程的模式没有市场?那可未必,不信你看Redis
。
但在目前的单线程Reactor
模式中,在这种模式种IO
操作和CPU
操作是没有分开的,不仅I/O
操作在该Reactor
线程上,连非I/O
的业务操作也在该线程上进行处理了,显然如果在Handler
处理某个请求超时了将会阻塞客户端的正常连接,这可能会大大延迟I/O
请求的响应。在Redis
中由于都是内存操作,速度很快,这种瓶颈虽然存在但是不够明显。我们应该将非I/O
的业务逻辑操作从Reactor
线程上卸载,以此来加速Reactor
线程对I/O
请求的响应。
单Reactor线程和线程池模式
单Reactor
线程模式的IO
操作和CPU
操作是在一个线程内部串行执行的,这样就拉低了CPU
操作效率。为了解决IO
操作和CPU
操作的不匹配,将IO
操作和CPU
操作分别由单独的线程来完成,相互不影响。单Reactor
线程完成IO
操作、复用工作线程池来完成CPU
操作,添加了一个工作者线程池,并将非I/O
操作从Reactor
线程中移出转交给工作者线程池(Thread Pool
)来执行就是一种解决思路。这样能够提高Reactor
线程的I/O
响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O
请求的处理。
在工作者线程池模式中,虽然非I/O
操作交给了线程池来处理,但是所有的I/O
操作依然由Reactor
单线程执行,在高负载、高并发或大数据量的应用场景,依然较容易成为瓶颈。
所以,对于Reactor
的优化,又产生出下面的多Reactor
线程和线程池模式。
多Reactor线程和线程池模式
水平扩展往往是提供性能的有效方法。
我们将Reactor
线程进行扩展,将Reactor
划分为两部分:mainReactor
和subReactor
,一个mainReactor
线程负责处理新连接,其余多个subReactor
线程负责处理连接成功的IO
数据读写。也就是进一步将监听、创建连接和处理连接,分别由两个及以上的线程来完成,进一步提高了IO
操作部分的效率。
mainReactor
负责监听server socket
,用来处理网络新连接的建立,将建立的socketChannel
指定注册给subReactor
,通常一个线程就可以处理;subReactor
维护自己的selector
,基于mainReactor
注册的socketChannel
多路分离I/O
读写事件,读写网络数据,通常使用多线程;对非I/O
的操作,依然转交给工作者线程池(Thread Pool
)执行。此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。在实际生产环境算是比较高配的版本了。
以上就是对Reactor
反应堆模式的总结,Reactor
模式是一种被动的处理模式,当有事件发生时被动处理事件。
高性能网络io模型(代码片段)
同步阻塞式IO开发简单,但在处理IO密集的并发任务时,非常浪费CPU资源,性能低;并且,当一个进程(线程)含有多个套接字上时,同步阻塞式IO会带来问题:因为同步阻塞式IO只支持进程(线程)阻塞在一个套接字上,其余套... 查看详情
linux网络性能优化(代码片段)
Linux网络性能优化我们知道,Linux网络根据TCP/IP模型,构建其网络协议栈。TCP/IP模型由应用层、传输层、网络层、网络接口层等四层组成。而本文将对Linux网络相关性能观测及优化进行分析。性能指标我们常用的衡量网络... 查看详情
肝了一夜,一文说清bionioaio不同io模型演进之路(代码片段)
引言Netty作为高性能的网络通信框架,它是IO模型演变过程中的产物。Netty以JavaNIO为基础,是一种基于异步事件驱动的网络通信应用框架,Netty用以快速开发高性能、高可靠的网络服务器和客户端程序,很多开源框... 查看详情
netty_02_高性能的nio框架(代码片段)
文章目录一、前言二、从NIO到RPC(原理)2.1磁盘IO和网络IO2.2JavaIO的三个阶段(bio-nio-aio)同步阻塞IO(BIO)非阻塞IO(NIO)多路复用机制(select、poll、epoll)异步IO2.3Reactor三种模式单线程单Reactor模型多线程单Reactor模型多线... 查看详情
java性能问题排查提效脚本工具(代码片段)
...往往会出现各种各样的性能瓶颈。其中java常见瓶颈故障模型有cpu资源瓶颈;文件IO瓶颈;网络IO瓶颈;内存资源瓶颈;资源消耗不高程序本身执行慢等场景模型。如何快速定位分析这些类型瓶颈?工欲善其事必先利其器。 本... 查看详情
http协议基础及报文抓包分析(代码片段)
...、工具。更细节的请参考HTTP相关书籍或RFC文档。HTTP基本架构下面我们用一张简单的流程图来展示HTTP协议基本架构,以便大家先有个基本的了解。WebClient可 查看详情
mpp数据库简介及架构分析(代码片段)
...SharedNothing架构的分布式并行结构化数据库集群,具备高性能、高可用、高扩展特性,可以为超大规模数据管理提供高性价比的通用计算平台,并广泛地用于支撑各类数据仓库系统、BI系统和决策支持系统特性并行处理... 查看详情
架构io多路复用(代码片段)
服务器端编程经常需要构造高性能的IO模型,常见的IO模型有:同步阻塞IO(BlockingIO)传统的IO模型同步非阻塞IO(Non-blockingIO)默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCKIO多路复用(IOMultiplexing)经典的Reacto... 查看详情
ncnn模型推理详解及实战(代码片段)
...cpu和内存硬件特性描述。最后结合shufflenetsample解析了,模型推理的全部流程,详解了sample代码的每个细节。一,依赖库知识速学aarch64OpenMPAVX512submoduleaptupgrade二,硬件基础知识速学2.1,内存2.2,CPU三,ncnn推理模型3.1,shufflenetv2... 查看详情
keras深度学习实战——使用长短时记忆网络构建情感分析模型(代码片段)
Keras深度学习实战——使用长短时记忆网络构建情感分析模型0.前言1.构建LSTM模型进行情感分类1.1数据集分析1.2模型构建2.构建多层LSTM进行情感分类相关链接0.前言我们已经学习了如何使用循环神经网络(Recurrentneuralnetworks,RNN)构建... 查看详情
ctpn网络理解(代码片段)
本文主要对常用的文本检测模型算法进行总结及分析,有的模型笔者切实run过,有的是通过论文及相关代码的分析,如有错误,请不吝指正。一下进行各个模型的详细解析CTPN详解代码链接:https://github.com/xiaofengShi/CHINESE-OCRCTPN是... 查看详情
openstack概述及环境部署(代码片段)
目录一、OpenStack概述1.云计算服务模型2.OpenStack的概念3.OpenStack核心组件二、OpenStack架构1.设计基本原则2.概念框架3.逻辑架构4.物理架构4.1网络节点-提供者网络4.2网络节点-自服务网络三、OpenStack环境部署1.基础环境配置(所有... 查看详情
yolov5结构分析与理解—图解(代码片段)
目录网络模型及网络结构网络结构详情代码的整体目录代码detect.py测试 各个模块 整体结构其他资料4种网络的宽度yolov5各个网络模型性能比较 yolov5结构 yolov5四种网络的深度 yolov5网络结构图一些工具代码voc2yolo.py ... 查看详情
dubbo架构设计及入门案例(代码片段)
框架介绍1.1.1概述Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的RPC实现服务的输出和输入功能,可以和Spring框架无缝集成。Dubbo是一款高性能、轻量级的开源JavaRPC框架,它提供了三大核心能力... 查看详情
mlir编译器调度与优化点滴
...方法,这些分析方法针对AI计算过程中关键算子以及网络模型进行建模分析,从PPA(Power-Performance-Area)三个角度评估硬件性能。与此同时,伴随着AI编译框架的发展,尤其受益于MLIR编译器框架的可复用及可扩展性(详见MLIR多层编译... 查看详情
nginx架构模型及常用配置(代码片段)
...ps一、Nginx简介 Nginx是俄罗斯人编写的十分轻量级的、高性能的HTTP服务器和反向代理服务器,同时也是一个IMAP/POP3/SMTP代理服务器。 Nginx的特点 支持5万高并发、内存消耗少。 Nginx在架构中的作用 1)网关—面... 查看详情
mysql性能管理及架构设计:什么影响了数据库查询速度什么影响了mysql性能(代码片段)
一、什么影响了数据库查询速度1.1影响数据库查询速度的四个因素1.2风险分析QPS:QueriesPerSecond意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标... 查看详情
redis批量操作详解及性能分析(代码片段)
通过mget批量执行指令可以节约网络连接和数据传输开销,在高并发场景下可以节约大量系统资源。本文中,我们更进一步,比较一下redis提供的几种批量执行指令的性能。1. 为什么需要批量执行redis指令众所周知... 查看详情