Node.js性能优化

本文内容源自知乎,地址:https://zhuanlan.zhihu.com/p/50055740

仅仅是简单的升级Node.js版本就可以轻松的获得性能提升,因为几乎任何新版本的Nodejs都会比老版本性能更好、
nodejs每个版本的性能提升主要来自于两个方面:

  • v8的版本更新
  • nodejs内部代码的更新优化

如何选择Nodejs的版本

nodejs的版本策略:

  • nodejs的笨笨主要分为Current和LTS;
  • Current就是当前最新的、依然处于开发中的nodejs版本
  • LTS就是稳定、会长期维护的版本
  • nodejs每六个月(每年的四月和十月)会发布一次大版本升级,大版本会带来一些不兼容的升级
  • 每年4月发布的版本通常是偶数,是LTS版本,即长期支持的版本,社区会从发布当年的十月开始,继续维护18+12个月(Active LTS + Maintenance LTS)
  • 每年十月分布的版本,版本号为奇数,只有8个月的维护期

举个例子,现在nodejs的版本是v11,LTS版本是V10和v8,更老的V6处于Maintenace LTS,从明年4月份起(18+12个月了)就不再维护,去年发布的v9版本今年6月就不在维护(奇数版本8个月)

使用fast-json-stringify加速JSON序列化

JavaScript中,生成json字符串是非常方便的

const json = JSON.stringify(obj)

JSON.stringify也存在性能优化的空间,那就是使用JSON Schema来加速序列化。
JSON序列化时,我们需要识别大量的字段类型,比如对于string类型,我们就需要两边加上"",对于数组类型,我们需要遍历数组,把每个对象序列化后,用,隔开,然后在两边加上[] ,诸如此类。如果我们已经提前通过Schema知道每个字段的类型,那么就不需要遍历,识别字段类型,而可以直接用序列化对应的字段,这就大大减少了计算开销,这就是fast-json-stringfy的原理,在项目的跑分中,在某些情况下甚至可以比JSON.stringify快接近10倍

jsonStringify

一个简单的示例:

const fastJson = require('fast-json-stringify')
const stringify = fastJson({
  title:'Example Schema',
  type:'object', 
  properties:{
    name:{type:'string'},
    age:{type:'integer'},
    books:{
      type:'array',
      item:{
        type:'array',
        uniqueItems:true
      }
    }
  }
})

console.log(stringify({
    name: 'Starkwang',
    age: 23,
    books: ['C++ Primer', '響け!ユーフォニアム~']
}))
//=> {"name":"Starkwang","age":23,"books":["C++ Primer","響け!ユーフォニアム~"]}

node.js的中间件业务中,通常会有很多数据使用json进行传输,并且这些json的结构非常相似(如果你使用了TypeScript,更是这样),这中场景就非常适用JSON Schema来优化。

提升Promise的性能

性能损耗主要来自于Promise对象自身的实现,V8原生实现的Promisebluebird这样的第三方实现的Promise库要慢很多。而async/await语法并不会带来太多的性能损失。
所以对于大量异步逻辑,轻量计算的中间件项目而言,可以在代码中把全局的Promise换为bluebird的实现

global.Promise = require('bluebird')

正确地编写异步代码

使用async/await之后,项目的一步代码会非常好看

const foo = await doSomethingAsync();
const bar = await doSomethingElseAsync();

但因此,有时我们也会忘记使用Promise给我们带来的其他能力,比如Promise.all()的并行能力

// bad
async function getUserInfo(id) {
    const profile = await getUserProfile(id);
    const repo = await getUserRepo(id)
    return { profile, repo }
}

// good
async function getUserInfo(id) {
    const [profile, repo] = await Promise.all([
        getUserProfile(id),
        getUserRepo(id)
    ])
    return { profile, repo }
}

还有比如Promise.any()(此方法不在ES6 Promise标准中,也可以使用标准的Promise.race()代替),我们可以用他轻松实现更加可靠快速的调用

async function getServiceIP(name) {
    // 从 DNS 和 ZooKeeper 获取服务 IP,哪个先成功返回用哪个
    // 与 Promise.race 不同的是,这里只有当两个调用都 reject 时,才会抛出错误
    return await Promise.any([
        getIPFromDNS(name),
        getIPFromZooKeeper(name)
    ])
}

优化V8 GC

我们在日常开发代码的时候,比较容易踩到下面的几个坑:

使用大对象作为缓存,导致老生代(Old space)的垃圾回收变慢

示例:

const cache = {}
async function getUserInfo(id) {
  if(!cache[id]){
    cache[id] = await getUserInfoFromDatabase(id)
  }
  return cache[id]
}

这里我们使用了一个变量cache作为缓存,加速用户信息的查询,进行了很多次查询后,cache对象会进入老生代,并且会变得无比庞大,而老生代是使用三色标记+DFS的方式进行GC的,一个大对象会直接导致GC花费的时间增长(而且也有内存泄露的风险)

解决方法就是:

  • 使用redis这样的外部缓存,实际上像Redis这样的内存型数据库非常适合这种场景
  • 限制本地缓存对象的大小,比如使用FIFO,TTL之类的机制来清理对象中的缓存

新生代空间不足,导致频繁GC

这个坑比较隐蔽
node默认给新生代分配的内存是64MB(64位的机器),但因为新生代GC使用的是Scavenge算法,所以实际使用的内存只有一半,即32MB。
当业务代码频繁地产生大量的小对象时,这个空间很容易被占满,从而触发GC。虽然新生代的GC比老生代要快得多,但是频繁的GC依然很大地影响性能,极端的情况下, GC甚至可以占用全部计算时间的30%左右。

解决方法就是,在启动node.js时,修改新生代的内存上限,减少GC的次数:

node --max-semi-space-size=128 app.js

也不是内存越大越好,因为随着内存的增加,GC的次数减少,但是每次GC所需的时间也会增加,所以并不是越大越好。但是一般根据经验而言,分配64MB或者128MB是比较合理的

正确的使用Stream

StreamNode.js最基本的概念,Node.js内部的大部分与IO相关的模块,比如HTTPnetfsrepl,都是建立在各种Stream之上的。

下面这个经典的例子,对于大文件,我们不需要把他完全读入内存,而是使用Stream流式地把他发送出去

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

// bad
http.createServer(function (req, res) {
    fs.readFile(__dirname + '/data.txt', function (err, data) {
        res.end(data);
    });
});

// good
http.createServer(function (req, res) {
    const stream = fs.createReadStream(__dirname + '/data.txt');
    stream.pipe(res);
});

使用pipeline管理stream

示例:

 const {pipeline} = require('stream');
const fs = require('fs');
const zlib = require('zlib');
pipeline(
    fs.createReadStream('archive.tar'),
    zlib.createGzip(),
    fs.createWriteStream('archive.tar.gz'),
    (err) => {
        if (err) {
            console.error('Pipeline failed', err);
        } else {
            console.log('Pipeline succeeded');
        }
    }
)

使用node-clinic快速定位性能问题

node-clinicNearForm 开源的一款 Node.js 性能诊断工具,可以非常快速地定位性能问题。

npm i -g clinic
npm i -g autocannon

使用的时候,先开启服务进程:

clinic doctor -- node server.js

·

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容

  • 老梁当家 文,冷月秋风 梁家村的老梁认为最自豪的事就是给儿子办的婚宴了,逢人只要提起来那是絮叨的没完,儿子...
    冷月秋风qin阅读 641评论 0 2