Node.js 8.0.0 async pattern benchmark

Node.js 的 8.0 版本终于发布了,等了两天没人测评,于是决定抛砖引玉。
首先需要说明的是,由于不方便拿业务代码测试来评测,所以本次基准测试(benchmark)毫无新意的沿用了 bluebird 的基准测试。测试比较的的内容是多个异步模式(async pattern)之间的性能对比。

目前 Node.js 业内比较异步模式性能比较靠谱的方式是基于 Gorki Kosev’s 的 《Analysis of generators and other async patterns in node》 的 benchmark。该 benchmark 模拟会模拟同时混合大量同步与异步操作的处理场景。具体指标是处理这些操作消耗的时间(ms)以及内存(MB)。

直接上一个 Node.js v8.0.0 的测试报告:

results for 10000 parallel executions, 1 ms per I/O op

file                                       time(ms)  memory(MB)
callbacks-baseline.js                           328       24.11
callbacks-suguru03-neo-async-waterfall.js       409       35.66
promises-bluebird-generator.js                  703       40.76
promises-bluebird.js                            781       51.74
promises-then-promise.js                        922       67.02
promises-cujojs-when.js                         963       65.11
promises-lvivski-davy.js                       1071       96.27
promises-tildeio-rsvp.js                       1159       87.05
callbacks-caolan-async-waterfall.js            1345      102.79
promises-dfilatov-vow.js                       1504      131.96
promises-calvinmetcalf-lie.js                  1870      164.99
promises-ecmascript6-native.js                 2167      167.91
promises-obvious-kew.js                        2269      225.37
generators-tj-co.js                            2273      119.98
observables-pozadi-kefir.js                    3170      188.55
promises-medikoo-deferred.js                   3202      114.25
observables-Reactive-Extensions-RxJS.js        4622      238.07
streamline-generators.js                       6096      128.54
promises-kriskowal-q.js                       12051      391.93
observables-caolan-highland.js                12269      534.94
streamline-callbacks.js                       62359      198.94
observables-baconjs-bacon.js.js                 OOM         OOM

Platform info:
Linux 2.6.32-042stab117.14 x64
Node.JS 7.7.4
V8 5.5.372.42
Intel(R) Xeon(R) CPU           L5640  @ 2.27GHz × 2

其中的测试项中, 比较常见的详情分别是:

执行速度最快的还是原生的 callback

在内存占用方面可以看到 native 的 promise 较之前的版本有明显的下降,已经达到 async 模块的水平了。

完整执行数据 (ms)

pattern 6-1 6-2 7-1 7-2 8-1 8-2
callbacks-baseline.js 335 348 345 328 357 370
callbacks-suguru03-neo-async-waterfall.js 426 433 428 409 462 428
promises-bluebird.js 829 892 750 781 779 770
promises-bluebird-generator.js 838 973 707 703 782 789
promises-cujojs-when.js 916 961 978 963 977 965
promises-then-promise.js 827 913 940 922 987 1102
promises-lvivski-davy.js 1014 1149 1035 1071 1131 1142
promises-tildeio-rsvp.js 1085 1124 1189 1159 1186 1142
promises-ecmascript6-native.js 2530 2540 2286 2167 1216 1350
callbacks-caolan-async-waterfall.js 1317 1312 1427 1345 1433 1430
generators-tj-co.js 2753 2642 2267 2273 1616 1671
promises-dfilatov-vow.js 1382 1496 1665 1504 1688 1770
promises-calvinmetcalf-lie.js 1650 1795 1784 1870 2031 2177
promises-obvious-kew.js 2326 2421 2173 2269 2610 2667
promises-medikoo-deferred.js 4231 4416 3477 3202 3227 3048
observables-pozadi-kefir.js 60466 64587 3177 3170 3442 3161
streamline-generators.js 5533 6048 6682 6096 3013 3228
streamline-callbacks.js 8340 8784 79046 62359 4360 4339
observables-Reactive-Extensions-RxJS.js 4488 4947 4514 4622 5614 5578
observables-caolan-highland.js 152903 164368 12903 12269 17796 17243
promises-kriskowal-q.js 14618 15963 13198 12051 21159 19219
observables-baconjs-bacon.js.js 37014 33282 45257

