当前位置: 首页 > 科技观察

图-深入理解Linux高性能网络架构的那些事儿

时间:2023-03-14 13:34:51 科技观察

本文转载自微信公众号《后端技术指南针》,作者指南针氪金入口。转载本文请联系后台技术指南针公众号。1.孤独的小黑上周北京很冷。周五晚上,大白下班去了地铁站。他收到好朋友小黑发来的一条微信:于是大白转身扫了辆自行车找五道口。小黑选择了一个不错的位置。小黑:你今天下班好早啊!大白:据我们所知,我们心里有工作,到处都是办公桌,不要拘泥于形式。明显能感觉到小黑哥最近好像有些疲惫,看不到之前那双眼睛里的不灵不灵的光了。大白:你下午约见了哪家公司?什么职位?如何?小黑:是一家做自动驾驶的创业公司。很熟悉,但回答不到位。大白:哦,我知道了。也就是当时没看懂,一头雾水。现在突然问起,想不起来要点了。小黑:差不多,问我做过哪些高性能网络框架模型,就是IO和事件驱动的。说完,小黑喝了一大口啤酒,大白看出小黑心里有些落寞。毕竟在帝都,竞争,工作的压力,生活的琐事一直围绕着我们,金钱和好运却巧妙的避开了我们……想到这里,大白也深深的吸了一口,我的命到头了忍不住了,开始吧!大白:黑哥,你说这个问题真的很难回答。它充满了术语和有点模棱两可的东西。我认为我们应该抓住本质并加以解释。小黑:来吧,表演开始吧,我会好好学习的。大白决定和小黑好好聊聊linux开发常用的高性能网络框架的一些东西。火锅让夜晚和天气不那么寒冷。通过本文你将学习到以下内容:IO事件和IO多路复用线程模型和事件驱动模型的架构基于事件驱动Reactor模式同步IO和异步IO详解2.IO事件和IO多路复用2.1什么是IO事件IO指的是Input/Output,但是从中文的角度来说,out和in是相对的,所以我们需要一个参考。这里,我们的参考对象是程序运行时的主要存储空间,外部通常包括网卡、磁盘等,有了上面的设置,就容易理解多了。我们来看一下:IO的本质是数据的流动。数据可以从网卡传输到程序内存,也可以从程序内存写入网卡。磁盘操作也是如此。因此,常见的IO可以分为:NetworkIO:内存和网卡的数据交互FileIO:内存和磁盘的数据交互那么什么是IO事件呢?事件可以理解为一种状态或动作,即状态迁移时会触发相应的动作。网络IO事件通常包括:可读事件可写事件异常事件了解可读和可写事件是非常有必要的。一般来说,一个套接字大部分时间是可写的,但并不是所有的时间都是可读的。可读一般是指一个新的连接或者一个已经存在的连接有新的数据交互,对于服务端程序来说也是一个关键事件。2.2什么是IO多路复用假设?如果有几万个IO事件,应用应该如何管理?这是关于IO多路复用。IO多路复用本质上就是应用程序通过IO多路复用函数向内核注册多种类型的IO事件。当这些注册的IO事件发生变化时,内核通过IO复用功能通知应用程序。从图中可以看出,IO多路复用中复用的是一个负责监控和管理这些IO事件的线程。一个线程之所以可以管理数百个IO事件,是因为大多数时候在某个时刻只会触发少量的IO事件。大概是这样的:草原上一只大狗可以养几十只羊,因为大多数时候只有几只羊不守规矩地跑来跑去,其他的都乖乖吃草。3.网络框架设计要素要理解网络框架是什么,必须清楚网络框架完成的是什么。大致描述一下这个请求处理的流程:远程机器A向服务器B发送一个HTTP请求,此时服务器B的网卡收到数据并产生一个IO可读事件;我们以同步IO为例,此时内核会将可读事件通知给应用程序的Listen线程;Listen线程将任务转储给Handler线程,Handler将数据从内核读缓冲区复制到用户空间读缓冲区;请求数据包在应用程序内部计算处理并封装响应包;Handler线程等待可写事件的到来;当连接可写时,将数据从用户态写缓冲区复制到内核缓冲区,并通过网卡发送出去;注意:上面的例子是同步IO的例子,线程中的角色分为Listen线程、Handler线程、Worker线程,分别完成不同的任务,后面会详细展开。所以我们可以知道,要完成一次数据交互,涉及到几大块内容:IO事件监控数据复制数据处理计算大白认为这三块内容无论是什么框架形式都无法避免,并且是也是理解网络架构的关键。4.高性能网络框架实践4.1基于线程的模型在早期并发量不多的场景中,有一种OneRequestOneThread架构模型。在这种模式下,每次接收到新请求时都会创建一个处理线程。虽然线程不会消耗大量资源,但是性能无法处理成千上万的请求。这是一个比较原始的架构,思路很清晰。创建多个线程是为了提供处理能力,但在高并发生产环境中几乎没有应用,本文不再展开。4.2事件驱动模型当前流行的基于事件的IO多路复用模型与多线程模型相比具有明显的优势。这里我们先了解一下什么是事件驱动的Event-Drive-Model。事件驱动编程是一种编程范式。程序的执行流程由外部事件决定。它的特点是事件循环,当外部事件发生时,使用回调机制触发相应的处理。通俗地说:有一个循环设备等待各种事件的到来,并将到达的事件放入队列,然后一个排序设备调用相应的处理设备进行响应。4.3Reactor模式第一次听到这个模式的时候,我很迷茫。什么是反应堆?经过一番研究,我发现反应堆是一个核物理概念,大致是这样的:核反应堆是核电站的心脏,它的工作原理是这样的:原子是由原子核和核外电子组成的,原子核是由质子和中子组成。当铀235的原子核受到外来中子轰击时,一个原子核会吸收一个中子,分裂成两个质量较小的原子核,同时释放出2-3个中子。这次裂变产生的中子轰击另一个铀235原子核引起新的裂变,连续过程就是裂变的连锁反应。结合这种核裂变图,好像当一个请求来的时候,服务器内部会延伸出很多分支来完成响应,一个变成两个,两个变成四个,甚至更多,真的有反应堆的感觉。接下来我们看看reactor模式是如何构建高性能网络框架的。5、reactor模型详解reactor模型是一种思想,但是有很多种形式。5.1reactor模式的本质是什么?本质上不管是什么网络框架都要完成两部分操作:IO操作:数据包的读写CPU操作:数据请求的处理和封装那么上面的问题由谁来负责,用多少线程来做做它会产生很多形式,所以不要被表面现象所迷惑,一定是有原因的,追根溯源才能真正把握。Reactor模式根据处理IO链路和数据处理链路数量的不同,分为以下几种:单Reactor线程单Reactor线程和线程池多Reactor线程和线程池下面我们来看看特点,原理,这三种常见模式的优缺点、应用场景等。5.2单Reactor线程模式这种模式是最简洁的。一个线程完成监控连接、接收新连接、处理连接、读取数据、写入数据的一整套任务。由于只使用一个线程,多核的利用率低,但编程简单。您认为这种单线程模型没有市场吗?不一定是这样。如果你不相信我,看看Redis。在这种模式下,IO操作和CPU操作是不分离的,都是一个线程完成的。显然,如果Handler处理请求超时,就会阻塞客户端的正常连接。在Redis中,因为所有的内存操作都进行,所以速度非常快。这个瓶颈虽然存在,但还不够明显。5.3单Reactor线程和线程池模式为了解决IO操作和CPU操作的不匹配问题,即IO操作和CPU操作在一个线程内串行执行,降低了CPU操作的效率。一种解决方案是将IO操作和CPU操作从单独的线程中分离出来,每个线程互不影响。一种方案是用单个Reactor线程完成IO操作,复用工作线程池完成CPU操作。在这种模式下,Reactor线程完成连接管理和数据读写,完全控制IO操作。工作线程池处理从上游分发过来的任务,对其中的数据进行解码、计算、编码,然后返回给Reactor线程和客户端完成交互。这种模式有效利用了多核,但是单Reactor线程完成IO操作在高并发场景下还是会造成瓶颈。也就是说,连接太多,一个Reactor线程忙不过来建立新连接和响应旧连接,所以Reactor线程也需要几个helper。5.4Multi-Reactor线程和线程池模式水平扩展往往是提升性能的有效途径。我们扩展Reactor线程,一个Reactor线程负责处理新的连接,多个Reactor线程负责处理成功连接的IO数据读写。也就是说监听&连接的创建和连接的处理进一步由两个或多个线程完成,进一步提高了IO操作部分的效率。该模式属于比较高端的版本,在实际生产环境中也有使用。5.5拓展:同步IO和异步IO我们很容易区分什么是阻塞IO和非阻塞IO,那么什么是同步IO和异步IO呢?前面提到,Reactor模式很重要的一个环节就是调用read/write函数来完成数据拷贝,由应用程序自己完成,内核只负责通知监听事件的到来,所以Reactor模式是本质上是一个非阻塞的同步IO。还有一个Preactor模式。操作系统借助系统自身的异步IO特性,进行数据拷贝。完成后,可以通知应用程序去取。效率更高,但是底层需要依赖内核的异步IO机制。完成。底层异步IO机制可以借助DMA和Zero-Copy技术实现,理论上性能更高。目前的Windows系统通过IOCP实现了真正的异步I/O,但是Linux系统中的异步I/O并不完善。比如Linux中的boost.asio模块就支持异步IO,但是目前的Linux系统还是基于Reactor模式的非阻塞同步IO为主。6.小结本文从IO事件和IO多路复用入手,讲解网络架构底层的组成。继续扩展基于线程模型和事件驱动模型的网络框架的特点和设计元素。然后重点描述Reactor模式的核心性质及其在生产环境中的多种形式。最后简单介绍一下同步IO和异步IO的区别,以及Preactor模式的优势。希望读者朋友能够摒弃专业术语和表述,抓住问题的本质和重点,找到适合自己思维方式的高性能网络架构设计的理解和掌握方法。或许,高性能网络框架只是纸老虎。