续:Nodejs异步IO一定更好吗?

续:Nodejs异步IO一定更好吗?

我之前的一篇文章《<a href=“http://cnodejs.org/blog/?p=611” target="_blank">异步IO一定更好吗?</a>》中举了一个很变态的例子,用以说明在单碟机械式硬盘上异步IO反而可能降低性能的问题,大家的讨论很热烈。前天的NodeParty杭州站分享会上,来自上海的@老赵 随口带出了这个问题,大概说是多个异步IO请求会使得性能提高,因为磁头走到哪里就处理到哪里,很多请求在“路上”就可以被处理掉了。 <br/> <br/>乍一看,我和老赵的结论是相反的。所以从nodeparty回来后查了查资料,并且做了一些实验,发现这两种情况在各自特定的场景里都可能是正确的,在别的场景又都可能是错误的。为什么呢? <br/> <br/>我之前的结论没有认真考虑操作系统对IO调度的优化,对此一知半解就拿出来大放厥词,实在不应该! <br/> <br/>Linux2.6内核对IO调度提供了四种算法:Completely Fair Queuing(CFQ)、Deadline、NOOP和Anticipatory(as),详细情况请点击<span style=“color: #ff0000”><strong><a href=“http://www.redhat.com/magazine/008jun05/features/schedulers/” target="_blank">这里</a></strong></span>阅读。在2.6内核中,系统默认的IO调度算法是CFQ,也就是OS会对请求的偏移量首先进行排序,按照这个排序顺序进行磁头的移动和请求响应,这正是老赵的观点的出处。而假如某个磁盘采用了NOOP的IO调度算法,也就是说内核不会对IO调度做任何优化,容易理解,正是我在《异步IO一定更好吗?》一问中的观点。除此之外,用户还可以为不同磁盘指定不同的IO调度算法。查看某个磁盘的IO调度算法的命令如下: <br/><blockquote>$[pengchun]$ cat /sys/block/sda/queue/scheduler <br/>noop anticipatory deadline [cfq]</blockquote> <br/><del datetime=“2011-05-17T05:36:41+00:00”>原文这里的测试方法不对,已删掉</del> <br/> <br/>总结我的几点经验: <br/> <br/>1. 异步IO不一定是性能最好的; <br/> <br/>2. 操作系统对磁盘IO的各种调度算法中,没有哪一个是绝对更好的;随着应用场景的不同,很可能某一个算法的性能远远好于其他几个;而在另外的场景中则相反。例如,某个磁盘专门用来写日志的,那么你用NOOP就最好了;而如果要作为数据库的磁盘来使用,deadline可能是最好的算法了; <br/> <br/>3. 尽管操作系统会做这样那样的优化,但不要懒惰地依赖操作系统;对机械式磁盘的访问,尽可能把随机访问变成顺序访问。


6 回复

续:Nodejs异步IO一定更好吗?

在之前的文章《异步IO一定更好吗?》中,我提到了一个极端的例子,展示了在单碟机械式硬盘上,异步IO可能会降低性能的问题。而在最近的NodeParty杭州站分享会上,@老赵提到多个异步IO请求可以提高性能,因为磁头可以在移动过程中处理多个请求。

乍一看,我和老赵的结论似乎相矛盾。然而,经过进一步的研究和实验,我发现这两种观点在各自的场景下都是正确的,但在其他场景下也可能是错误的。这背后的原因是什么呢?

操作系统对IO调度的影响

Linux 2.6内核提供了四种主要的IO调度算法:Completely Fair Queuing (CFQ)、Deadline、NOOP和Anticipatory。每种算法都有其适用的场景。默认情况下,大多数Linux系统使用CFQ算法,它会对请求的偏移量进行排序,从而优化磁头的移动顺序。而NOOP算法则不对请求进行排序,只是简单地按照接收到的顺序执行请求。

例如,如果你有一个专门用于写日志的磁盘,使用NOOP算法可能更合适,因为它能够更快地处理写操作。而对于需要频繁读取数据的数据库磁盘,Deadline算法可能更为适合,因为它可以确保较长时间未完成的请求优先得到处理。

示例代码

为了更好地理解这些概念,我们可以通过一个简单的Node.js示例来演示不同IO调度算法对性能的影响。假设我们有一个文件系统,我们希望对其进行一系列的读取操作:

