提升node.js性能的8个技巧

Node.js 受益于它的事件驱动和异步的特征,已经很快了。
但是,在现代网络中只是快是不行的。如果你打算用 Node.js 开发你的下一个Web 应用的话,那么你就应该无所不用其极,让你的应用更快,异常的快。
本文将介绍 10 条,经过检验得知可大大提高 Node 应用的技巧。废话不多说,让我们逐条来看看

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

在 JavaScript 中,生成 JSON 字符串是非常方便的:

const json = JSON.stringify(obj)

但很少人会想到这里竟然也存在性能优化的空间,那就是使用 JSON Schema 来加速序列化。

在 JSON 序列化时,我们需要识别大量的字段类型,比如对于 string 类型,我们就需要在两边加上 ",对于数组类型,我们需要遍历数组,把每个对象序列化后,用 , 隔开,然后在两边加上 [ 和 ],诸如此类等等。

但如果已经提前通过 Schema 知道每个字段的类型,那么就不需要遍历、识别字段类型,而可以直接用序列化对应的字段,这就大大减少了计算开销,这就是 fast-json-stringfy 的原理。

根据项目中的跑分,在某些情况下甚至可以比 JSON.stringify 快接近 10 倍!

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

console.log(stringify({
    name: 'node.js',
    age: 10,
    books: ['Node.js实战(第2版)', '[英] 亚历克斯•杨']
}))
//=> {"name":"node.js","age":23,"books":["Node.js实战(第2版)","[英] 亚历克斯•杨"]}

2、提升 Promise 的性能

Promise 是解决回调嵌套地狱的灵丹妙药,特别是当自从 async/await 全面普及之后,它们的组合无疑成为了 JavaScript 异步编程的终极解决方案,现在大量的项目都已经开始使用这种模式。

3、正确地编写异步代码

使用 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)
    ])
} 

4、正确地使用 Stream

Stream 是 Node.js 最基本的概念之一,Node.js 内部的大部分与 IO 相关的模块,比如 http、net、fs、repl,都是建立在各种 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);
});

在业务代码中合理地使用 Stream 能很大程度地提升性能,当然是但实际的业务中我们很可能会忽略这一点,比如采用 React 服务器端渲染的项目,我们就可以用 renderToNodeStream:

const ReactDOMServer require('react-dom/server')
const http = require('http')
const fs = require('fs')
const app = require('./app')

// bad
const server = http.createServer((req, res) => {
    const body = ReactDOMServer.renderToString(app)
    res.end(body)
});

// good
const server = http.createServer(function (req, res) {
    const stream = ReactDOMServer.renderToNodeStream(app)
    stream.pipe(res)
})

server.listen(8000)

使用 pipeline 管理 stream
在过去的 Node.js 中,处理 stream 是非常麻烦的,举个例子:

source.pipe(a).pipe(b).pipe(c).pipe(dest)
一旦其中 source、a、b、c、dest 中,有任何一个 stream 出错或者关闭,会导致整个管道停止,此时我们需要手工销毁所有的 stream,在代码层面这是非常麻烦的。

所以社区出现了 pump 这样的库来自动控制 stream 的销毁。而 Node.js v10.0 加入了一个新的特性:stream.pipeline,可以替代 pump 帮助我们更好的管理 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');
        }
    }
);

实现自己的高性能 Stream
在业务中你可能也会自己实现一个 Stream,可读、可写、或者双向流,可以参考文档:

Stream 虽然很神奇,但自己实现 Stream 也可能会存在隐藏的性能问题,比如:

class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            this.push(chunk);
        }
    }
}

当我们调用 new MyReadable().pipe(xxx) 时,会把 getNextChunk() 所得到的 chunk 都 push 出去,直到读取结束。但如果此时管道的下一步处理速度较慢,就会导致数据堆积在内存中,导致内存占用变大,GC 速度降低。

而正确的做法应该是,根据 this.push() 返回值选择正确的行为,当返回值为 false 时,说明此时堆积的 chunk 已经满了,应该停止读入。

class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            if (!this.push(chunk)) {
                return false  
            }
        }
    }
}

这个问题在 Node.js 官方的一篇文章中有详细的介绍:Backpressuring in Streams

5.实现反向代理服务器

我们在NGINX.Inc的时候,如果看到有应用程序服务器直接接触传入的访问流量,用于高性能网站核心的时候,总会不自觉地有点担忧。这包括许多基于WordPress的网站,也包括Node.js网站。

Node.js专为可扩展性而设计,它比大多数应用服务器更易于扩展,它的web服务器端可以处理好大量的访问流量。但是web服务并不是Node.js存在的理由——Node.js并不是因为这个目的而被构建的。

如果你有一个大流量网站,提高应用程序性能的第一步是在你的Node.js服务器前放一个反向代理服务器。这样可以保护Node.js服务器直接接触外部访问流量,还能让你灵活使用多个应用程序服务器,平衡负载服务器,缓存内容。

image

在现有的服务器设置前放NGINX作为一个反向代理服务器,是NGINX的核心用例,全世界各地已经有数以千万计的网站实施了。

使用NGINX作为Node.js的反向代理服务器还有一些特定的优势,其中包括:

  • 简化操作权限和端口分配

  • 更有效地服务于静态图像(见第二个小窍门)

  • 成功管理Node.js崩溃的情况

  • 减轻DoS攻击

6.缓存静态文件

随着基于Node.js的网站的使用量的增长,服务器的压力开始越来越大。这时候你要做这两件事情:

充分利用Node.js服务器。
使得添加应用程序服务器和负载均衡变得容易。
这其实是很容易做到的。一开始就实施NGINX作为反向代理服务器,就像第一点技巧中所描述的那样。这样就能轻易实现高速缓存、负载平衡(如果有多个Node.js服务器的话)等。