完整内存数据 (MB)

pattern 6-1 6-2 7-1 7-2 8-1 8-2
callbacks-baseline.js 30 30 24 24 31 31
callbacks-suguru03-neo-async-waterfall.js 43 43 35 35 39 39
promises-bluebird-generator.js 39 40 40 40 41 43
promises-bluebird.js 49 49 50 51 49 48
promises-cujojs-when.js 59 60 65 65 61 60
promises-then-promise.js 59 59 68 67 73 74
generators-tj-co.js 131 131 119 119 74 75
promises-tildeio-rsvp.js 89 89 88 87 80 82
promises-lvivski-davy.js 91 90 97 96 103 102
streamline-generators.js 179 179 129 128 102 102
promises-ecmascript6-native.js 187 187 163 167 95 104
callbacks-caolan-async-waterfall.js 101 101 102 102 108 108
streamline-callbacks.js 163 251 199 198 127 126
promises-dfilatov-vow.js 130 147 132 131 153 159
promises-calvinmetcalf-lie.js 152 135 164 164 147 166
promises-medikoo-deferred.js 190 190 114 114 183 183
observables-pozadi-kefir.js 146 152 188 188 202 202
promises-obvious-kew.js 217 216 79 225 228 232
observables-Reactive-Extensions-RxJS.js 229 230 236 238 243 244
promises-kriskowal-q.js 837 848 370 391 387 388
observables-caolan-highland.js 485 492 517 534 585 559
observables-baconjs-bacon.js.js 826 833 582

小结

  • neo 版的 async 依旧是最快的异步解决方案。
  • 依赖 native 版本的实现(promise/co 等)都有一定程度的性能、内存提升(不过提升的不是很多)。
  • native 的 promise 在提升之后性能已与 async 模块相当,并且 bluebird 的实现与 native 版本差距进一步缩小,bluebird 已经没有成倍的优势。

PS:目前该 benchmark 尚未加入 async/await (笔者已经提了 issue,在考虑要不要自己撸),加入之后会更新本文。

http://echarts.min.js

Node.js 的 require.resolve 简介

简单的说, 在 Node.js 中使用 fs 读取文件的时候, 经常碰到要拼一个文件的绝对路径的问题 (fs 处理相对路径均以进程执行目录为准). 之前一直的方法都是, 使用 path 模块以及 __dirname 变量 :

fs.readFileSync(path.join(__dirname, './assets/some-file.txt'));

使用 require.resolve 可以简化这一过程:

fs.readFileSync(require.resolve('./assets/some-file.txt'));

此外, require.resolve 还会在拼接好路径之后检查该路径是否存在, 如果 resolve 的目标路径不存在, 就会抛出 Cannot find module './some-file.txt' 的异常. 省略了一道检查文件是否存在的工序 (fs.exists).

这个报错并不会加重你的检查负担, 毕竟使用 fs 去操作文件时, 如果发现文件不存在也会抛出异常. 反之, 通过 require.resovle 可以在提前在文件中作为常量定义, 那么在应用启动时就可以抛异常, 而不是等到具体操作文件的时候才抛异常.

Node.js Mongodb 密码特殊字符 @

在去年的 DB 勒索事件之后, 不少的同学开始加强 Mongodb 的安全性, 其中一种办法就是设置复杂的密码. 那么问题来了, 如果设置的密码里包含一些如 “@”, “:” 一样的特殊字符怎么办?

mongodb://username:password@host:port/db

这种情况可能使得你的 Mongodb 连接串不能被正常解析, 并且完全有可能出现. 烦人的地方在于:

  1. 使用 “” 双引号将 password 包起来没有用
  2. 使用 \@ 转义也没有用

解决方案 1

开启 uri_decode_auth 功能, 拼接连接串之后先 encode 一下, 然后通过 uri_decode_auth 在 driver 内部 decode 来绕过这个问题

mongoClient.connect("mongodb://username:p%40ssword@host:port/dbname", {
    uri_decode_auth: true
    }, function(err, db) {

    }
);

解决方案2

