nodejs异步IO的实现
nodejs的核心之一就是非阻塞的异步IO,于是想知道它是怎么实现的,挖了下nodejs源码,找到些答案,在此跟大家分享下。首先,我用了一段js代码test-fs-read.js做测试,代码如下:?
这段代码的异步IO操作就在fs.read的调用上,读取的experiment.log是一个12M的文本文件,所谓的异步,大家大概能想得到运行时会先打印?
[main thread] execute operation after read?
然后打印回调函数中的?
[main thread] execute read callback?
但大家也许还听说过,nodejs是单线程的,那又是怎么实现异步IO的呢?读文件操作是在哪里执行的呢?读完又是怎么调用回调函数的呢?猜想读文件可能是在另一个线程中完成的,读完后通过事件通知nodejs执行回调。为了一探究竟,debug了一把nodejs和libeio源码,重新编译后,运行测试代码node test-fs-read.js,输出如下:?
?
可以看到,nodejs的IO操作是通过调用libeio库完成的,debug从fs.read开始,js代码经过v8编译后,fs.read会调用node_file.cc中的Read方法,测试代码的运行经历了以下步骤:?
1 node_file.cc中的Read方法调用libeio(eio.c)的eio_read, read请求被放入请求队列req_queue中。?
2 主线程创建了1个eio线程,此时主线程的read调用返回。?
3 eio线程从req_queue中取出1个请求,开始执行read IO?
4 主线程继续执行read调用后的其它操作。?
5 主线程poll eio,从响应队列res_queue取已经完成的请求,此时res_queue为空,主线程stop poll?
6 eio线程完成了read IO,read请求被放入响应队列res_queue中,并且向主线程发送libev事件want_poll(通过主线程初始化eio时提供的回调函数)。?
7 eio线程从req_queue中取下一个请求,此时已经没有请求。?
8 主线程响应want_poll事件,从res_queue中取出1个请求,取出请求后res_queue变为空,主线程发送done_poll事件。?
9 主线程执行请求的callback函数。?
还需要说明的是,当同时有多个IO请求时,主线程会创建多个eio线程,以提高IO请求的处理速度。?
为了更清晰的看到nodejs的IO执行过程,图示如下,序号仅用来标示流程,与上述步骤序号并无对应关系。?
?
最后总结几条,不当之处还请大家指正。?
1 nodejs通过libev事件得到IO执行状态,而不是轮询,提高了CPU利用率。?
2 虽然nodejs是单线程的,但它的IO操作是多线程的,多个IO请求会创建多个libeio线程(最多4个),使通常情况的IO操作性能得到提高。?
3 但是当IO操作情况比较复杂的时候,有可能造成线程竞争状态,导致IO性能降低;而且libeio最多创建4个线程,当同时有大量IO请求时,实际性能有待测量。另外,由于每个IO请求对应一个libeio的数据结构,当同时有大量IO操作驻留在系统中时候,会增加内存开销。?
4 Libeio为了实现异步IO功能,带来了额外的管理,当IO数据量比较小的时候,整体性能不一定比同步IO好。
标签:?原创文章changlin?在 2011-1-30 18:12发布changlin?在 1-19 12:04重新编辑??分享到 weibo
var path = require('path'),
fs = require('fs'),
filepath = path.join(__dirname, 'experiment.log'),
fd = fs.openSync(filepath, 'r');
fs.read(fd, 12*1024*1024, 0, 'utf-8', function(err, str, bytesRead) {
console.log('[main thread] execute read callback');
});
console.log('[main thread] execute operation after read');
这段代码的异步IO操作就在fs.read的调用上,读取的experiment.log是一个12M的文本文件,所谓的异步,大家大概能想得到运行时会先打印?
[main thread] execute operation after read?
然后打印回调函数中的?
[main thread] execute read callback?
但大家也许还听说过,nodejs是单线程的,那又是怎么实现异步IO的呢?读文件操作是在哪里执行的呢?读完又是怎么调用回调函数的呢?猜想读文件可能是在另一个线程中完成的,读完后通过事件通知nodejs执行回调。为了一探究竟,debug了一把nodejs和libeio源码,重新编译后,运行测试代码node test-fs-read.js,输出如下:?
?
可以看到,nodejs的IO操作是通过调用libeio库完成的,debug从fs.read开始,js代码经过v8编译后,fs.read会调用node_file.cc中的Read方法,测试代码的运行经历了以下步骤:?
1 node_file.cc中的Read方法调用libeio(eio.c)的eio_read, read请求被放入请求队列req_queue中。?
2 主线程创建了1个eio线程,此时主线程的read调用返回。?
3 eio线程从req_queue中取出1个请求,开始执行read IO?
4 主线程继续执行read调用后的其它操作。?
5 主线程poll eio,从响应队列res_queue取已经完成的请求,此时res_queue为空,主线程stop poll?
6 eio线程完成了read IO,read请求被放入响应队列res_queue中,并且向主线程发送libev事件want_poll(通过主线程初始化eio时提供的回调函数)。?
7 eio线程从req_queue中取下一个请求,此时已经没有请求。?
8 主线程响应want_poll事件,从res_queue中取出1个请求,取出请求后res_queue变为空,主线程发送done_poll事件。?
9 主线程执行请求的callback函数。?
还需要说明的是,当同时有多个IO请求时,主线程会创建多个eio线程,以提高IO请求的处理速度。?
为了更清晰的看到nodejs的IO执行过程,图示如下,序号仅用来标示流程,与上述步骤序号并无对应关系。?
?
最后总结几条,不当之处还请大家指正。?
1 nodejs通过libev事件得到IO执行状态,而不是轮询,提高了CPU利用率。?
2 虽然nodejs是单线程的,但它的IO操作是多线程的,多个IO请求会创建多个libeio线程(最多4个),使通常情况的IO操作性能得到提高。?
3 但是当IO操作情况比较复杂的时候,有可能造成线程竞争状态,导致IO性能降低;而且libeio最多创建4个线程,当同时有大量IO请求时,实际性能有待测量。另外,由于每个IO请求对应一个libeio的数据结构,当同时有大量IO操作驻留在系统中时候,会增加内存开销。?
4 Libeio为了实现异步IO功能,带来了额外的管理,当IO数据量比较小的时候,整体性能不一定比同步IO好。
13 回复
#1 ?在 2011-1-31 08:32回复
绝对重量级的文章!
#2 ?在 2011-1-31 11:38回复
好文!必须精读
#3 ?在 2011-2-1 14:12回复
看了这篇文章很受启发。因此去了解了一下node.js异步文件操作的情况,补充几点:?
1、node使用libeio实现非阻塞异步文件操作,但实际上libeio是通过多线程的方式,在标准的阻塞式IO上模拟非阻塞异步。?
2、为什么node没有使用原生态的异步IO API(AIO)?原因可能是为了实现跨平台的兼容性(主要针对Windows,具体参见:http://groups.google.com/group/nodejs/browse_thread/thread/aff97d25c59f6f2d)?
3、libeio的4个线程限制是默认配置,可以对此配置进行修改
1、node使用libeio实现非阻塞异步文件操作,但实际上libeio是通过多线程的方式,在标准的阻塞式IO上模拟非阻塞异步。?
2、为什么node没有使用原生态的异步IO API(AIO)?原因可能是为了实现跨平台的兼容性(主要针对Windows,具体参见:http://groups.google.com/group/nodejs/browse_thread/thread/aff97d25c59f6f2d)?
3、libeio的4个线程限制是默认配置,可以对此配置进行修改
#4 ?在 2011-2-1 15:29回复
4个线程如何保证io密集型的应用的高效,为什么缺省是4个线程?
#5 ?在 2011-2-1 18:15回复
我猜测,由于磁盘的物理操作一般情况是串行的,除非多个物理磁盘,或者考虑cache 的情况,所以并发线程太多对性能提升帮助不大
#6 ?在 2011-2-2 22:52回复
多个eio线程完成自己的任务,去req_queue中请求新的任务的时候,可能会发生请求冲突。猜测req_queue上应该有锁吧,来保证请求不会出现问题。不过这样的话,这个锁可能会在出现大量io请求的时候成为性能瓶颈吧。
#7 ?在 2011-2-6 18:17回复
确实有线程锁
#8 ?在 2011-3-11 12:30回复
Well Done.?
对于磁盘IO密集性的可能多个执行线程反而会OS系统层面的IO dispatch造成干扰,但是对于网络IO多个线程的效果会很显著,主线程执行callback也是有利有弊,某些情况下让执行线程执行会更好或者有单独的执行线程,实现上可能会稍复杂。
对于磁盘IO密集性的可能多个执行线程反而会OS系统层面的IO dispatch造成干扰,但是对于网络IO多个线程的效果会很显著,主线程执行callback也是有利有弊,某些情况下让执行线程执行会更好或者有单独的执行线程,实现上可能会稍复杂。
#9{2} ?在 2011-5-15 15:54回复
如果是磁盘IO密集型应用是否适合用异步IO?举个例子,在IO调度队列里有多个任务,操作系统调度时,是串行的执行任务(频繁移动磁头),还是先计算任务位置,尽量减少磁盘移动的距离?
在 2011-5-15 23:12回复
?Linux本身自带了好几种IO调度算法,每种算法都有自己的特性并且适合于一定类型的应用,但本质就是尽可能降低磁头移动或磁盘操作,同时兼顾IO响应时间。虽然具体的磁盘操作算法被OS层屏蔽掉了,应用的IO请求方式也会影响性能,比如第一次请求地址1的数据,第二次请求地址10000的数据,这样交替请求的话,系统的性能就会下降。
在 2011-5-16 10:01回复
?我在另外一篇文章《异步IO一定更好吗?》里对这个问题做了分析。但是昨天在NodeParty杭州站上,jscex的作者@老赵 也提到过你这个问题。但是他一笔带过,貌似观点是OS会做优化,我不敢苟同,打算测一测,明天给出结论。请关注?
http://cnodejs.org/blog/?p=611
http://cnodejs.org/blog/?p=611
#10 ?在 2011-6-13 19:24回复
OS层优化是必然的,不然要IO调度干嘛呢。但对于SSD,应该不存在这种问题
#11 ?在 2011-6-13 21:19回复
SSD也有他自己的问题,如果你频繁写,就要频繁擦除SSD的一块,导致性能下降。 Linux系统中数据存储涉及到的是一系列的子系统的相互协作,比如你要先优化驱动参数,然后是磁盘参数,文件系统参数,最后才是应用层的优化。
#12 ?在 2011-6-30 19:55回复
之所以默认4个线程,应该是因为一般机器的cpu 数目 为 2~4个,?
这意味着同时可以同时运行4个线程而不切换,没有引发context调度?
这其实是最适合的场景 ,微软的IOCP 也采用了类似的算法,线程池的?
容量设为 2 倍 cpu 的数目 ...?
这算是 多线程 + 异步通知 同样 会导致context 切换的问题?
和传统的 多线程 + 同步阻塞 线程切换调度 性能上基本一致吧?
这意味着同时可以同时运行4个线程而不切换,没有引发context调度?
这其实是最适合的场景 ,微软的IOCP 也采用了类似的算法,线程池的?
容量设为 2 倍 cpu 的数目 ...?
这算是 多线程 + 异步通知 同样 会导致context 切换的问题?
和传统的 多线程 + 同步阻塞 线程切换调度 性能上基本一致吧?
#13 ?在 2011-9-7 12:01回复
[...] (这个文章的流程图还是蛮靠谱的:http://cnodejs.org/blog/?p=244 ?,此处更详细的补充一下下) [...]
回到顶部
关于???|???FAQ???|???Github???|???RSS
? 2012?
本社区为开源系统,版本: 0.2.1 ,欢迎贡献代码
由?NAE?为 CNode 提供动力
本社区为开源系统,版本: 0.2.1 ,欢迎贡献代码
由?NAE?为 CNode 提供动力