Nodejs 系列三:异步模型(Asynchronous Mode)之我见

前言

本文是笔者所总结的有关 Nodejs 基础系列之一,

本来打算直接对 Nodejs 的事件机制既 Event 和 EventEmitter 进行剖析,但是发现,如果没有对异步模型在整体上有着一个清晰和直观的认识,便直接对底层的东西进行分析,那就像瞎子摸象,只能知道局部;于是在深入 Nodejs 各个模块之前,笔者打算对异步模型做一个顶层和全盘的分析以后,再逐步的的深入到各个细节当中去;

注明:本文为作者的原创作品,转载需注明出处;

异步模型之我见

我眼中的异步模型

首先,要知道目前的服务器主要有两个派别,一类是科学计算性、一类是 I/O 优化型;有什么区别呢?

  • 科学计算型的服务器主要以消耗 CPU 和内存资源为主,基本上不发生 I/O 读写;所以,这类服务器主要以优化 CPU 计算能力为主,在单位时间内可以执行更多次的计算操作;要特别注意的是,要使得这种类型的服务器的性能发挥至极致,则要求进程尽可能的不要发生同步操作,因为同步操作往往伴随着 CPU 中断,因此则要求无论是进程还是线程都是科学计算型的进程;
  • I/O 优化型的服务器主要以消耗存储资源为主,这里的存储资源主要指的就是磁盘;在没有 DMA 控制器和零拷贝( Zero Copy )技术以前,基本上都需要使用中断 CPU 的方式,使得 CPU 暂停当前的执行,进而处理文件的读写;所以,这里服务器主要以优化 I/O 设备的性能为主;适用于作为文件系统服务器,大型数据库服务器等;

有了上面的背景知识以后,我们再来看看,异步模型的处理流程是怎样的?来看笔者画的下面这张逻辑图,
my asynchronous design mode.png
里面主要分为两类 Request 请求,一类是计算型请求 cal Request,灰黑色方框表示,另一类请求是 I/O 请求(同时有可能兼计算型请求,比如图中的 _db_ Request),由红色方框表示;我们要知道,异步模型的终极目标是什么?它的终极目标就是让所有的处理过程都不发生中断,既是不让 CPU 停止,让它一直执行,以便使得 CPU 达到最大化的性能;可是这么多不同类型的请求,怎么能够保证不发生中断呢?是的,这个的确不容易做到,光靠软件优化是做不到的,必须结合硬件的改良,一起才能做到;

  • 要能够达成这个终极目标,必须想办法让 I/O 操作不对 CPU 发起任何的中断,这个重担就由硬件工程师来完成了,他们设计了 DMA 控制器,以及零拷贝技术完美的解决了 I/O 包括 Socket 流读写过程的中断 CPU 的问题,所以,如图,一旦是 cal 请求以外的其它 I/O 类型请求,将会交给 DMA 控制器执行,当 I/O 请求完毕以后,直接通过回调方式将 I/O 结果的内存地址直接写入 main process 既主进程中,这样就不需要任何 CPU 的中断操作了;
  • Ok,看似一切都可以异步了,无论 cal Request 还是 file Request 都可以异步的执行而无需中断 CPU,但是别忘了,还有 _db_ Request,特别是老牌的 Database,比如 MySQL、Orcale、DB2 等,里面存在着大量的同步操作,同步往往都是通过休眠和唤醒的操作来实现的,其实归根结蒂仍然是靠中断 CPU 来实现的,那该如何是好呢,怎么来处理它的请求呢?其实答案也很简单,把他们的同步请求统统转换成异步的 Socket 请求,这样,main process 就不会发生中断了,这种转换正好是 Nodejs 天生的强项;最常见的做法就是把 database 部署在另外一台 I/O 优化型的服务器上就可以了,main process 只需要和这台服务器通过异步 Socket 进行连接就可以了;这样自然,main process 所在的主机一定能够达到最大化的执行性能;

上述便是笔者眼中的异步模型;

Nodejs 所要达到的异步模式

Nodejs 的异步模型到底想要达成一个什么样的目标?它的野心十足,至少目前对比与其它类型的编程语言来看,它是走得最彻底的,它不仅仅想让 Web Server 变成异步模型,同时也想让 Application Server 也变成异步模型的方式;(备注,Web Server 等价于 Tomcat、Netty 等服务器,Application Server 等价于 Spring 容器或者 J2EE 容器等;)

为什么说 Nodejs 是走得最彻底的呢?因为它天生就是为异步而生的,它的编程语言的底层架构,从出生的那一天起,就是按照异步模型来设计的,因此,它是天生的异步模式的语言;如果说我们 80 后是互联网时代的移民者,那么 00 后就诞生在互联网时代,是互联网时代的真正主人;同样,Java、Python 等语言可以认为是异步模型的移民者,而 Nodejs 是异步模型天生的主人;这就是笔者所认为的 Nodejs 与其它语言最本质的区别!

服务器拓扑

在有了上述的分析以后,笔者胸有成竹的画出了下面的这张拓扑图,

asynchronous topological diagram.png

是的,只要这样部署你的服务器,恭喜你,你的服务器处理 Request 的性能就能达到最大化;(什么,你说汇.. 什么,没听清,再说一遍,哦,汇编,你说用汇编才能最大化,啊,是的,你说对了,CPU 少了无数次的将 Nodejs 转换成 C,再将 C 转换成汇编的过程;不过笔者这里所谓的最大化有个前提,那就是高级语言!)

