OS-08-事件驱动:C10M是如何实现的?
08-事件驱动:C10M是如何实现的?
你好,我是陶辉。
上一讲介绍了广播与组播这种一对多通讯方式,从这一讲开始,我们回到主流的一对一通讯方式。
早些年我们谈到高并发,总是会提到C10K,这是指服务器同时处理1万个TCP连接。随着服务器性能的提升,近来我们更希望单台服务器的并发能力可以达到C10M,也就是同时可以处理1千万个TCP连接。从C10K到C10M,实现技术并没有本质变化,都是用事件驱动和异步开发实现的。[第5讲] 介绍过的协程,也是依赖这二者实现高并发的。
做过异步开发的同学都知道,处理基于TCP的应用层协议时,一个请求的处理代码必须被拆分到多个回调函数中,由异步框架在相应的事件生成时调用它们。这就是事件驱动方式,它通过减少上下文切换次数,实现了C10M级别的高并发。
不过,做应用开发的同学往往不清楚什么叫做“事件”,不了解处理HTTP请求的回调函数与事件间的关系。这样,在高并发下,当多个HTTP请求争抢执行时,涉及资源分配、释放等重要工作的回调函数,就可能在错误的时间被调用,进而引发一系列问题。比如,不同的回调函数对应不同的事件,如果某个函数执行时间过长,就会影响其他请求,可能导致大量请求出现超时而处理失败。
这一讲我们就来介绍一下,事件是怎样产生的?它是如何驱动请求执行的?多路复用技术是怎样协助实现异步开发的?理解了这些,你也就明白了这种事件驱动的解决方案,知道了怎么样实现C10M。
事件是怎么产生的?
要了解“事件驱动”的运作机制,首先就要搞清楚到底什么是事件。这就需要你对网络原理有深入的理解了。
简单来说,从网络中接收到一个报文,就可能产生一个事件。如上一讲介绍过的UDP请求就是最简单的例子,一个UDP请求通常仅由一个网络报文组成,所以,当收到一个UDP报文,就意味着收到一个请求,它会生成一个事件,进而触发回调函数执行。
不过,常见的HTTP等协议都是基于TCP实现的。由于TCP是一种面向字节流的协议,HTTP请求的大小并不受限制,当一个HTTP请求的大小超过TCP报文的最大长度时,请求会被拆分到多个报文中运输,在接收端的缓冲区中重组、排序。因此,并不是每个到达的TCP报文都能生成事件的。
如果不理解事件和TCP报文的关系,就没法准确地掌握处理HTTP请求的函数何时被调用。当然,作为应用开发工程师,我们无须在意实现细节,只要了解TCP连接建立、关闭,以及消息的发送和接收这四个场景中,报文与事件间的关系就可以了。
事件并没有你想象中那么复杂,它只有两种类型:读事件与写事件,其中,读事件表示有到达的消息需要处理,而写事件表示可以发送消息(TCP连接的写缓冲区中有可用空间)。我们先从三次握手建立连接说起,这一过程会产生一读、一写两个事件。
由于TCP允许双向传输,所以建立连接时,会依次在连接的两个方向上建立通道。主动发起连接的一方叫做客户端,被动监听端口等待连接的一方叫做服务器。
客户端首先发送SYN报文给服务器,而服务器收到后回复ACK和SYN(这里我们只需要知道产生事件的过程即可,下一讲会详细介绍这两个报文的含义),当它们到达客户端时,双向连接中由客户端到服务器的通道就建立好了,此时客户端就已经可以发送请求了,因此客户端会产生写事件。接着,客户端发送ACK报文,到达服务器后,服务器上会产生读事件,因为进程原本在监听80等端口,此时有新连接建立成功,应当调用accept函数读取这个连接,所以这是一个读事件。
在建立好的TCP连接上收发消息时,读事件对应着接收到对方的消息,这很好理解。写事件则稍微复杂些,我们举个例子加以说明。假设要发送一个2MB的请求,当调用write函数发送时,会先把内存中的数据拷贝到写缓冲区中后,再发送到网卡上。
为何要多此一举呢?这是因为在对方没有明确表示收到前,TCP会通过定时器重发写缓冲区中的数据,保证消息能够到达对方。写缓冲区是有大小限制的,我在[第10讲]中会详细介绍。这里假设写缓冲区只有1MB,所以调用write发送2MB数据时,write函数的返回值只有1MB,表示写缓冲区已用尽。当收到对方发来的ACK报文后,缓冲区中的数据才能释放,就会产生写事件通知进程发送剩余的那1MB数据。
如同建立连接需要双向建立一样,关闭连接也需要双方各自关闭每个方向的通道。主动关闭的一方发送FIN报文,到达被动方后,内核自动回复ACK报文,这表示从主动方到被动方的通道已经关闭。但被动方到主动方的通道也需要关闭,所以此时被动方会产生读事件,提醒被动方调用close函数关闭连接。
这样,我们就清楚了TCP报文如何产生事件,也明白回调函数何时执行了。然而,同步代码拆分成多个异步函数成本并不低,咱们手里拿着事件驱动这个锤子,可不能看到什么都像是钉子。
什么样的代码值得基于事件来做拆分呢?还得回到高性能这个最终目标上来。我们知道,做性能优化一定要找出性能瓶颈,针对瓶颈做优化性价比才最高。对于服务器来说,对最慢的操作做异步化改造,才能值回开发效率的损失。而服务里对资源的操作速度由快到慢,依次是CPU、内存、磁盘和网络。CPU和内存的执行速度都是纳秒级的,无须考虑事件驱动,而磁盘和网络都可以采用事件驱动的异步方式处理。
相对而言,网络不只速度慢,而且波动很大,既受制于连接对端的性能,也受制于网络传输路径。把操作网络的同步API改为事件驱动的异步API收益最大。而磁盘(特别是机械硬盘)访问速度虽然不快,但它最慢时也不过几十毫秒,是可控的。而且目前磁盘异步IO技术(参见[第4讲])还不成熟,它绕过了PageCache性能损失很大。所以当下的事件驱动,主要就是指网络事件。
该怎样处理网络事件?
有了网络事件的概念后,我们再来看用户态代码如何处理事件。
网络事件是由内核产生的,进程该怎样获取到它们呢?如epoll这样的多路复用技术可以帮我们做到。多路复用是通讯领域的词汇,有些抽象但原理确很简单。
比如,一条高速的光纤上,允许多个用户用较低的网速同时通讯,这就是多路复用。同样道理,一个进程虽然任一时刻只能处理一个请求,但处理每个请求产生的事件时,若耗时控制在1毫秒以内,这样1秒钟就可以处理数千个请求,从更长的时间维度上看,多个请求复用了一个进程,也叫做多路复用(或者叫做时分多路复用)。我们熟知的epoll,就是内核提供给用户态的多路复用接口,进程可以通过它从内核中获取事件。
epoll是如何获取网络事件的呢?最简单的方法,就是在获取事件时,把所有并发连接传给内核,再由内核返回产生了事件的连接,再处理这些连接对应的请求即可。epoll前的select等多路复用函数就是这么干的。
然而,C10M意味着有一千万个连接,若每个socket是4字节,那么1千万连接就是40M字节。这样,每收集一次事件,就需要从用户态复制40M字节到内核态。而且,高性能Server必须及时地处理网络事件,所以每隔几十毫秒就要收集一次事件,性能消耗巨大。
epoll为了降低性能消耗,把获取事件拆分成两步。
- 第一步把需要监控的socket传给内核(epoll_ctl函数),它仅在连接建立等有限的时机调用;
- 第二步收集事件(epoll_wait函数)便不用传递socket了,这样就把socket的重复传递改为了一次传递,降低了性能损耗。
由于网卡的处理能力有限,千兆网卡下,每秒只能接收100MB左右的数据,如果每个请求约10KB,那么每秒大概有1万个请求到达、10万个事件需要处理。这样,即使每隔100毫秒收集一次事件(调用epoll_wait),每次也不过只有1万个事件(100000 Event/s * 0.1s = 10000 Event/s)需要处理,只要保证处理一个事件的平均时间小于10微秒(多核处理器可以做到),100毫秒内就可以处理完这些事件(100ms = 10us * 10000)。 因此,哪怕有1千万并发连接,也能保证1万RPS的处理能力,这就是epoll能在C10M下实现吞吐量的原因。
进程获取到产生事件的socket后,又该如何处理它呢?这里的核心约束是,处理任何一个事件的耗时都应该是微秒级或者毫秒级,否则就会延误其他事件的处理,不只降低了用户的体验,而且会形成恶性循环。
我们知道,为了应对网络的不确定性,每个参与网络通讯的进程都会为请求设置超时时间。一旦某个socket上的事件迟迟不被处理,当客户端的超时定时器触发时,客户端往往会关闭连接并重发请求,这会让服务器雪上加霜。
怎样保证处理一个事件的时间不会太长呢? 我们把处理事件的代码分为三类来看。
第一类是计算任务,虽然内存、CPU的速度很快,然而循环执行也可能耗时达到秒级。所以,如果一定要引入需要密集计算才能完成的请求,为了不阻碍其他事件的处理,要么把这样的请求放在独立的线程中完成,要么把请求的处理过程拆分成多段,确保每段能够快速执行完,同时每段执行完都要均等的处理其他事件,这样通过放慢该请求的处理时间,就保障了其他请求的及时处理。
第二类会读写磁盘,由于磁盘的写入操作使用了PageCache的延迟写特性,当write函数返回时只是复制到了内存中,所以写入操作很快。磁盘的读取操作就比较慢了,这时,通常要把大文件的读取,拆分成许多份,每份仅有几十KB,降低单次操作的耗时。
第三类是通过网络访问上游服务。与处理客户端请求相似,我们必须使用非阻塞socket,用事件驱动方式处理请求。需要注意的是,许多网络服务提供的SDK,都是基于阻塞socket实现的,使用前必须先做完非阻塞改造。比如Memcached的官方SDK是用阻塞socket实现的,Nginx如果直接使用该SDK访问它,性能就会一落千丈。正确的访问方式,是使用第三方提供的ngx_http_memcached_module模块,它用非阻塞socket重新封装了SDK。
总之,网络报文到达后,内核就产生了读、写事件,而epoll函数使得进程可以高效地收集到这些事件。接下来,要确保在进程中处理每个事件的时间足够短,才能及时的处理所有请求,这个过程中既要避免阻塞socket的使用,也要把耗时过长的操作拆成多份执行。最终,通过快速、及时、均等地执行所有事件,异步Server实现了高并发。
小结
最后我们对这一讲做个小结。异步服务改为从事件层面处理请求,在epoll这样的多路复用机制协助下,最终实现了C10M级别的高并发服务。
事件有很多种,网络消息的传输既慢又不可控,所以用网络事件驱动请求的性价比最高。这样,就需要你了解TCP报文是如何产生事件的。
TCP连接建立时,会在客户端产生写事件,在服务器端产生读事件。连接关闭时,则会在被动关闭端产生读事件。在连接上收发消息时,也会产生事件,其中发送消息前的写事件与内核分配的缓冲区有关。
清楚了事件与TCP报文的关系后,可以用多路复用技术获取事件,其中epoll是佼佼者,它取消了收集事件时重复传递的大量socket参数,给C10M的实现提供了基础。
你需要注意的是,处理epoll收集到的事件时,必须保证处理一个事件的平均时间在毫秒级以内。传统的阻塞socket是做不到的,所以必须用非阻塞socket替换阻塞socket。如果事件的回调函数耗时过长,也得拆分为多个耗时短的函数,用多次事件(比如定时器事件)的触发来替代。
虽然我们有了上述的事件驱动方案,但实现C10M还需要更谨慎地使用不过数百GB的服务器内存。关于如何降低内存的消耗,可以关注[第2讲] 提到的内存池,[第11讲] 还会介绍如何减少连接缓冲区的空间占用。
这一讲我们介绍了事件驱动的总体方案,但C10M需要高效的用心几乎所有服务器资源,所以,我们还得通过Linux更精细的控制TCP的行为,接下来的3讲我们将深入Linux,讨论如何优化TCP的性能。
思考题
最后,留给你一个思考题,需要CPU做密集计算的请求,该如何拆分到事件驱动框架中呢?欢迎你在留言区留言,与大家一起探讨。
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。
精选留言:
-
安排 2020-05-15 02:35:52
思考题:可以弄一个计算线程池,多路复用只负责将网络事件收上来,然后交给线程池计算,这样多路复用的回调可以立刻返回并处理下一个事件回调。计算线程池弄一个eventfd,多路复用也监听这个eventfd,线程池算完了之后,通过eventfd通知多路复用。多路复用将计算结果写到socket缓冲区.
-
那时刻 2020-05-15 01:15:16
老师,关于读事件和写事件有个疑问的地方,基于定义:读事件表示有到达的消息需要处理,而写事件表示可以发送消息。在三次握手里的SYN+ACK,客户端产生写事件,为什么不是读事件呢?先读到SYN+ACK然后触发写事件恢复ACK到服务端
作者回复2020-05-15 07:27:07
这个读、写面对的对象,是应用代码。SYN+ACK对于系统层来说,是到达了1个报文,需要内核发送ACK去处理,但对于应用层来说,是需要发送消息了,因此是写事件,这是我的理解
相关文章:

OS-08-事件驱动:C10M是如何实现的?
08-事件驱动:C10M是如何实现的? 你好,我是陶辉。 上一讲介绍了广播与组播这种一对多通讯方式,从这一讲开始,我们回到主流的一对一通讯方式。 早些年我们谈到高并发,总是会提到C10K,这是指服务…...
mysql 主从同步排查和处理 Slave_IO、Slave_SQL
目录 查看主从是否同步 详解Slave_IO、Slave_SQL 判断主从完全同步 各个 Log_File 和 Log_Pos的关系 修复命令 查看主从是否同步 show slave status; Slave_IO_Running、Slave_SQL_Running,这两个值是Yes表示正常,No是异常 使用竖排显示…...

基于解析法和遗传算法相结合的配电网多台分布式电源降损配置(Matlab实现)
目录 1 概述 2 数学模型 2.1 问题表述 2.2 DG的最佳位置和容量(解析法) 2.3 使用 GA 进行最佳功率因数确定和 DG 分配 3 仿真结果与讨论 3.1 33 节点测试配电系统的仿真 3.2 69 节点测试配电系统仿真 4 结论 1 概述 为了使系统网损达到最低值&a…...
07mysql查询语句之子查询
#1.查询和Zlotkey相同部门的员工姓名和工资 SELECT last_name,salary FROM employees WHERE department_id IN ( SELECT department_id FROM employees WHERE last_name Zlotkey ); #2.查询工资比公司平均工资高的员工的员工号࿰…...
笙默考试管理系统-MyExamTest(22)
笙默考试管理系统-MyExamTest(22) 目录 一、 笙默考试管理系统-MyExamTest 二、 笙默考试管理系统-MyExamTest 三、 笙默考试管理系统-MyExamTest 四、 笙默考试管理系统-MyExamTest 五、 笙默考试管理系统-MyExamTest 笙默考试管理系统-MyExa…...

