Mac docker-machine: Error with pre-create check: “exit status 126”

126 是没有安装 VirtualBox 的报错. 如果你想安装是可以通过如下命令安装:

if ! type "brew" > /dev/null; then
  ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)";
fi
brew tap phinze/homebrew-cask && brew install brew-cask;
brew cask install vagrant;
brew cask install virtualbox;

不过需要了解的是 Docker for Mac 默认是没有使用 VirtualBox 而是使用了 HyperKit, 所以是不用建立 VirtualBox 来做 service. 如果你需要改什么配置可以直接通过 docker for Mac 的 Docker -> Preferences 去改.

如何分析 Node.js 中的内存泄漏

内存泄漏(Memory Leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。如果内存泄漏的位置比较关键,那么随着处理的进行可能持有越来越多的无用内存,这些无用的内存变多会引起服务器响应速度变慢,严重的情况下导致内存达到某个极限(可能是进程的上限,如 v8 的上限;也可能是系统可提供的内存上限)会使得应用程序崩溃。

传统的 C/C++ 中存在野指针,对象用完之后未释放等情况导致的内存泄漏。而在使用虚拟机执行的语言中如 Java、JavaScript 由于使用了 GC (Garbage Collection,垃圾回收)机制自动释放内存,使得程序员的精力得到的极大的解放,不用再像传统语言那样时刻对于内存的释放而战战兢兢。

但是,即便有了 GC 机制可以自动释放,但这并不意味这内存泄漏的问题不存在了。内存泄漏依旧是开发者们不能绕过的一个问题,今天让我们来了解如何分析 Node.js 中的内存泄漏。

GC in Node.js

Node.js 使用 V8 作为 JavaScript 的执行引擎,所以讨论 Node.js 的 GC 情况就等于在讨论 V8 的 GC。在 V8 中一个对象的内存是否被释放,是看程序中是否还有地方持有改对象的引用。

在 V8 中,每次 GC 时,是根据 root 对象 (浏览器环境下的 window,Node.js 环境下的 global ) 依次梳理对象的引用,如果能从 root 的引用链到达访问,V8 就会将其标记为可到达对象,反之为不可到达对象。被标记为不可到达对象(即无引用的对象)后就会被 V8 回收。更多细节,可以参见 alinode 的 解读 V8 GC

了解上述的点之后,你就会知道,在 Node.js 中内存泄露的原因就是本该被清除的对象,被可到达对象引用以后,未被正确的清除而常驻内存。

内存泄漏的几种情况

一、全局变量

a = 10;
//未声明对象。

global.b = 11;
//全局变量引用

这种比较简单的原因,全局变量直接挂在 root 对象上,不会被清除掉。

二、闭包

function out() {
  const bigData = new Buffer(100);
  inner = function () {
    void bigData;
  }
}

闭包会引用到父级函数中的变量,如果闭包未释放,就会导致内存泄漏。上面例子是 inner 直接挂在了 root 上,从而导致内存泄漏(bigData 不会释放)。

需要注意的是,这里举得例子只是简单的将引用挂在全局对象上,实际的业务情况可能是挂在某个可以从 root 追溯到的对象上导致的。

三、事件监听

Node.js 的事件监听也可能出现的内存泄漏。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现,所以事件重复监听可能收到如下警告:

(node:2752) Warning: Possible EventEmitter memory leak detected。11 haha listeners added。Use emitter。setMaxListeners() to increase limit

例如,Node.js 中 Agent 的 keepAlive 为 true 时,可能造成的内存泄漏。当 Agent keepAlive 为 true 的时候,将会复用之前使用过的 socket,如果在 socket 上添加事件监听,忘记清除的话,因为 socket 的复用,将导致事件重复监听从而产生内存泄漏

原理上与前一个添加事件监听的时候忘了清除是一样的。在使用 Node.js 的 http 模块时,不通过 keepAlive 复用是没有问题的,复用了以后就会可能产生内存泄漏。所以,你需要了解添加事件监听的对象的生命周期,并注意自行移除。

关于这个问题的实例,可以看 Github 上的 issues(node Agent keepAlive 内存泄漏

四、其他原因

还有一些其他的情况可能会导致内存泄漏,比如缓存。在使用缓存的时候,得清楚缓存的对象的多少,如果缓存对象非常多,得做限制最大缓存数量处理。还有就是非常占用 CPU 的代码也会导致内存泄漏,服务器在运行的时候,如果有高 CPU 的同步代码,因为Node.js 是单线程的,所以不能处理处理请求,请求堆积导致内存占用过高。

定位内存泄漏

一、重现内存泄漏情况

想要定位内存泄漏,通常会有两种情况:

  1. 对于只要正常使用就可以重现的内存泄漏,这是很简单的情况只要在测试环境模拟就可以排查了。
  2. 对于偶然的内存泄漏,一般会与特殊的输入有关系。想稳定重现这种输入是很耗时的过程。如果不能通过代码的日志定位到这个特殊的输入,那么推荐去生产环境打印内存快照了。需要注意的是,打印内存快照是很耗 CPU 的操作,可能会对线上业务造成影响。

快照工具推荐使用 heapdump 用来保存内存快照,使用 devtool 来查看内存快照。使用 heapdump 保存内存快照时,只会有 Node.js 环境中的对象,不会受到干扰(如果使用 node-inspector 的话,快照中会有前端的变量干扰)。

PS:安装 heapdump 在某些 Node.js 版本上可能出错,建议使用 npm install heapdump -target=Node.js 版本来安装。

二、打印内存快照

将 heapdump 引入代码中,使用 heapdump.writeSnapshot 就可以打印内存快照了了。为了减少正常变量的干扰,可以在打印内存快照之前会调用主动释放内存的 gc() 函数(启动时加上 –expose-gc 参数即可开启)。

const heapdump = require('heapdump');

const save = function () {
  gc();
  heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');
}

在打印线上的代码的时候,建议按照内存增长情况来打印快照。heapdump 可以使用 kill 向程序发送信号来打印内存快照(只在 *nix 系统上提供)。

kill -USR2 

推荐打印 3 个内存快照,一个是内存泄漏之前的内存快照,一个是少量测试以后的内存快照,还有一个是多次测试以后的内存快照。

第一个内存快照作为对比,来查看在测试后有哪些对象增长。在内存泄漏不明显的情况下,可以与大量测试以后的内存快照对比,这样能更容易定位。

三、对比内存快照找出泄漏位置

通过内存快照找到数量不断增加的对象,找到增加对象是被谁给引用,找到问题代码,改正之后就行,具体问题具体分析,这里通过我们在工作中遇到的情况来讲解。

const {EventEmitter} = require('events');
const heapdump = require('heapdump');

global.test = new EventEmitter();
heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');

function run3() {
  const innerData = new Buffer(100);
  const outClosure3 = function () {
    void innerData;
  };
  test.on('error', () => {
    console.log('error');
  });
  outClosure3();
}

for(let i = 0; i < 10; i++) {
  run3();
}
gc();

heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');

这里是对错误代码的最小重现代码。

首先使用 node –expose-gc index.js 运行代码,将会得到两个内存快照,之后打开 devtool,点击 profile,载入内存快照。打开对比,Delta 会显示对象的变化情况,如果对象 Delta 一直增长,就很有可能是内存泄漏了。

可以看到有三处对象明显增长的地方,闭包、上下文以及 Buffer 对象增长。点击查看一下对象的引用情况:

其实这三处对象增长都是一个问题导致的。test 对象中的 error 监听事件中闭包引用了 innerData 对象,导致 buffer 没有被清除,从而导致内存泄漏。

其实这里的 error 监听事件中没有引用 innerData 为什么会闭包引用了 innerData 对象,这个问题很是疑惑,后来弄清是 V8 的优化问题,在文末会额外讲解一下。对于对比快照找到问题,得看你对代码的熟悉程度,还有眼力了。

如何避免内存泄漏

文中的例子基本都可以很清楚的看出内存泄漏,但是在工作中,代码混合上业务以后就不一定能很清楚的看出内存泄漏了,还是得依靠工具来定位内存泄漏。另外下面是一些避免内存泄漏的方法。

  1. ESLint 检测代码检查非期望的全局变量。
  2. 使用闭包的时候,得知道闭包了什么对象,还有引用闭包的对象何时清除闭包。最好可以避免写出复杂的闭包,因为复杂的闭包引起的内存泄漏,如果没有打印内存快照的话,是很难看出来的。
  3. 绑定事件的时候,一定得在恰当的时候清除事件。在编写一个类的时候,推荐使用 init 函数对类的事件监听进行绑定和资源申请,然后 destroy 函数对事件和占用资源进行释放。

额外说明

在做了很多测试以后得到下面关于闭包的总结。

class Test{};
global.test = new Test()
function run5(bigData) {
  const innerData = new Buffer(100);

  // 被闭包引用,创建一个 context: context1。
  // context1 引用 bigData,innerData。
  // closure 为 function run5()
  // run5函数没有 context,所以 context1 没有previous。
  // 在 run5中新建的函数将绑定上 context1。

  test.outClosure5 = function () {

    // 此函数闭包 context 指向 context1。

    void bigData;
    const closureData = new Buffer(100);

    // 被闭包使用,创建 context: context2。
    // outClosure5 函数有 context1,previous 指向 context1。
    // 在 outClosure5 中新建的函数将绑定上context2。

    test.innerClosure5 = function () {

      // 此函数闭包 context 指向 context2。

      void innerData;
    }
    test.innerClosure5_1 = function () {

      // 此函数闭包 context 指向 context2。

      void closureData;
    }
  };
  test.outClosure5_1 = function () {

  }
  test.outClosure5();
}

run5(new Buffer(1000));

V8 会生成一个 context 内部对象来实现闭包。下面是 V8 生成 context 的规则。

V8 会在被闭包引用变量声明处创建一个 context2,如果被闭包的变量所在函数拥有 context1 ,则创建的 context2 的 previous指向函数 context1。在被闭包引用变量的函数内新建的函数将会绑定上 context2。

由于这个和 V8版本相关,这里只测试了 v6.2.2 和 v6.10.1 还有 v7.7.1,都是相同的情况。如果想实践测试可以在这个 repo 上了解更多。

注:本文由饿了么大前端 Node 组推出,本组的 @王大帅 起稿@lellansin 整理。

5年博客小结以及立 flag

这几年写博客的时间越来越少了。早期的时候写博客主要是因为想要做学习笔记,比如学到了xxx 或者用了 xx 做到了 yy 之类的,写的很开心。

之后写博客越来越少,除了因为工作忙加班加成狗之外。还有明显的问题是,发觉博客越来越难写了。而觉得越来越难写的原因,则是因为自己懂得越来越多了。

知道的越多,写出来的越少,看起来很矛盾,其实这是博主这几年成长中碰到的一个非常真实的问题。

说起来有点好笑就好像,小学的时候上自然课,听说牛顿和记者的故事一样。画了两个圈,一个大圈一个小圈,分别表示两人的知识量。然后圈外则是不知道的知识一样。

现在看来,这个故事是非常真实的。因为知道的越多,就越会发现自己的无知。

写博客去记录或者解释问题的时候,深深感受到的日益增长的困难。因为懂的多了,所以在一些问题的细节上会察觉到更多的问题,从而导致原本的一个问题不敢去写,或者是写了一半不想含糊带过于是就此搁笔。

要量化这个问题来看,好比一个问题,你要了解 90分了,才能写出来一个50分的文章。如果只了解 60分 那么基本写不出来什么东西,还不如直接转载别人的教程。

前不久有幸碰到陈皓(左耳朵耗子),并询问了几个目前比较在意的问题(非技术)。谈话过程其实并没有聊到这个问题。不过陈老师确实给了我很深的印象。

当时的问题记得是问,关于平常注意力分散的情况,比如写一个代码,查一个 API,最后问题发散出去看了很多其他与手头工作无关的技术资料。陈老师但是的说法挺简单,就是“极端一点”感兴趣就继续深究,继续学下去不要回头。

另一个问题真是关于技术的选择上,目前的技术非常快,博客、IT咨询很多,也出现了例如掘进、伯乐在线之类专门整理咨询的网站。不过陈皓老师的想法依旧极端,不需要特意去理会这些咨询,大概是做好自己就行了。

所以现在想想,还是准备重新开始梳理下自己的想法和知识。碰到细节问题也都多列举,尽量试试“求甚解”。所以也准备立个 flag,今年剩下的时间每周都写几篇博文。

Mongodb Error: network error while attempting to run command ‘isMaster’ on host

$ mongo
MongoDB shell version: 3.2.4
connecting to: test
2016-05-16T14:33:58.461+0800 E QUERY    [thread1] Error: network error while attempting to run command 'isMaster' on host '127.0.0.1:27017'  :
connect@src/mongo/shell/mongo.js:224:14
@(connect):1:6

exception: connect failed

搜了一下有说一个解决方案是在启动参数上加个flag

Solution: add the following option to your mongod command
--bind_ip 127.0.0.1 

不过燃冰暖,随后检查了下日志发现其实就是连接数过多,

...
2016-05-16T14:33:28.311+0800 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:50075 #200 (200 connections now open)
2016-05-16T14:33:28.311+0800 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:50074 #201 (201 connections now open)
2016-05-16T14:33:28.311+0800 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:50089 #202 (202 connections now open)
2016-05-16T14:33:28.312+0800 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:50088 #203 (203 connections now open)
2016-05-16T14:33:28.312+0800 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:50087 #204 (204 connections now open)
2016-05-16T14:33:38.360+0800 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:50092 #205 (205 connections now open)
2016-05-16T14:33:38.360+0800 I NETWORK  [initandlisten] connection refused because too many open connections: 204

根据日志里面的 accepted from ip:port ,找到端口,然后 lsof -i tcp:50075 之类的命令找到访问太多的进程,问题基本上就找到了。

实际上的问题应该是 “connection refused because too many open connections: 204”

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)。

node.js Mongodb parseError occured 导致连接断开

情况:node.js 使用原生 mongodb 依赖查询。

最近日志收到如下报错:

[2015-12-28 21:00:01.848] [ERROR] console - [Error: parseError occured]
Error: parseError occured
    at null.<anonymous> (/data/game_server_142/node_modules/mongodb/lib/mongodb/connection/connection_pool.js:198:34)
    at emit (events.js:98:17)
    at Socket.<anonymous> (/data/game_server_142/node_modules/mongodb/lib/mongodb/connection/connection.js:411:20)
    at Socket.emit (events.js:95:17)
    at Socket.<anonymous> (_stream_readable.js:764:14)
    at Socket.emit (events.js:92:17)
    at emitReadable_ (_stream_readable.js:426:10)
    at emitReadable (_stream_readable.js:422:5)
    at readableAddChunk (_stream_readable.js:165:9)
    at Socket.Readable.push (_stream_readable.js:127:10)

查询 mongodb 日志如下:

Mon Dec 28 21:00:00 [conn72] query game_142.player query: { ... 查询 ... } nscanned:101 nreturned:101 reslen:1374301 146ms
Mon Dec 28 21:00:01 [conn72] getmore game_142.player query: { ... 查询 ... } cursorid:5517049421632407939 nreturned:427 reslen:4201834 688ms
Mon Dec 28 21:00:01 [conn71] end connection 10.105.50.74:59131
Mon Dec 28 21:00:01 [conn72] SocketException handling request, closing client connection: 9001 socket exception [2] server [10.105.50.74:59132]
Mon Dec 28 21:00:01 [conn74] end connection 10.105.50.74:59134
Mon Dec 28 21:00:01 [conn70] end connection 10.105.50.74:59130
Mon Dec 28 21:00:01 [conn73] end connection 10.105.50.74:59133

报错之后客户端与mongodb之间的连接断开。

谷歌了不少老外的情况看到 http://stackoverflow.com/questions/19546561/node-mongodb-error-connection-closed-due-to-parseerror 这一篇的情况基本与博主碰到的情况相同。

里面提到 “The production Mongo driver throws away all errors in a catch block.” 然后后面说 node 的原生 npm 模块 mongodb 在它的 1.4 版本里面修复了这个问题。

随后检查了下 node_modules/mongodb/package.json 发现版本确实是新的,不过顺着版本思路检查,发现运维新搭的 mongod server 的版本是 2.0.x 而目前公司服务器用的 mongod 版本是 3.0.3,于是升级 mongod 之后解决了。(真是没有一点点防备啊 (╯‵□′)╯︵┻━┻)

ld: library not found for -lgcc_s.10.5 在 Mac 下 NPM 编译安装的常见错误

情况:通过 Node 的 NPM 编译安装某些模块的时候报错:

ld: library not found for -lgcc_s.10.5  
clang: error: linker command failed with exit code 1 (use -v to see invocation)  

解决方案:
到 AppStore 中安装 XCode 7。装完以后,打开 XCode 并且接受 license 协议。更新之后,node-gyp 编译就没有这个问题了。