Node.js 性能调优之代码篇(一)——使用原生模块

Node.js 性能调优之代码篇(一)——使用原生模块

nswbmwnswbmw
每次看 cpuprofile,我都情不自禁地作一个黑人问号???的表情。

由于历史原因,石墨后端的框架选型是 koa@1+(generator|bluebird)+sequelize。这个选型并没有什么问题,也很常见,但是到了滥用的地步。就像封面图表达的那样,排除掉 sequelize 这个不得不用的模块,从 cpuprofile 角度讲讲为什么我认为应该用 async/await+Promise 替代 co+generator|bluebird。

我的观点是:使用原生模块具有更清晰的调用栈

下面用 4 个例子对比,看下相同逻辑不同代码生成的 cpuprofile 中调用栈的信息。

async.js

'use strict';

const fs = require('fs');
const profiler = require('v8-profiler');

async function A() {
  return await Promise.resolve('A');
}

async function B() {
  return await A();
}

(async function asyncWrap() {
  const start = Date.now();
  profiler.startProfiling();
  while (Date.now() - start < 10000) {
    await B();
  }
  const profile = profiler.stopProfiling();
  profile.export()
    .pipe(fs.createWriteStream('async.cpuprofile'))
    .on('finish', () => {
      profile.delete();
      console.error('cpuprofile export success');
    })
})();

截图如下:

可以看出:asyncWrap 中调用了 B 函数,B 函数调用了 A 函数,A 函数中 resolve 了一个值。asyncWrap 中还调用了 stopProfiling 函数。

co.js

'use strict';

const fs = require('fs');
const co = require('co');
const profiler = require('v8-profiler');

function *A() {
  return yield Promise.resolve('A');
}

function *B() {
  return yield A();
}

co(function *coWrap() {
  const start = Date.now();
  profiler.startProfiling();
  while (Date.now() - start < 10000) {
    yield B();
  }
  const profile = profiler.stopProfiling();
  profile.export()
    .pipe(fs.createWriteStream('co.cpuprofile'))
    .on('finish', () => {
      profile.delete();
      console.error('cpuprofile export success');
    })
}).catch(console.error);

截图如下:

可以看出:调用栈非常不清晰,太多没有用的 co 相关的调用栈。如果 n 个 generator 层层嵌套,就会出现 n 倍的 (anonymous)->onFullfiled->next->toPromise->co->Promise->(anonymous) 调用栈。如果你读过 co 的源码可能知道,这是 co 将 generator 解包的过程。其实这个可以通过 yield generator -> yield* generator 来解决。

co_better.js

'use strict';

const fs = require('fs');
const co = require('co');
const profiler = require('v8-profiler');

function *A() {
  return yield Promise.resolve('A');
}

function *B() {
  return yield *A();
}

co(function *coWrap() {
  const start = Date.now();
  profiler.startProfiling();
  while (Date.now() - start < 10000) {
    yield *B();
  }
  const profile = profiler.stopProfiling();
  profile.export()
    .pipe(fs.createWriteStream('co_better.cpuprofile'))
    .on('finish', () => {
      profile.delete();
      console.error('cpuprofile export success');
    })
}).catch(console.error);

截图如下:

可以看出:相比 co.js 调用栈就清晰了很多,不过相比用 async/await 还是多了些 onFulfilled、next。

co_bluebird.js

'use strict';

const fs = require('fs');
const co = require('co');
const Promise = require('bluebird');
const profiler = require('v8-profiler');

function *A() {
  return yield Promise.resolve('A');
}

function *B() {
  return yield *A();
}

co(function *coBluebirdWrap() {
  const start = Date.now();
  profiler.startProfiling();
  while (Date.now() - start < 10000) {
    yield *B();
  }
  const profile = profiler.stopProfiling();
  profile.export()
    .pipe(fs.createWriteStream('co_bluebird.cpuprofile'))
    .on('finish', () => {
      profile.delete();
      console.error('cpuprofile export success');
    })
}).catch(console.error);

截图如下:

可以看出:相比较 co_better.js,调用栈中多了许多 bluebird 模块的无用信息。而且这只是非常简单的示例代码,要是复杂的业务逻辑代码生成的 cpuprofile,几乎没法看了。

结论:使用 async/await+Promise+命名函数,具有更清晰的调用栈,让分析 cpuprofile 时不再痛苦。

聪明的你会问:

  1. bluebird 太好用了,完全放弃不下啊?你可以逐步替换嘛,大部分场景多写点代码就可以避免使用 bluebird 了,比如 bluebird 的 .all .map .filter 啥的都可以用 Promise.all 去实现,实在实现起来复杂的那就先用着 bluebird 呗。
  2. 我现在代码中大量使用了 yield+generator 怎么办?
    1. 将所有 yield generator 替换成 yield* generator。
    2. 升级到 node@^8,逐步用 async/await 替换,毕竟 async 函数调用后返回的也是一个 promise 嘛,也是 yieldable 的。
  3. 性能比较呢?node@8 下 async/await 完胜 co。medium.com/@markherhold

yield -> yield* 的坑

上面讲到可以将 yield generator -> yield* generator,这里面有一个坑,也是由于滥用 co 导致的。代码如下:

const co = require('co')

function* genFunc () {
  return Promise.resolve('genFunc')
}

co(function* () {
  console.log(yield genFunc()) // => genFunc
  console.log(yield* genFunc()) // => Promise { 'genFunc' }
})

可以看出:一个 generatorFunction 返回一个 promise,当使用 yield 的时候,co 判断返回了一个 promise 会继续帮我们调用它的 then 得到真正的字符串。如果使用 yield*,即用了语言原生的特性而不经过 co,直接返回一个 promise。

解决方法(任选其一):

  1. function* genFunc -> function genFunc,用 yield genFunc()
  2. return Promise.resolve('genFunc') -> return yield Promise.resolve('genFunc'),用 yield* genFunc()

作业

请读者自行尝试 async/await+bluebird 的情况。

async_bluebird.js

'use strict';

const fs = require('fs');
const profiler = require('v8-profiler');
const Promise = require('bluebird');

async function A() {
  return await Promise.resolve('A');
}

async function B() {
  return await A();
}

(async function asyncBluebirdWrap() {
  const start = Date.now();
  profiler.startProfiling();
  while (Date.now() - start < 10000) {
    await B();
  }
  const profile = profiler.stopProfiling();
  profile.export()
    .pipe(fs.createWriteStream('async_bluebird.cpuprofile'))
    .on('finish', () => {
      profile.delete();
      console.error('cpuprofile export success');
    })
})();

结论:调用栈比 co_blueblird.js 的还难懂。。黑人问号???

文章被以下专栏收录
10 条评论