如何打造高性能Node应用

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

关于性能优化的考量应该贯穿整个产品的设计和实现过程中,特别是对于前期的基础架构的设计显得尤为重要,因为一旦应用的体系结构趋于稳定,在生产中再进行更改就特别困难。


1

性能分析


在具体实现性能优化之前,需要对系统的性能进行测试,才能知道系统的瓶颈和要优化的点在哪里。

这里推荐几个有用的测试工具:

第一个是benchmark(http://benchmarkjs.com/),它主要用来做前端代码性能测试:

 1// 例子来自官方
2var suite = new Benchmark.Suite;
3
4// add tests
5suite.add('RegExp#test'function({
6  /o/.test('Hello World!');
7})
8.add('String#indexOf'function({
9  'Hello World!'.indexOf('o') > -1;
10})
11// add listeners
12.on('cycle'function(event{
13  console.log(String(event.target));
14})
15.on('complete'function({
16  console.log('Fastest is ' + this.filter('fastest').map('name'));
17})
18// run async
19.run({ 'async'true });

第二个是常用的性能测试工具——ab(http://httpd.apache.org/docs/2.2/programs/ab.html),它是由大名鼎鼎的apache提供的网站性能测试工具。如果你使用的是红帽主机,可以使用以下命令进行安装:

1yum install httpd-tools

Mac下使用如下命令:

1brew install homebrew/apache/ab

ab的功能比较强大,不过一般来说使用如下命令来测试网站性能就可以了:

1ab -c 1000 -n 1000 http://localhost:3000/index.html

其中c是concurrency的缩写,表示一次并发的请求数量。n是numbers of request,表示在测试会话中所执行的请求个数。与之类似,loadtest也是一个比较好用的工具。

第三个方法就是使用node官方提供的性能检测功能,运行应用的时候输入如下代码:

1node --prof app.js

这行代码会在本地生成一个日志文件,名字就像这样:isolate-000001FD8971C200-v8.log。然后使用prof-process执行命令将该文件转换成有意义的性能信息:

1node --prof-process isolate-000001FD8971C200-v8.log > output.txt

然后就能在output.txt文件中看到对应的性能信息了。


1

网络优化


压缩

跟服务器性能相关的几个资源就是网络,CPU,内存和硬盘。我们先从网络的带宽开始说起。网络的带宽会影响API请求的接收,通常我们可以采用压缩资源的方式进行优化,从而更好地利用带宽。具体到实际的代码当中我们可以尝试使用一个叫做compression的中间件。

1const app = express()
2
3app.use(compression)


Keep-alive

频繁的建立TCP连接也是性能损耗的一大元凶。通过设置有效的持久连接,可以更好地利用同一个TCP连接。在API的响应头部启用Keep-alive值以及设置合适的持久连接超时时长就能达到这个目的:

1Object.assign(headers, {
2    'Connection''keep-alive',
3    'Keep-Alive''timeout=200'
4})

建立连接时使用连接池(connection pooling)也是一个不错的解决方案:

1const http = require('http');
2
3fetch(url, {
4    agentnew http.Agent({
5        keepAlivetrue,
6        maxSockets24
7    })
8})


1

主机优化

由于我们的应用都运行在Linux主机上,所以操作系统的性能也至关重要。

可以借助命令限制单个 server 程序所能使用的最大 socket 数,以供其他的 server 程序所使用。这样就能防止单个应用占用太多socket资源,从而导致其他应用无法正常工作。如下所示:

1ulimit -n 1024

有些Node应用可能对内存有更高要求,这时候你需要以类似于以下命令的方式启动应用,从而让其突破内存限制:

1node --max_old_space_size 4000 application


1

多实例


一个常用的Node应用的代码应该是长这个样子的:


 1const os = require('os');
2const express = require('express');
3const app = express();
4const port = process.env.PORT || 8080;
5
6const server = app.listen(port, () => {
7    console.log("Server is listening on port " + port);
8});
9
10app.get('/', (req, res, next) => {
11    // do something
12});

由于Node是单进程模型,所以它处理的请求数有所限制。不过为了最大化系统资源的调度,我们通常会采用多实例的方式启动Node服务器。借助官方提供的cluster模块可以很轻松地实现这个功能:

 1const os = require('os');
2const cluster = require('cluster');
3
4if (cluster.isMaster) {
5    // Master process
6    const numsOfCPU = os.cpus().length;
7
8    for(let i = 0; i < numsOfCPU; i++) {
9        cluster.fork();
10    }
11else {
12    const express = require('express');
13    const app = express();
14    const port = process.env.PORT || 8080;
15
16    const server = app.listen(port, () => {
17        console.log("Server is listening on port " + port);
18    });
19
20    app.get('/', (req, res, next) => {
21        // do something
22    });
23}

可以从代码看出,我们通过检测系统CPU内核数来启动多个实例,这样就可以有效地利用系统资源,从而提高性能。

针对特定的API,我们也可以另外起一个线程去处理复杂运算逻辑:

1app.get('/computation', (req, res, next) => {
2    const compute = fork('computation.js');
3    compute.send('start');
4    compute.on('message', (result) => {
5        res.send('Result is: ' + result);
6    });
7});


1

处理进程退出

在应用运行过程当中,可能会发现一些突发情况导致整个程序奔溃退出。这时候我们可以采用监听的模式,让这些已经退出的进程再重启,从而保证整个服务的可用性:

 1if (cluster.isMaster) {
2    // Master process
3    const numsOfCPU = os.cpus().length;
4
5    for(let i = 0; i < numsOfCPU; i++) {
6        cluster.fork();
7    }
8
9   cluster.on('exit', worker => {
10     console.log(`worker process ${process.pid} had exited`)
11     cluster.fork();
12   });
13


1

使用PM2

手动去写代码来控制进程实例的增减显然并不高效。这里介绍一个更实用的工具——PM2。借助它我们可以很容易地去管理我们的应用。

使用起来也很简单,首先我们先在本地安装pm2:

1npm install pm2 -g

启动应用的时候执行命令:

1pm2 start app.js --name app -i 3

-i 3表示的是启用3个实例。启动后可以利用命令pm2 list查看所有启动的实例列表。

如何打造高性能Node应用
image.png

然后我们试一下性能:

1loadtest -n 2000 http://localhost:8080

要使用监听模式的话,大家可以采用以下命令:

1pm2 monit


1

数据库优化

数据库优化在提高Node服务器性能上面也有很大的帮助。为了演示方便,我这里采用一个简易的持久化类库node-localstorage

1npm install node-localstorage

它的使用和浏览器端的localStorage十分类似,可以用来存储一些简单的对象:

1const {LocalStorage} = require('node-localstorage')
2const db = new LocalStorage('./data')
3
4db.setItem('test''hello world')
5db.getItem('test')

可以看到上面我使用了data.js这一个文件用来存储数据,而这种方式类比到数据库上如果在高并发场景下有很多的写操作的,势必会影响性能。
这时候我们考虑数据库的分片处理,比如A数据库处理a-m字段的数据,而B数据库m-z之间的数据。反应到代码上就像这样:

1const dbA = new LocalStorage('data-a-m')
2const dbB = new LocalStorage('data-m-z')
3
4const whichDB = name => name.match(/^[A-M]|^[a-m]/) ? dbA : dbB
5
6// use whichDB


1

微服务

第3个维度可以采用拆分微服务的方式来优化服务器性能。通过业务分析,我们通常将强耦合的一系列API都归为一个微服务,然后提供一个总的聚合入口,去聚合这些被拆分的微服务,微服务之间的通信采用网络连接进行。这样一来,能够更好地根据需要来对某些微服务进行扩容,比如说,订单微服务的用户访问量比较大,那么我们就可以给它多开几个实例。而管理用户的微服务使用量没那么大,那么我们就可以减少其对应实例的数量,这样按需分配资源可以得到最大化地兼顾成本和性能。

1

参考资料

https://www.ibm.com/developerworks/cn/linux/l-cn-ulimit/index.html

《Node.js High Performance》

https://nodejs.org/zh-cn/docs/guides/simple-profiling/

https://blog.xizhibei.me/2017/09/09/node-js-profiling-tool-flamegraph/


最后,欢迎大家关注我的公众号,一起学习交流。

如何打造高性能Node应用

微信扫描二维码,关注我的公众号


原文始发于微信公众号(Chason的成长之路):如何打造高性能Node应用