const fs = require('fs');
const path = require('path');

// 获取当前磁盘的IO调度算法
function getScheduler(device) {
    return new Promise((resolve, reject) => {
        fs.readFile(`/sys/block/${device}/queue/scheduler`, 'utf8', (err, data) => {
            if (err) return reject(err);
            resolve(data.split('[')[1].split(']')[0]);
        });
    });
}

// 执行一系列异步读取操作
async function performReads(device) {
    const scheduler = await getScheduler(device);
    console.log(`当前IO调度算法:${scheduler}`);
    
    // 创建多个读取操作
    const reads = Array.from({ length: 10 }, (_, i) => 
        fs.promises.readFile(path.join('/tmp', `file${i}.txt`), 'utf8')
    );
    
    // 并发执行所有读取操作
    const results = await Promise.all(reads);
    console.log(results);
}

performReads('sda').catch(console.error);

在这个示例中,我们首先获取当前磁盘的IO调度算法,然后并发执行一系列文件读取操作。通过这种方式,我们可以观察到不同IO调度算法对性能的具体影响。

结论

总结一下,异步IO并不总是性能最优的选择,具体取决于应用场景和操作系统对IO调度的优化策略。因此,我们在设计系统时,不应盲目依赖异步IO或某种特定的IO调度算法,而是应该根据实际情况选择最合适的方案。此外,尽可能将随机访问转化为顺序访问,也是提升磁盘性能的一个重要手段。


对这个测试有点疑问:lseek和fread都是系统调用,磁盘调度应该在此之下,不同的调度算法不应该改变lseek和fread的调用顺序。而且程序是同步调用,调用未完成前不会继续执行后边的调用,操作系统无法聪明的将还没有被执行的调用进行调度。 <br/> <br/>因此怀疑是php自己做的优化,查阅了一下php的源代码,发现在php-5.3.6\main\streams\streams.c 中的_php_stream_read函数实现中确实有buffer,而php的fread是通过调用_php_stream_read来实现的。 <br/> <br/>建议换成C语言再实现一下同样的逻辑,看看结果是否有所不同。

看到ls的回复,期待一篇"操作系统与编译器/解释器中的IO调度算法分析"

是的,我的测试方法搞错了,先删掉。IO调度应该是lseek里的实现,怎么去验证这个谁能支点招?

异步IO请求会使得性能提高,因为磁头走到哪里就处理到哪里,很多请求在“路上”就可以被处理掉了。看来异步还能保护硬盘哦。

关于“Node.js 异步IO一定更好吗?”这一问题,答案取决于具体的应用场景和环境。尽管Node.js的设计理念之一就是通过非阻塞I/O模型来提高并发性能,但在某些特定情况下,同步I/O或不同的I/O调度算法可能会更合适。

示例代码

假设我们有一个简单的文件读取操作,分别使用同步和异步的方式来实现:

// 异步读取文件
const fs = require('fs');

console.log('开始异步读取文件...');
fs.readFile('./data.txt', 'utf8', (err, data) => {
    if (err) throw err;
    console.log(`异步读取文件内容:${data}`);
});

console.log('异步读取文件完成。');

// 同步读取文件
console.log('开始同步读取文件...');
try {
    const data = fs.readFileSync('./data.txt', 'utf8');
    console.log(`同步读取文件内容:${data}`);
} catch (err) {
    throw err;
}
console.log('同步读取文件完成。');

在这个例子中,fs.readFile 是异步调用,fs.readFileSync 是同步调用。异步调用不会阻塞主线程,适合高并发场景,而同步调用则会阻塞主线程,适合简单场景。

总结经验

  1. 异步I/O并不总是最佳选择:对于简单的任务或小文件,同步I/O可能更简单易用且不易出错。
  2. 操作系统I/O调度算法:根据应用场景选择合适的I/O调度算法。例如,对于频繁写入的操作,使用 NOOP 可能更高效;而对于数据库应用,deadline 可能更适合。
  3. 减少随机访问:尽管操作系统可以优化磁盘I/O调度,但尽可能将随机访问转换为顺序访问可以显著提高性能。

因此,选择同步还是异步I/O,以及选择哪种I/O调度算法,应基于具体的应用场景和性能需求。

回到顶部