关于性能优化的考量应该贯穿整个产品的设计和实现过程中,特别是对于前期的基础架构的设计显得尤为重要,因为一旦应用的体系结构趋于稳定,在生产中再进行更改就特别困难。
性能分析
在具体实现性能优化之前,需要对系统的性能进行测试,才能知道系统的瓶颈和要优化的点在哪里。
这里推荐几个有用的测试工具:
第一个是benchmark(http://benchmarkjs.com/),主要用来做前端代码性能测试:
// 例子来自官方
var suite = new Benchmark.Suite;
// add tests
suite.add('RegExp#test', function() {
/o/.test('Hello World!');
})
.add('String#indexOf', function() {
'Hello World!'.indexOf('o') > -1;
})
// add listeners
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
// run async
.run({ 'async': true });
第二个是常用的性能测试工具——ab(http://httpd.apache.org/docs/2.2/programs/ab.html),它是由大名鼎鼎的apache提供的网站性能测试工具。如果你使用的是红帽主机,可以使用以下命令进行安装:
yum install httpd-tools
Mac下使用如下命令:
brew install homebrew/apache/ab
ab的功能比较强大,不过一般来说使用如下命令来测试网站性能就可以了:
ab -c 1000 -n 1000 http://localhost:3000/index.html
其中c是concurrency的缩写,表示一次并发的请求数量。n是numbers of request,表示在测试会话中所执行的请求个数。与之类似,loadtest也是一个比较好用的工具。
第三个方法就是使用node官方提供的性能检测功能,运行应用的时候输入如下代码:
node --prof app.js
这行代码会在本地生成一个日志文件,名字就像这样:isolate-000001FD8971C200-v8.log
。然后使用prof-process
执行命令将该文件转换成有意义的性能信息:
node --prof-process isolate-000001FD8971C200-v8.log > output.txt
然后就能在output.txt文件中看到对应的性能信息了。
常用优化策略
网络优化
压缩
跟服务器性能相关的几个资源就是网络CPU,内存和硬盘,我们先从网络的带宽开始说起。网络的带宽会影响API请求的接收,通常我们可以采用压缩资源的方式进行优化,从而更好地利用带宽。具体到实际的代码当中我们可以尝试使用一个叫做compression的中间件。
const app = express()
app.use(compression)
Keep-alive
频繁的建立TCP连接也是性能损耗的一大元凶。设置有效的持久连接,可以更好的利用同一个TCP连接。可以通过在API的响应头部启用Keep-alive
值以及设置合适的持久连接超时时长:
Object.assign(headers, {
'Connection': 'keep-alive',
'Keep-Alive': 'timeout=200'
})
建立连接时使用连接池(connection pooling)也是一个不错的解决方案:
const http = require('http');
fetch(url, {
agent: new http.Agent({
keepAlive: true,
maxSockets: 24
})
})
主机优化
由于我们的应用都运行在Linux主机上,所以操作系统的性能也至关重要。
可以借助命令限制单个 server 程序所能使用的最大 socket 数,以供其他的 server 程序所使用。防止单个应用占用太多socket资源,导致其他应用无法正常工作。如下所示:
ulimit -n 1024
对于有些Node应用可能对内存有更高要求,这时候你需要以类似于以下命令的方式启动具有更高限制的应用程序:
node --max_old_space_size 4000 application
多实例
一个常用的Node应用的代码应该是长这个样子的:
const os = require('os');
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
const server = app.listen(port, () => {
console.log("Server is listening on port " + port);
});
app.get('/', (req, res, next) => {
// do something
});
由于Node是单进程模型,所以它处理的请求数有所限制。不过为了最大化系统资源的调度,我们通常会采用多实例的方式启动Node服务器。借助官方提供的cluster模块可以很轻松地实现这个功能:
const os = require('os');
const cluster = require('cluster');
if (cluster.isMaster) {
// Master process
const numsOfCPU = os.cpus().length;
for(let i = 0; i < numsOfCPU; i++) {
cluster.fork();
}
} else {
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
const server = app.listen(port, () => {
console.log("Server is listening on port " + port);
});
app.get('/', (req, res, next) => {
// do something
});
}
可以从代码看出,我们通过检测系统CPU内核数来启动多个实例,这样就可以有效地利用系统资源,从而提高性能。
针对特定的API,我们也可以另外起一个线程去处理复杂运算逻辑:
app.get('/computation', (req, res, next) => {
const compute = fork('computation.js');
compute.send('start');
compute.on('message', (result) => {
res.send('Result is: ' + result);
});
});
处理进程退出
在应用运行过程当中,可能会发现一些突发情况导致整个程序奔溃退出。这时候我们可以采用监听的模式,让这些已经退出的进程再重启,从而保证整个服务的可用性:
if (cluster.isMaster) {
// Master process
const numsOfCPU = os.cpus().length;
for(let i = 0; i < numsOfCPU; i++) {
cluster.fork();
}
cluster.on('exit', worker => {
console.log(`worker process ${process.pid} had exited`)
cluster.fork();
});
}
使用PM2
手动去写代码来控制进程实例的增减显然并不高效。这里介绍一个更实用的工具——PM2。借助它我们可以很容易地去管理我们的应用。
使用起来也很简单,首先我们先在本地安装pm2:
npm install pm2 -g
启动应用的时候执行命令:
pm2 start app.js --name app -i 3
-i 3
表示的是启用3个实例。启动后可以利用命令pm2 list
查看所有启动的实例列表。
然后我们试一下性能:
loadtest -n 2000 http://localhost:8080
要使用监听模式的话,大家可以采用以下命令:
pm2 monit
更多用法大家可以参考 http://pm2.keymetrics.io/
数据库优化
数据库优化在提高Node服务器性能上面也有很大的帮助。为了演示方便,我这里采用一个简易的持久化类库node-localstorage
。
npm install node-localstorage
它的使用和浏览器端的localStorage
十分类似,可以用来存储一些简单的对象:
const {LocalStorage} = require('node-localstorage')
const db = new LocalStorage('./data')
db.setItem('test', 'hello world')
db.getItem('test')
可以看到上面我使用了data.js
这一个文件用来存储数据,而这种方式类比到数据库上如果在高并发场景下有很多的写操作的,势必会影响性能。
这时候我们考虑数据库的分片处理,比如A数据库处理a-m
字段的数据,而B数据库m-z
之间的数据。反应到代码上就像这样:
const dbA = new LocalStorage('data-a-m')
const dbB = new LocalStorage('data-m-z')
const whichDB = name => name.match(/^[A-M]|^[a-m]/) ? dbA : dbB
// use whichDB
微服务
第3个维度可以采用拆分微服务的方式来优化服务器性能。通过业务分析,我们通常将强耦合的一系列API都归为一个微服务,然后提供一个总的聚合入口,去聚合这些被拆分的微服务,微服务之间的通信采用网络连接进行。这样一来,能够更好地根据需要来对某些微服务进行扩容,比如说,订单微服务的用户访问量比较大,那么我们就可以给它多开几个实例。而管理用户的微服务使用量没那么大,那么我们就可以减少其对应实例的数量,这样按需分配资源可以得到最大化地兼顾成本和性能。
参考资料
《Node.js High Performance》
共同学习,写下你的评论
评论加载中...
作者其他优质文章