老老实实查文档, 在 options 中指明:

mongoose.connect('mongodb://localhost/test',
                 {user: 'username', pass: 'p@ssword'},
                 callback);

在 Node.js 中看 Javascript 的引用

本文首发于《程序员》杂志 2017 年第 3 期

早期学习 Node.js 的时候 (2011-2012),有挺多是从 PHP 转过来的,当时有部分人对于 Node.js 编辑完代码需要重启一下表示麻烦(PHP不需要这个过程),于是社区里的朋友就开始提倡使用 node-supervisor 这个模块来启动项目,可以编辑完代码之后自动重启。不过相对于 PHP 而言依旧不够方便,因为 Node.js 在重启以后,之前的上下文都丢失了。

虽然可以通过将 session 数据保存在数据库或者缓存中来减少重启过程中的数据丢失,不过如果是在生产的情况下,更新代码的重启间隙是没法处理请求的(PHP可以,另外那个时候 Node.js 还没有 cluster)。由于这方面的问题,加上本人是从 PHP 转到 Node.js 的,于是从那时开始思考,有没有办法可以在不重启的情况下热更新 Node.js 的代码。

最开始把目光瞄向了 require 这个模块。想法很简单,因为 Node.js 中引入一个模块都是通过 require 这个方法加载的。于是就开始思考 require 能不能在更新代码之后再次 require 一下。尝试如下:

a.js

var express = require('express');
var b = require('./b.js');

var app = express();

app.get('/', function (req, res) {
  b = require('./b.js');
  res.send(b.num);
});

app.listen(3000);

b.js

exports.num = 1024;

两个 JS 文件写好之后,从 a.js 启动,刷新页面会输出 b.js 中的 1024,然后修改 b.js 文件中导出的值,例如修改为 2048。再次刷新页面依旧是原本的 1024。

再次执行一次 require 并没有刷新代码。require 在执行的过程中加载完代码之后会把模块导出的数据放在 require.cache 中。require.cache 是一个 { } 对象,以模块的绝对路径为 key,该模块的详细数据为 value。于是便开始做如下尝试:

a.js

var path = require('path');
var express = require('express');
var b = require('./b.js');

var app = express();

app.get('/', function (req, res) {
  if (true) { // 检查文件是否修改
    flush();
  }
  res.send(b.num);
});

function flush() {
  delete require.cache[path.join(__dirname, './b.js')];
  b = require('./b.js');
}

app.listen(3000);

再次 require 之前,将 require 之上关于该模块的 cache 清理掉后,用之前的方法再次测试。结果发现,可以成功的刷新 b.js 的代码,输出新修改的值。

了解到这个点后,就想通过该原理实现一个无重启热更新版本的 node-supervisor。在封装模块的过程中,出于情怀的原因,考虑提供一个类似 PHP 中 include 的函数来代替 require 去引入一个模块。实际内部依旧是使用 require 去加载。以b.js为例,原本的写法改为 var b = include(‘./b’),在文件 b.js 更新之后 include 内部可以自动刷新,让外面拿到最新的代码。

但是实际的开发过程中,这样很快就碰到了问题。我们希望的代码可能是这样:

web.js

var include = require('./include');
var express = require('express');
var b = include('./b.js');
var app = express();

app.get('/', function (req, res) {
  res.send(b.num);
});

app.listen(3000);

但按照这个目标封装include的时候,我们发现了问题。无论我们在include.js内部中如何实现,都不能像开始那样拿到新的 b.num。

对比开始的代码,我们发现问题出在少了 b = xx。也就是说这样写才可以:

web.js

var include = require('./include');
var express = require('express');
var app = express();

app.get('/', function (req, res) {
  var b = include('./b.js');
  res.send(b.num);
});

app.listen(3000);

修改成这样,就可以保证每次能可以正确的刷新到最新的代码,并且不用重启实例了。读者有兴趣的可以研究这个include是怎么实现的,本文就不深入讨论了,因为这个技巧使用度不高,写起起来不是很优雅[1],反而这其中有一个更重要的问题——JavaScript的引用。

JavaScript 的引用与传统引用的区别