针对Modulus,一个应用程序容器平台,有一篇非常有用的关于利用NGINX增压Node.js应用程序性能的文章。由于Node.js都是靠自己完成所有的工作的,所以我们的网站平均每秒只能服务将近900个请求。使用NGINX作为反向代理服务器,提供静态内容,一个站点每秒可服务超过1600个请求——性能提升了近2倍。

性能的提升能让你有时间采取额外措施以适应进访问量的增长,如审查(或提高)网站设计,优化程序代码,部署更多的应用程序服务器。

以下配置代码适用运行于Modulus的网站:

server {
    listen 80;
    server_name static - test - 47242.onmodulus.net;
    root / mnt / app;
    index index.html index.htm;
    location / static / {
        try_files $uri $uri / =404;
    }
    location / api / {
        proxy_pass http: //node-test-45750.onmodulus.net;
    }

例如,在Nginx位置块中,你可能不想要缓存某些内容。例如,你通常不会想要缓存博客平台的管理界面的。以下就是禁用[或免除]缓存Ghost管理界面的配置代码:

location~ ^ /(?:ghost|signout) { 
        proxy_set_header X-Real-IP $remote_addr;  
        proxy_set_header Host $http_host;      
        proxy_pass http:/ / ghost_upstream;
        add_header Cache - Control "no-cache, private, no-store,
        must-revalidate, max-stale=0, post-check=0, pre-check=0";
}

缓存NGINX服务器上的静态文件可以显著减轻Node.js应用程序服务器的负载,让它能够达到更佳性能。

7.实现Node.js负载平衡

Node.js应用高性能的真正关键运行多个应用程序服务器和平衡负载。

Node.js负载平衡可能特别棘手,因为Node.js允许运行在web浏览器上的JavaScript代码和运行在Node.js应用服务器上的JavaScript代码做高水平的交互,同时使用JSON对象作为数据交换的介质。这意味着,一个给定的客户会话会持续运行在特定的应用程序服务器上,并且会话持久性用多个应用程序服务器天然地难以实现。

Internet和web的主要优点之一就是高度无国界,其中包括通过任意服务器访问请求文件来满足客户端请求。Node.js颠覆了无国界,并且在有状态的环境中——同一服务器始终如一地响应来自任意特定客户端的请求——效果最好。

通过NGINX Plus,而非开源NGINX软件,可以最好地满足这个需求。NGINX的两个版本颇为相似,但一个主要区别就是它们对负载平衡算法的支持不同。

NGINX支持无状态的负载均衡方法:

  • 循环。新的请求会去往列表中的下一个服务器。

  • 最少的连接。新的请求会去到活跃连接最少的服务器。

  • IP Hash。新的请求会去往哈希分配客户端IP地址的服务器。

只是这些方法中的一种,IP Hash,可靠地发送指定客户端请求到同一服务器,有利于Node.js应用程序。然而,IP Hash很容易导致某台服务器收到的请求数量不成比例,在牺牲其他服务器的代价下,正如这一篇博客中描述的负载均衡技术那样。此方法支持的有状态是以牺牲潜在不理想的跨服务器资源的请求分配为代价的。

不同于NGINX,NGINX Plus支持会话持久性。在使用会话持久性的时候,同一服务器还能可靠地接收来自指定客户端的所有请求。 Node.js的优势——在客户端和服务器之间有状态的通信,以及NGINX Plus的优势——高级负载均衡能力,都达到最大化。

所以,你可以使用NGINX或NGINX Plus来支持多个Node.js服务器的负载均衡。只有NGINX才有可能让你最大化地实现负载均衡性能和友好的Node.js有状态性。内置于NGINX的应用健康检查以及监控功能也很有用。

NGINX Plus还支持会话维持,因此允许应用程序服务器在它采取停止服务的请求之后,还能优雅地完成当前会话。

8.代理WebSocket连接

HTTP,在所有版本里,是专为“pull”通信——来自于服务器的客户端请求文件设计的。WebSocket是一个允许“push”和“push/pull”通信的工具,即服务器可以主动发送客户端没有请求的文件。

WebSocket协议可以更容易地支持客户端和服务器之间更坚固的相互作用,同时减少传输的数据量并最小化等待时间。当需要时,可以实现全双工传输连接,也就是说根据需要客户和服务器都可以发起并接收请求。

WebSocket协议具有强大的JavaScript接口,因此非常适合作为应用服务器的Node.js——而且,对于事务量不多的web应用程序,也可以作为web服务器。当事务量增加,那么在客户端和Node.js web服务器之间,多个应用服务器之间使用NGINX或NGINX Plus插入NGINX就有必要了。

image

Node.js通常与Socket.IO联合使用,Socket.IO是一个WebSocket API,它在Node.js应用程序中很受欢迎。这可能会导致port 80(对于HTTP)或port 443(对于HTTPS)变得相当拥挤,而解决方法就是代理Socket.IO服务器。你可以使用NGINX作为代理服务器中,就像前面说的那样,并且还获得其他的功能,例如静态文件缓存,负载均衡等。

以下就是作为server.js node应用程序文件监听port 5000的代码。它担当了代理服务器(而不是web服务器)的角色,并路由请求到正确的端口:

const io = require('socket.io').listen(5000);

io.sockets.on('connection', function (socket) {
  socket.on('set nickname', function (name) {
    socket.set('nickname', name, function () {
      socket.emit('ready');
    });
  });
  
  socket.on('msg', function () {
    socket.get('nickname', function (err, name) {     
    console.log('Chat message by ', name);
    });
  });
});

const socket = io(); // 这是你的初始化代码。

推荐阅读更多精彩内容