Windows 不同方式打开的cmd/dos窗口属性配置不同
文章目录 1. 默认值(控制台窗口)属性2. "C:\Windows\System32\cmd.exe" 属性3. "命令提示符"属性4. 自定义某标题cmd窗口属性5. cmd快捷方式的属性总结 最近在写某个批处理脚本时,意外发现 Windows系统中,在不…...
性能优化-webpack配置gzip
3步搞定,实测1.3Mjs压缩到363k,体积减少70% 1.装包 yarn add compression-webpack-plugin --dev 2.配置webpack 打开config/webpack.config.js 1)在 module.exports 导出函数前面引入插件 // gzip插件 const CompressionPlugin require(&qu…...

RabbitMQ 教程 | 第3章 客户端开发向导
👨🏻💻 热爱摄影的程序员 👨🏻🎨 喜欢编码的设计师 🧕🏻 擅长设计的剪辑师 🧑🏻🏫 一位高冷无情的编码爱好者 大家好,我是 DevO…...

基于深度学习的CCPD车牌检测系统(PyTorch+Pyside6+YOLOv5模型)
摘要:基于CCPD数据集的高精度车牌检测系统可用于日常生活中检测与定位车牌目标,利用深度学习算法可实现图片、视频、摄像头等方式的车牌目标检测识别,另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检测模型训练数据集…...
input元素中的form属性有什么用?
在HTML中,input元素的form属性用于指定该输入字段所属的表单(form元素)。通过将input元素的form属性设置为相应的表单的id值,可以将输入字段与表单进行关联。 这个属性对于两个主要目的非常有用: 表单关联࿱…...