要讨论这个问题,我们首先要了解 JavaScript 的引用于其他语言中的一个区别,在 C++ 中引用可以直接修改外部的值:

#include <iostream>

using namespace std;

void test(int &p) // 引用传递
{
    p = 2048;
}

int main()
{
    int a = 1024;
    int &p = a; // 设置引用p指向a

    test(p); // 调用函数

    cout << "p: " << p << endl; // 2048
    cout << "a: " << a << endl; // 2048
    return 0;
}

而在 JavaScript 中:

var obj = { name: 'Alan' };

function test1(obj) {
  obj = { hello: 'world' }; // 试图修改外部obj
}

test1(obj);
console.log(obj); // { name: 'Alan' } // 并没有修改①

function test2(obj) {
  obj.name = 'world'; // 根据该对象修改其上的属性
}

test2(obj);
console.log(obj); // { name: 'world' } // 修改成功②

我们发现与 C++ 不同,根据上面代码 ① 可知 JavaScript 中并没有传递一个引用,而是拷贝了一个新的变量,即值传递。根据 ② 可知拷贝的这个变量是一个可以访问到对象属性的“引用”(与传统的 C++ 的引用不同,下文中提到的 JavaScript 的引用都是这种特别的引用)。这里需要总结一个绕口的结论:Javascript 中均是值传递,对象在传递的过程中是拷贝了一份新的引用

为了理解这个比较拗口的结论,让我们来看一段代码:

var obj = {
  data: {}
};

// data 指向 obj.data
var data = obj.data;

console.log(data === obj.data); // true-->data所操作的就是obj.data

data.name = 'Alan';
data.test = function () {
  console.log('hi')
};

// 通过data可以直接修改到data的值
console.log(obj) // { data: { name: 'Alan', test: [Function] } }

data = {
  name: 'Bob',
  add: function (a, b) {
    return a + b;
  }
};

// data是一个引用,直接赋值给它,只是让这个变量等于另外一个引用,并不会修改到obj本身
console.log(data); // { name: 'Bob', add: [Function] }
console.log(obj); // { data: { name: 'Alan', test: [Function] } }

obj.data = {
  name: 'Bob',
  add: function (a, b) {
    return a + b;
  }
};

// 而通过obj.data才能真正修改到data本身
console.log(obj); // { data: { name: 'Bob', add: [Function] } }

通过这个例子我们可以看到,data 虽然像一个引用一样指向了 obj.data,并且通过 data 可以访问到 obj.data 上的属性。但是由于 JavaScript 值传递的特性直接修改 data = xxx 并不会使得 obj.data = xxx。

打个比方最初设置 var data = obj.data 的时候,内存中的情况大概是:

|   Addr   |  内容  |
|----------|--------
| obj.data |  内存1 |
|   data   |  内存1 |

所以通过 data.xx 可以修改 obj.data 的内存1。

然后设置 data = xxx,由于 data 是拷贝的一个新的值,只是这个值是一个引用(指向内存1)罢了。让它等于另外一个对象就好比:

|   Addr   |  内容  |
|----------|--------
| obj.data |  内存1 |
|   data   |  内存2 |

让 data 指向了新的一块内存2。

如果是传统的引用(如上文中提到的 C++ 的引用),那么 obj.data 本身会变成新的内存2,但 JavaScript 中均是值传递,对象在传递的过程中拷贝了一份新的引用。所以这个新拷贝的变量被改变并不影响原本的对象。

Node.js 中的 module.exports 与 exports

上述例子中的 obj.data 与 data 的关系,就是 Node.js 中的 module.exports 与 exports 之间的关系。让我们来看看 Node.js 中 require 一个文件时的实际结构:

function require(...) {
  var module = { exports: {} };
  ((module, exports) => { // Node.js 中文件外部其实被包了一层自执行的函数
    // 这中间是你模块内部的代码.
    function some_func() {};
    exports = some_func;
    // 这样赋值,exports便不再指向module.exports
    // 而module.exports依旧是{}

    module.exports = some_func;
    // 这样设置才能修改到原本的exports
  })(module, module.exports);
  return module.exports;
}

所以很自然的:

console.log(module.exports === exports); // true
// 所以 exports 所操作的就是 module.exports

