本文节选自《深入理解Nginx模块开发与架构解析(第2版)》第8章 Nginx基础架构 >> 8.2Nginx的架构设计 >> 8.2.3 请求的多阶段异步处理,和这段文字一样格式的内容是我追加的注解。

这里所讲的多阶段异步处理请求与事件驱动架构是密切相关的,换句话说,请求的多阶段异步处理只能基于事件驱动架构实现。什么意思呢?就是把一个请求的处理过程按照事件的触发方式划分为多个阶段,每个阶段都可以由事件收集、分发器来触发。

例如,处理一个获取静态文件的HTTP请求可以分为以下几个阶段(见表8-1)。

表8-1 处理获取静态文件的HTTP请求时切分的阶段及各阶段的触发事件:

阶段意义 触发事件
建立TCP连接 接收到TCP中的SYNC包
开始接收用户请求 接收到TCP中的ACK包表示连接建立成功
接收到用户请求并分析已接收的请求是否完整 接收到用户的数据包
接收到完整的用户请求后开始处理用户请求 接收到用户的数据包
由目标静态文件中读取部分内容(避免长期阻塞事件分发者进程)并直接发送给用户 接收到用户的数据包、或者接收到TCP中的ACK包表示用户已接收到上次发送的数据包,TCP滑动窗口向前滑动
对于非keep-alive请求,在发送完静态文件后主动关闭连接 接收到TCP中的ACK包表示用户已接收到之前发送的所有数据包
由于用户关闭连接而结束请求 接收到TCP中的FIN包

这个例子中大致分为7个阶段,这些阶段是可以重复发生的,因此一个下载静态资源请求可能会由于请求数据过大、网速不稳定等因素而被分解成上千个表8-1中所列出的阶段。

对于嵌入式软件而言,可以拿I2C驱动做个例子,I2C的操作过程可以划分为以下阶段:发送起始位阶段、发送设备地址阶段、数据传输阶段、ACK处理阶段、发送停止位阶段。其中数据传输阶段和ACK处理阶段会循环多次。

异步处理和多阶段是相辅相成的,只有把请求分为多个阶段,才有所谓的异步处理。也就是说,当一个事件被分发到事件消费者中进行处理时,事件消费者处理完这个事件只相当于处理完1个请求的某个阶段。什么时候可以处理下一个阶段呢?这只能等待内核的通知,即当下一次事件出现时,epoll等事件分发器将会获取到通知,再继续调用事件消费者处理请求。这样,每个阶段中的事件消费者都不清楚本次完整的操作究竟什么时候会完成,只能异步被动地等待下一次事件的通知。

请求的多阶段异步处理优势在哪里?这种设计配合事件驱动架构,将会极大地提高网络性能,同时使得每个进程都能全力运转,不会或者尽量少地出现进程休眠状况。因为一旦出现进程休眠,必然减少并发处理事件的数目,一定会降低网络性能,同时会增加请求时间的平均时延!这时,如果网络性能无法满足业务需求将只能增加进程数目,进程数目过多就会增加操作系统内核的额外操作:进程间切换,可是频繁地进行进程间切换仍会消耗CPU等资源,从而降低网络性能。同时,休眠的进程会使进程占用的内存得不到有效释放,这最终必然导致系统可用内存的下降,从而影响系统能够处理的最大并发连接数。

根据什么原则来划分请求的阶段呢?一般是找到请求处理流程中的阻塞方法(或者造成阻塞的代码段),在阻塞代码段上按照下面4种方式来划分阶段:

(1)将阻塞进程的方法按照相关的触发事件分解为两个阶段

一个本身可以能导致进程休眠的方法或系统调用,一般都能够分解为多个更小的方法或者系统调用,这些调用间可以通过事件触发关联起来。大部分情况下,一个阻塞进程的方法调用时可以划分为两个阶段:阻塞方法改为非阻塞方法调用,这个调用非阻塞方法并将进程归还给事件分发器的阶段是第一阶段;增加新的处理阶段(第二阶段)用于处理非阻塞方法最终返回的结果,这里的结果返回事件就是第二阶段的触发事件。

例如,在使用send调用发送数据给用户时,如果使用阻塞socket句柄,那么send调用在向操作系统内核发出数据包后就必须把当前进程休眠,直到成功发出数据才能“醒来”。这时的send调用发送数据并等待结果。我们需要把send调用分解为两个阶段:发送且不等待结果阶段、send结果返回阶段。因此,可以使用非阻塞socket句柄,这样调用send发送数据后,进程是不会进入休眠的,这就是发送且不等待结果阶段;再把socket句柄加入到事件收集器中就可以等待相应的事件触发下一个阶段,send发送的数据被对方收到后这个事件就会触发send结果返回阶段。这个send调用就是请求的划分阶段点。