【数据结构篇C++实现】- 特殊的线性表 - 串
友情链接:C/C系列系统学习目录 文章目录 串🚀一、串的定义🚀二、串的存储结构🛴(一)串的顺序存储结构1、定长顺序存储表示2、堆分配存储表示 🛴(二)串的链式存储结构3、块…...
DevOps系列文章 之 Springboot单元测试
在没有代码生成工具或尝试一门新的 ORM框架时,当我们希望不去另外写 Service 和 Controller 来验证 DAO 层的代码不希望只通过接口请求的方式来验证时,这时候单元测试的方式就可以帮助我们满足这一需求。 在我们开发Web应用时,经常会直接去观…...

04 linux之C 语言高级编程
gcc和gdb GNU工具 编译工具:把一个源程序编译为一个可执行程序调试工具:能对执行程序进行源码或汇编级调试软件工程工具:用于协助多人开发或大型软件项目的管理,如make、CVS、Subvision其他工具:用于把多个目标文件链…...

深入学习 Redis - Stream、Geospatial、HyperLogLog、Bitmap、Bitfields 类型扩展
目录 前言 Stream geospatial HyperLogLog Bitmaps Bitfields 前言 redis 中最关键的五个数据类型 String、List、Hash、Set、Zset 应用最广泛,同时 redis 也推出了额外的 5 个数据类型,他们分别是针对特殊场景才进行的应用的. Ps:这几种…...