Node.js 中的 exports 就是拷贝的一份 module.exports 的引用。通过 exports 可以修改Node.js 当前文件导出的属性,但是不能修改当前模块本身。通过 module.exports 才可以修改到其本身。表现上来说:

exports = 1; // 无效
module.exports = 1; // 有效

这是二者表现上的区别,其他方面用起来都没有差别。所以你现在应该知道写module.exports.xx = xxx; 的人其实是多写了一个module.。

更复杂的例子

为了再练习一下,我们在来看一个比较复杂的例子:

var a = {n: 1};  
var b = a; 
a.x = a = {n: 2};  
console.log(a.x);
console.log(b.x);

按照开始的结论我们可以一步步的来看这个问题:

var a = {n: 1};   // 引用a指向内存1{n:1}
var b = a;        // 引用b => a => { n:1 }

内部结构:

|   Addr  |     内容     |
|---------|-------------|
|    a    |  内存1 {n:1} |
|    b    |  内存1       |

继续往下看:

a.x = a = {n: 2};  //  (内存1 而不是 a ).x = 引用 a = 内存2 {n:2}

a 虽然是引用,但是 JavaScript 是值传的这个引用,所以被修改不影响原本的地方。

|    Addr   |          内容         |
|-----------|-----------------------|
| 1) a	    |  内存2({n:2})         |
| 2) 内存1.x |  内存2({n:2})         |
| 3) b	    |  内存1({n:1, x:内存2}) |

所以最后的结果

  • a.x 即(内存2).x ==> {n: 2}.x ==> undefined
  • b.x 即(内存1).x ==> 内存2 ==> {n: 2}

总结

JavaScrip t中没有引用传递,只有值传递。对象(引用类型)的传递只是拷贝一个新的引用,这个新的引用可以访问原本对象上的属性,但是这个新的引用本身是放在另外一个格子上的值,直接往这个格子赋新的值,并不会影响原本的对象。本文开头所讨论的 Node.js 热更新时碰到的也是这个问题,区别是对象本身改变了,而原本拷贝出来的引用还指向旧的内存,所以通过旧的引用调用不到新的方法。

Node.js 并没有对 JavaScript 施加黑魔法,其中的引用问题依旧是 JavaScript 的内容。如 module.exports 与 exports 这样隐藏了一些细节容易使人误会,本质还是 JavaScript 的问题。另外推荐一个关于 Node.js 的进阶教程 《Node.js 面试》

注[1]:

  1. 老实说,模块在函数内声明有点谭浩强的感觉。
  2. 把 b = include(xxx) 写在调用内部,还可以通过设置成中间件绑定在公共地方来写。
  3. 除了写在调用内部,也可以导出一个工厂函数,每次使用时 b().num 一下调用也可以。
  4. 还可以通过中间件的形式绑定在框架的公用对象上(如:ctx.b = include(xxx))。
  5. 要实现这样的热更新必须在架构上就要严格避免旧代码被引用的可能性,否则很容易写出内存泄漏的代码。

ERR_INCOMPLETE_CHUNKED_ENCODING koa node

请求突然出现了 ERR_INCOMPLETE_CHUNKED_ENCODING 这个报错,貌似 http 返回的 chunk 跪了。虽然已经修复了,但还是没搞清楚发生了什么。

谷歌娘说在 nginx 的 location 里面加上 proxy_buffering off; 然后重启 nginx 果然好了。

问题是,我随后注释掉这个依旧能没问题,所以是 nginx 闹脾气了需要重启?

5628dd6ecd9fa100f371_size30_w521_h534

Cannot read property pm2_env of undefined

[PM2][WARN] Applications x-filter not running, starting...
/usr/local/lib/node_modules/pm2/lib/CLI.js:461
        Common.printOut(cst.PREFIX_MSG + 'App [%s] launched (%d instances)', data[0].pm2_env.name, data.length);
                                                                                    ^