使用中断驱动模式时就不自觉地使用了这种划分,设置外设的代码是一个阶段(发起请求的阶段),中断服务函数是另一个阶段(处理结果的阶段),而且中断服务函数确实是事件(中断是事件的一种特殊形式,硬件产生的事件通常称为中断)触发的。

(2)将阻塞方法调用按照时间分解为多个阶段的方法调用

注意,系统中的事件收集、分发者并非可以处理任何事件。如果按照前一种方式试图划分某个方法时,那么可能会发现找出的触发事件不能够被事件收集、分发器所处理,这时只能按照执行时间来拆分这个方法了。

例如读取文件的调用(非异步I/O),如果我们读取10MB的文件,这些文件在磁盘中的块未必是连续的,这意味着当这10MB文件内容不在操作系统的缓存中时,可能需要多次驱动硬盘寻址。在寻址过程中,进程多半会休眠或者等待。我们可能会希望像上文所说的那样把读取文件调用分解成两个阶段:发送读取命令且不等待结果阶段、读取结果返回阶段。这样当然很好,可惜的是,如果我们的事件收集、分发者不支持这么做,该怎么办?例如,在Linux上Nginx的事件模块在没打开异步I/O时就不支持这种方法,像ngx_epoll_module模块只要是针对网络事件的,而主机的磁盘事件目前还不支持(必须通过内核异步I/O)。这时,我们可以这样来分解读取文件调用:把10MB均分成1000份,每次只读取10KB。这样,读取10KB的时间就是可控的,意味着这个事件接受器占用进程的时间不会太久,整个系统可以及时地处理其他请求。

那么,在读取0KB~10KB的阶段完成后,怎样进入10KB~20KB阶段呢?这有很多种方式,如读取完10KB文件后,可能需要使用网络来发送它们,这时可以由网络事件来触发。或者,如果没有网络事件,也可以设置一个简单的定时器,在某个时间点后再次调用下一个阶段。

在单线程编程模型中,将长时任务分割成多个短时任务,这可以避免执行长时任务时饿死其他任务的现象。这个方法在《The Definitive Guide to ARM® Cortex®-M0 and Cortex-M0+ Processors》3.2.5 Introduction to Embedded Software Program Flows >> Handling Concurrent Processes 小节中也有论述。

(3)在“无所事事”且必须等待系统的响应,从而导致进程空转时,使用定时器划分阶段

有时阻塞的代码段可能是这样的:进行某个无阻塞的系统调用后,必须通过持续的检查标志位来确定是否继续向下执行,当标志位没有获得满足时就循环地检查下去。这样的代码段本身没有阻塞方法调用,可实际上是阻塞进程的。这时,应该使用定时器来代替循环检查标志,这样定时器事件发生时就会先检查标志,如果标志位不满足,就立刻归还进程控制权,同时继续加入期望的下一次定时器事件。

在嵌入式软件中,这种情况可以对应繁忙等待轮询,繁忙等待轮询的过程就是“无所事事”的过程,可以使用定时轮询替代繁忙等待轮询。我碰到的一个只能轮询的情况是:写数据到SD卡。数据传输完毕后要不断地读取SD卡状态寄存器看是否写入完成。对于没有中断线的外设,例如SD卡、USB设备等,都是只支持轮询的。

(4)如果阻塞方法完全无法继续划分,则必须使用独立的进程执行这个阻塞方法

如果某个方法调用时可能导致进程休眠,或者占用进程时间过长,可是又无法将该方法分解为不阻塞的方法,那么这种情况是与事件驱动架构相违背的。通常是由于这个方法的实现者没有开发非阻塞接口所导致,这时必须通过产生新的进程或者指定某个非事件分发者进程来执行阻塞方法,并在阻塞方法执行完毕时向事件收集、分发器进程发送事件通知继续执行。因此,至少要拆分为两个阶段:阻塞方法执行前阶段、阻塞方法执行后阶段,而阻塞方法的执行要使用单独的进程去调用,并在方法返回后发送事件通知。一旦出现上面这种设计,我们必须审视这样的事件消费者是否足够合理,有没有必要用这种违反事件驱动架构的方式来解决阻塞问题。

当然也可以由新创建的线程来执行阻塞的操作,甚至可以创建专门的线程池来处理阻塞操作。

请求的多阶段异步处理将会提高网络性能、降低请求的时延,在与事件驱动架构配合工作后,可以使得Web服务器同时处理十万甚至百万级别的并发连接,我们在开发Nginx模块时必须遵循这一原则。

