话很少说,先上图,这是获得App静态资源更新服务的CPU使用率监控,能够看到7月2号到7月3号后,cpu使用率发生了爆涨,在八点的早高峰和下午六点的晚高峰,几乎能够把cpu打满。发现问题当机立断,升级配置将2核4g升级至4核8g,先保证服务稳定,咱们再继续查问题。javascript
下图是升级配置后的截图,因此看到的图已经温柔不少了,本人当时看到监控的时候,全部波峰都是打在红线以上的,虽然尚未引发报警,可是默默掏出小本本记下找时间查问题。html
由于有很明显的发生变化的时间点,直接能找到这一次的改动,通过一点点的代码级review,并无发现变更的代码上有什么问题。做为一个小前端没遇到过这种问题呀,毫无头绪的我,把救世主锁定在了火焰图身上,想看一看到底什么地方耗时长到底cpu占用在了什么东西上。前端
因而怎么生成火焰图成了我最大的难题,开始Google搜索,“如何生成火焰图” ,“node 火焰图”,“node cpu profiler”, “node flamegraph”。看来看去全部文章千篇一概,95%以上的文章都是以下解决方案。java
参考文章:nodejs调试指南 perf + FlameGraphnode
Linux自带的系统性能分析工具,一堆功能我就很少说了,有兴趣的本身去看nodejs调试指南打开书的第一页。由于使用的局限性不是Linux的我,第一步apt install linux-tools-common都安不上,若是还要跑在虚拟机什么的上面是否是太麻烦了,方案一卒。linux
参考文章:易于分析的 Node.js 应用程序 | Node.jsgit
Node.js4.4.0开始,node自己就能够记录进程中V8引擎的性能信息(profiler),只须要在启动的时候加上参数--prof。 Node自带的分析工具:github
直接查到应用的pid直接对pid进行收集,而后也能够将收集到的数据制成火焰图,具体操做就不作赘述了,最后跑出来的图以下, chrome
好了以上就是我Google出来的各类方案在我一一踩坑后所有以失败了结,其实也还有一些更简单的方案,例如直接接入alinode用阿里云的平台就好,一方面该项目没有接入阿里云,恰好用的node镜像又不是ali的,另外一方面,若是能够在开发环境查出问题,不但愿再经过上线去查问题。npm
Node.js 是基于 V8 引擎的,V8 暴露了一些 profiler API,咱们能够经过 v8-profiler 收集一些运行时的CPU和内存数据。 在安装v8-profiler的时候遇到了一些问题老是安装失败,而且得不到解决。不过好在有大神基于v8-profiler发布了v8-profiler-node8,下面是v8-profiler-node8的一段描述。
Based on v8-profiler-node8@5.7.0, Solved the v8-profiler segment fault error in node-v8.x.
v8-profiler-node8 provides node bindings for the v8 profiler and integration with node-inspector
收集数据: 简单的npm install v8-profiler-node8后,开始收集CPU profile,收集时长五分钟。
const profiler = require('v8-profiler-node8');
const fs = require('fs');
const Bluebird = require('bluebird');
class PackageController extends Controller {
async cpuProf() {
console.log('开始收集');
// Start Profiling
profiler.startProfiling('CPU profile');
await Bluebird.delay(60000 * 5);
const profile = profiler.stopProfiling();
profile.export()
.pipe(fs.createWriteStream(`cpuprofile-${Date.now()}.cpuprofile`))
.on('finish', () => profile.delete());
this.ctx.status = 204;
}
}
复制代码
而后当即用ab压测,给服务压力,
ab -t 300 -c 10 -p post.txt -T "application/json" http://localhost:7001/xxx/xxxxx/xxxxxx/xxxxx
复制代码
收集完成后,获得一个cpuProfile文件,Chrome 自带了分析 CPU profile 日志的工具。打开 Chrome -> 调出开发者工具(DevTools) -> 单击右上角三个点的按钮 -> More tools -> JavaScript Profiler -> Load,加载刚才生成的 cpuprofile 文件。能够直接用chrome的性能分析直接读这个文件打开分析。 这里我要推荐一下 speedscope 一个根据cpuProfile生成火焰图的工具,他生成的火焰图,更清晰,还有leftHeavy模式,直接将CPU占用率最高的排在最左边,一目了然,快速的能够定位到问题。
下面是该火焰图的leftHeavy模式
看火焰图的时候越图形越尖说明越正常,横条越长说明占用时间越长,从图中能够看到压测的五分钟里,CPU占用时间长达两分钟,其中绝大多数被红框中占据,来张大图
/*! * hydrates many documents * * @param {Model} model * @param {Array} docs * @param {Object} fields * @param {Query} self * @param {Array} [pop] array of paths used in population * @param {Function} callback */
function completeMany(model, docs, fields, userProvidedFields, pop, callback) {
var arr = [];
var count = docs.length;
var len = count;
var opts = pop ? { populated: pop } : undefined;
var error = null;
function init(_error) {
if (error != null) {
return;
}
if (_error != null) {
error = _error;
return callback(error);
}
--count || callback(null, arr);
}
for (var i = 0; i < len; ++i) {
arr[i] = helpers.createModel(model, docs[i], fields, userProvidedFields);
arr[i].init(docs[i], opts, init);
}
}
复制代码
completeMany方法会将传入的每个docs经过 helpers.createModel变成一个mongoose Document,咱们再来看一下是哪里调用的completeMany方法,发如今find方法中会判断options.lean是否等于true若是不等于true才会去调用completeMany方法去包装查询结果。
/** * Thunk around find() * * @param {Function} [callback] * @return {Query} this * @api private */
Query.prototype._find = function(callback) {
this._castConditions();
if (this.error() != null) {
callback(this.error());
return this;
}
this._applyPaths();
this._fields = this._castFields(this._fields);
var fields = this._fieldsForExec();
var options = this._mongooseOptions;
var _this = this;
var userProvidedFields = _this._userProvidedFields || {};
var cb = function(err, docs) {
if (err) {
return callback(err);
}
if (docs.length === 0) {
return callback(null, docs);
}
if (!options.populate) {
// 看这里 重点重点!
return !!options.lean === true
? callback(null, docs)
: completeMany(_this.model, docs, fields, userProvidedFields, null, callback);
}
var pop = helpers.preparePopulationOptionsMQ(_this, options);
pop.__noPromise = true;
_this.model.populate(docs, pop, function(err, docs) {
if (err) return callback(err);
return !!options.lean === true
? callback(null, docs)
: completeMany(_this.model, docs, fields, userProvidedFields, pop, callback);
});
};
return Query.base.find.call(this, {}, cb);
};
复制代码
去文档上搜一下lean mongoose query lean 文档上说了若是使用了lean那么查询返回的将是一个javascript objects, not Mongoose Documents 。原话以下。
Documents returned from queries with theleanoption enabled are plain javascript objects, not Mongoose Documents . They have nosavemethod, getters/setters, virtuals, or other Mongoose features.
在文档中还提到了,lean精简模式,对于高性能只读的状况是很是有用的。
回到问题上来,看到mongoose Document的问题,7月2号到7月3号后,为何会忽然致使CPU暴涨恍然大悟,本身以前review 代码,看着代码没问题,可是忽略了这一个版本由于业务调整致使查询压力大大增长,多是过去的好几倍这个问题。随即将查询改为精简模式。只须要以下很简单的几处修改便可。
await model.Package.find(query).lean();
复制代码
那说到频繁的处理mongoose Document致使的性能问题,那其实还有一个优化点能够作,其实在查询的时候多多使用find的第二个参数projection去投影所须要返回的键,须要用什么就投影什么,不要一股脑把全部的键值一块儿返回了。处理完这一系列,重写在本地进行了一次一样的压测五分钟,出了一份火焰图,下面图1就是这五分钟期间的火焰图,图二就是通过speedscope解析事后的leftHeavy图,直接观察重灾区。
从图一的火焰图中,并不能看出明显的区别,可是一看到图二就知道咱们的优化是有效果的,从最直观的,本来左侧红框中completeMany的部分直接没有了,而后cpu占用的总时长也由本来的接近两分钟直接降到了36s,优化效果仍是十分明显的。上线观察几天看看效果
谨记。