TypeError: Cannot read property 'pm2_env' of undefined
    at /usr/local/lib/node_modules/pm2/lib/CLI.js:461:85
    at /usr/local/lib/node_modules/pm2/node_modules/pm2-axon-rpc/lib/client.js:45:10
    at Parser.<anonymous> (/usr/local/lib/node_modules/pm2/node_modules/pm2-axon/lib/sockets/req.js:67:8)
    at emitOne (events.js:96:13)
    at Parser.emit (events.js:188:7)
    at Parser._write (/usr/local/lib/node_modules/pm2/node_modules/amp/lib/stream.js:91:16)
    at doWrite (_stream_writable.js:307:12)
    at writeOrBuffer (_stream_writable.js:293:5)
    at Parser.Writable.write (_stream_writable.js:220:11)
    at Socket.ondata (_stream_readable.js:555:20)

碰到这个心情不好,最后发现。其实只要跑一下 pm2 update 就好了。大致是因为更新了 pm2 没有跑这个命令出来的问题。

node.js 调用 NATS 与 disque 性能简单对比

比较新的消息队列 server, NATSdisque 的对比。

仅插入

disque

Amount并发 100 500 1000 2000 4000
10w 3568.814ms 1910.550ms 2322.112ms 2577.898ms 2780.189ms
100w 32951.437ms 19610.882ms 20488.052ms 21875.545ms 26354.094ms

NATS

Amount并发 100 500 1000 2000 4000
10w 4486.236ms 4162.363ms 4330.767ms 4458.188ms 4987.342ms
100w 40481.896ms 41818.800ms 41704.926ms 45198.646ms 46906.605ms

双方的插入都在 500 的并发时表现还行,所以后面的测试用这个并发量来插入。

同时插入并读取

测试情况:

插入节点 : Server 节点: 读取节点 = 1: 1: N

disque

10w 级

2327.073ms 11085.363ms
3970.004ms 8064.683ms/8097.301ms
7637.522ms 8798.931ms/8804.586ms/8820.094ms/8986.903ms

100w 级

33974.781ms 128753.262ms
44862.931ms 90111.012ms/90975.795ms
67741.766ms 75597.516ms/75983.190ms/76229.671ms/76412.002ms

NATS

10w 级

6292.183ms 6268.520ms
6172.031ms 3673.333ms/3673.315ms
6127.849ms 2332.848ms/2332.729ms/2338.836ms/2339.670ms

100w 级

55505.581ms 55473.022ms
54234.438ms 29926.494ms/29926.681ms
52572.856ms 16027.231ms/16027.502ms/16025.114ms/16015.165ms

根据数据可以简单看出来, C语言编写的 disque 在纯插入的时候速度确实会快一些。然而到了同时插入并读取的时候, disque 的读取速度就慢了很多,可以看出来 1对多个客户端让 disque 在调度上产生了不小的消耗,而且读取的速度确实也比较慢。

NATS 就比较让人惊喜了,大概是因为异步式的架构,让 NATS 可以不关心插入的确认而是更注重调度和转发,所以让 NATS 的队列读取速度非常快,在跑测试的可以明显的感觉到,在 1:1:4 的时候 NATS 的流程分为 ①插入过程 ②clients读取 ③确认,NATS 的这种架构方式让流程②变得非常迅速。

使用这两个消息队列虽然不会内存泄漏,不过 disque 上限是 1779155,按照博主本机的测试数据看插入的快读取的慢随着时间推移总是有塞满的风险。disque 这种情况大概是因为设计的是安全队列的原因,安全队列类似 Linux 信号中的后 32 个用户自定义信号,不会因为当时没收到就丢失。而 NATS 的队列则属于不安全的队列,如果当时没收到也不会保留该消息,所以 NATS 比较起来塞满的风险更低。

测试环境:

macbook pro
CPU: 2.7 GHz Intel Core i5
MEM: 8 GB 1867 MHz DDR3

测试代码:

node-gnats/req.js
node-gnats/get.js
thunk-disque/req.js
thunk-disque/get.js

PS:
1) 博主这里只测了 1:1:N,没有测 N:1:M 和 N:L:M。
2) 一些比较高级的特性没有开启测试。
如果这些没关注到的这些地方有反转、文中有问题或者有更好的客户端推荐还请评论告知(或者电邮博主 lellansin@gmail.com)。