对于嵌入式软件而言,多阶段异步处理机制的引入,使用单线程就可以完成原来需要多线程才能完成的工作,减少线程就可以减少线程堆栈空间的使用,进而减少RAM的使用,甚至可以完全摒弃RTOS。另一方面,异步处理机制并发处理多个任务,使得CPU有很好的执行效率,可以在更短的时间内完成任务,这可以节省电力,在低功耗应用场合,尽快完成任务是非常关键的。

嵌入式软件异步编程:请求的多阶段异步处理相关推荐

  1. 【转】1.7异步编程:基于事件的异步编程模式(EAP)

    传送门:异步编程系列目录-- 上一篇,我给大家介绍了".NET1.0 IAsyncResult异步编程模型(APM)",通过Begin*** 开启操作并返回IAsyncResult ...

  2. rust异步编程--理解并发/多线程/回调/异步/future/promise/async/await/tokio

    1. 异步编程简介 通常我们将消息通信分成同步和异步两种: 同步就是消息的发送方要等待消息返回才能继续处理其它事情 异步就是消息的发送方不需要等待消息返回就可以处理其它事情 很显然异步允许我们同时做更 ...

  3. 深入理解python异步编程_深入理解Python异步编程

    1 什么是异步编程 1.1 阻塞程序未得到所需计算资源时被挂起的状态. 程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的. 常见的阻塞形式有:网络I/O阻塞.磁盘I/O ...

  4. python3异步编程_协程 Python异步编程(asyncio)

    协程(Coroutine) 也可以被称为微线程,是一种用户态内的上下文切换技术.简而言之,其实就是通过一个线程实现代码块相互切换执行. 直接上代码,例如: 同步编程 import time def f ...

  5. C#异步编程(一):异步基础

    class Program{static void Main(string[] args){var result = DownloadStringWithTimeout("http://ww ...

  6. 认识Java异步编程

    一 .认识异步编程 通常Java开发人员喜欢使用同步代码编写程序,因为这种请求(request)/响应(response)的方式比较简单,并且比较符合编程人员的思维习惯;这种做法很好,直到系统出现性能 ...

  7. 《Java8实战》读书笔记10:组合式异步编程 CompletableFuture

    <Java8实战>读书笔记10:组合式异步编程 CompletableFuture 第11章 CompletableFuture:组合式异步编程 11.1 Future 接口 (只是个引子 ...

  8. 深入理解Python异步编程

    声明:本文为转载内容 前言 很多朋友对异步编程都处于"听说很强大"的认知状态.鲜有在生产项目中使用它.而使用它的同学,则大多数都停留在知道如何使用 Tornado.Twisted. ...

  9. 深入理解 Python 异步编程

    原文地址:点击打开链接 来源:阿驹(微信公号:驹说码事) 如有好文章投稿,请点击 → 这里了解详情 前言 很多朋友对异步编程都处于"听说很强大"的认知状态.鲜有在生产项目中使用它. ...

最新文章

  1. Cause:compileSdkVersion is not specified
  2. linux共享比windows好处,开源Linux虚拟化优势比Windows有何特点?
  3. MFC遍历窗体所有的控件
  4. 开篇词丨这样学Redis,才能技高一筹
  5. 年化利息100%,现金贷监管寒冬将至
  6. centos7挂载nas存储_CentOS7搭建NAS文件共享存储
  7. 移动硬盘无法弹出的问题
  8. 在集群环境中使用 EhCache 缓存系统|RMI 集群模式
  9. 10.1综合强化刷题 Day1 morning
  10. 谈谈物联网产业4G转型中的隐忧与应对
  11. Pycharm快捷键设置(鼠标滚动控制字体大小)
  12. 8000401a错误解决方式(Excel)
  13. Android的消息机制 Handler、MessageQueue、ThreadLocal、Looper
  14. python wilcoxon test_自动化框架之 python+selenium+pytest · TesterHome
  15. “DOTA“巨魔战将连续晕眩的概率
  16. java基础知识粗略整理
  17. java filer,java – Filer的原始元素是否有用?
  18. 记一次拿webshell踩过的坑(如何利用PHP编写一个不包含数字和字母的后门)
  19. 快速EDAS字体嵌入问题
  20. android之NFC基础技术分享

热门文章

  1. 使用Audacity对清浊音进行频谱分析
  2. Matlab——向量及其运算
  3. ICC 图文学习——LAB3:Placement 布局
  4. Win10启用Linux子系统安装Ubuntu
  5. php农历生日计算,php实现的农历算法实例
  6. cocos2d-x 禁用触摸
  7. 信息安全工程师(软考中级)
  8. python+selenium+pycharm安装
  9. html中保留空格及换行
  10. 创新设计思维——做出好产品的艺术