大哥,别骗我,我见过的拓扑图和架构图比这个复杂多了,你这个算哪门子的拓扑图呢?这么简单?是啊,为什么又不是呢?技术难道就该复杂呀?在你彻底搞懂了它的逻辑以后,去掉边边角角,化繁为简以后,它的确就是这么简单,Nodejs 以异步的方式不断的接收 Request,然后将不必要可能同步的请求转换成异步的 Socket 请求,完了,就这么简单;

是的,清静了,至少笔者的内心清静了;为什么呢?想想 Java 的 Web Server 和 Application Server 你就明白了,你得花多大的力气将 Tomcat 改造成 NIO 或者 AIO 的方式,是的,你说配置一下就好了,那你去试试,看看有没有什么 Bug?当你把 Web Server 搞定以后,Applicaiton Server,是呀 Spring 容器和 J2EE 又该怎么搞呢?这么多的应用和组件都是以同步的方式设计的,难道你去把他们统统都改造成异步的方式… 这就是为什么那么多 Java 工程师和架构师一天到晚费尽心力干的一件事情就是,无止尽的调优,无止尽的堆积服务器,无止尽的架构图,复杂的让你眼花缭乱的架构图;

哎,终于从内心中了解到了做 Java 工程师或者 Java 架构师为什么会那么的苦了,么的苦了,的苦了,苦了,了 ~~~ 无止尽的苦 ~ (听闻某人读到这里以后,喷了,稳住,别喷 ~)

Java 的异步模型 NIO/AIO

浅谈一下 Java 的 NIO/AIO,

大象能否跳舞

大多数 Java 工程师和架构师都对 Nodejs 不屑一顾,Java 不也有 NIO/AIO 嘛,你的异步模型,Java 都有,也都可以有;而且他们为什么不愿意接受 Nodejs 的一个最主要原因(之前包括笔者在内),Java 作为一个老牌的语言,已经有那么多成熟的开源的“轮子”可用,而且又经过这么多年行业的检验,Nodejs 算哪门子老几,一个新鲜出炉的小鲜肉而已;

对,这也是之前笔者一直不愿意接受 Nodejs 的一个主要原因,认为 Nodejs 能干的事情,Java 也能干;但是为什么笔者又改变了呢?为什么又愿意花这么大的力气来对它进行研究呢?嗯~ 好像是,吃饱了没事干呐~~ 呵呵… 开个玩笑;平心而论,就事论事,其实正是因为 Java 的最大的优点,它有那么多成熟的“轮子”,这是它最大的优势,但同时,也是它最大的“弱势”,为什么呢?因为它的“轮子”基本上都是构建在“同步机制”之上的;这样说,不知道你明白了没有?

所以,大象能够跳舞,只是它太累了,背负了太多,太多的负担;如果只是两三个包裹到还好办,但是它背负的是数不清的大山;它已经跳不动了!

写在最后

温故中断

归纳起来,CPU 中断主要经历了那么两个时期,且都是为了解决 CPU 的计算效率和 I/O 设备输入输出效率之间时间差的问题,

  1. CPU 空转,等待 I/O 的反馈
    这个时期,CPU 的中断主要是靠程序自己实现的循环来不断的轮询 I/O 是否反馈,然后继续执行后续的程序逻辑;这个时期与其说是中断,实际上 CPU 是一直在工作,其它的进程都会被阻塞;
  2. 进程休眠,等待 I/O 反馈以后被唤醒
    这个时期的特征是,CPU 不再为了等待 I/O 而空转,转而是保护当前线程的现场,然后让当前的进程休眠,I/O 返回以后,唤醒之前沉睡中的进程,让它继续执行;看似整个过程都水到渠成,但是里面却隐含了巨大的隐患,那就是性能问题,想想,“睡眠”和“唤醒”,有那么容易实现吗?一个进程会有大量的上下文环境来支撑它来执行,也就注定了在这个过程中,在“睡眠”的时候要进行所谓的“保护现场”,其实也就是缓存当前进程的上下文环境,同样,在“唤醒”的时候需要进行所谓的“恢复现场”,其实也就是从内存中读取该进程的上下文环境;说了这么多,聪明的你应该可以感知着一进一出会消耗多少 CPU 资源了吧,何况,当某个进程被“唤醒”的同时,一定会让另一个进程“休眠”,又一次的保护现场;这就是我们常说的,传统编程语言下多线程会出现的频繁上下文切换,当并发量高了以后,就会导致 CPU 因此而效率急剧降低,因为它的大部分工作都是去进行上下文切换了;

实际上,#1 和 #2 所导致的 CPU 中断的罪魁祸首就是“同步”,想想某个进程必须等待 I/O 返回以后才能继续执行,那么没有办法,要么进程空转、要么让进程睡眠和唤醒;所以,只要能够解决应用程序“同步”操作,那么就能解决 CPU 中断的问题;这也就是 Nodejs 这么语言从诞生之日起所背负的使命;

慎用 Nodejs

最后,笔者还是得提一下,Nodejs 最大的优势同时也是它最大的劣势,它最大的优势就是它非常的新,很多东西都可以重头来过,但也正是因为它的新,导致很多“轮子”或许还没有,或许有但还不够成熟;所以,如果你当前的系统不需要非常高的并发,同时对事务完整性和强一致性特别高,比如说金融领域,笔者还是推荐你使用传统的 J2EE 架构;不过这个建议是有时效的,当 Nodejs 变得成熟以后,自然便可以替代 J2EE 的地位了;所以,笔者有必要注明一下提出这个论点的时间点是 2018-02-26;