Windows11+Opencv+Clion编译源码
Windows11OpencvClion编译源码 参考:https://www.robotsfan.com/posts/69395e08.html 注意事项 编译过程中使用的软件,开源码等所有工具的安装路径一定不要有中文和空格。cmake过程会下载一些文件,如果是局域网的话可能下载不下来…...

【机器学习】Cost Function
Cost Function 1、计算 cost2、cost 函数的直观理解3、cost 可视化总结附录 首先,导入所需的库: import numpy as np %matplotlib widget import matplotlib.pyplot as plt from lab_utils_uni import plt_intuition, plt_stationary, plt_update_onclic…...

【黑马头条之内容安全第三方接口】
本笔记内容为黑马头条项目的文本-图片内容审核接口部分 目录 一、概述 二、准备工作 三、文本内容审核接口 四、图片审核接口 五、项目集成 一、概述 内容安全是识别服务,支持对图片、视频、文本、语音等对象进行多样化场景检测,有效降低内容违规风…...

回归预测 | MATLAB实现GRNN广义回归神经网络多输入单输出回归预测(多指标,多图)
回归预测 | MATLAB实现GRNN广义回归神经网络多输入单输出回归预测(多指标,多图) 目录 回归预测 | MATLAB实现GRNN广义回归神经网络多输入单输出回归预测(多指标,多图)效果一览基本介绍程序设计参考资料效果一览 基本介绍 MATLAB实现GRNN广义回归神经网络多输入单输出回归…...
STM32 HAL库函数——HAL_UART_RxCpltCallback()详解
HAL_UART_RxCpltCallback函数 他是谁,他和谁有关功能用法每收到一个字符,就自动调用一次??示例----接收未知长度的字符 他是谁,他和谁有关 HAL_UART_RxCpltCallback 是一个回调函数,用于在使用 HAL 库进行…...

前端调用合约如何避免出现transaction fail
前言: 作为开发,你一定经历过调用合约的时候发现 gas fee 超出限制,但是不知道报了什么错。这个时候一般都是触发了require错误合约校验。对于用户来说他不理解为什么一笔交易会花费如此大的gas,那我们作为开发如何尽量避免这种情…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...

网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)
Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败,具体原因是客户端发送了密码认证请求,但Redis服务器未设置密码 1.为Redis设置密码(匹配客户端配置) 步骤: 1).修…...

GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...
命令行关闭Windows防火墙
命令行关闭Windows防火墙 引言一、防火墙:被低估的"智能安检员"二、优先尝试!90%问题无需关闭防火墙方案1:程序白名单(解决软件误拦截)方案2:开放特定端口(解决网游/开发端口不通)三、命令行极速关闭方案方法一:PowerShell(推荐Win10/11)方法二:CMD命令…...
算法250609 高精度
加法 #include<stdio.h> #include<iostream> #include<string.h> #include<math.h> #include<algorithm> using namespace std; char input1[205]; char input2[205]; int main(){while(scanf("%s%s",input1,input2)!EOF){int a[205]…...

构建Docker镜像的Dockerfile文件详解
文章目录 前言Dockerfile 案例docker build1. 基本构建2. 指定 Dockerfile 路径3. 设置构建时变量4. 不使用缓存5. 删除中间容器6. 拉取最新基础镜像7. 静默输出完整示例 docker runDockerFile 入门syntax指定构造器FROM基础镜像RUN命令注释COPY复制ENV设置环境变量EXPOSE暴露端…...