一次Node服务压测引发的思考
pplgin opened this issue · comments
事件还原
由于公司内部计划将前端node服务从 kvm切到docker,那么避免不了的就是要考虑 docker container的性能相关问题,从开发->jenkins编译->k8s部署一切看着很顺利,上线前一天准备开始压测,1Core 1G的2个容器配置QPS只有600-700(不稳定)平均下来一个容器只有300左右,这个数据对外肯定完蛋。
问题排查
上面的数据引发我们的思考主要考虑了一下几个方面:
- 网络问题(apigateway规则、是否有限流)
- 容器性能(配置过低)
- node代码本身的问题
猜测一(网络)
配合运维对比了apigateway修改前后的对比,结果在意料之中,规则么有大的变化(甩锅失败)
猜测二(容器配置)
查完了apigateway 直接看容器配置,是不是1Core1G太低了?好直接改成2Core1G, 再来一次结果数据上并么有明显的提升,然后直接到容器内看cpu和内存情况,发现cpu已经100%,内存还剩下很多(node端做的业务还比较单一)(又一次甩锅失败)
猜测三(代码问题)
首先说明下为什么排查过程不是先排查node端代码,因为node端代码一直在线上跑着性能杠杠的!就是由于太过自信而忽略的这次上docker的部分修改。
排查代码开始了,考虑到不能改下就直接发布 然后找测试压测,本地压测搞起!
工具准备:
- 压测工具wrk(or ab 个人觉得wrk比较简单好用) 安装wrk
- pm2 node进程管理(为了尽可能的和测试环境的一致,测试环境使用docker 使用的是pm2-runtime)
pm2
的使用估计大家都熟知了,这里简单的介绍下wrk
的使用,当然也可以看文档(推荐),首先我们通过 wrk
会出现如下信息:
知道上面的参数后,就简单了代码如下:
// 10个线程 200个链接数 压测10s
wrk -t10 -c200 -d10s http://127.0.0.1:4100
准备好了压测工具后,开始分析代码,这次的变动改了哪些,然后直接定位到了 koa-view,由于历史写的node代码render中间件太重,想了想就直接用koa-views
, 由于我们内部使用的是nunjucks
,然后配置如下:
...
app.use(views(path.resolve(__dirname, '../views'), {
map: {
html: 'nunjucks'
},
options: {
nunjucks: {
loader: path.resolve(__dirname, '../views')
}
}
}))
...
整体看着没有问题,本地开压、数据果然和测试环境一样上不上去、修改代码 直接去掉该中间件、数据瞬间上去直接上到了2k(包含了其他的一些中间件逻辑基本和线上kvm单个实例数据差不多), 开始排查koa-views
源码逻辑
//直接看最核心部分 engineSource = consolidate
...
const engineName = map && map[suffix] ? map[suffix] : suffix
const render = engineSource[engineName]
if (!engineName || !render)
return Promise.reject(
new Error(`Engine not found for the ".${suffix}" file extension`)
)
return render(resolve(path, paths.rel), state).then(html => {
// since pug has deprecated `pretty` option
// we'll use the `pretty` package in the meanwhile
if (locals.pretty) {
debug('using `pretty` package to beautify HTML')
html = pretty(html)
}
ctx.body = html
})
有一个疑问?为什么要每次都要去匹配模板引擎,接下来去找对应的render函数
...
/**
* Nunjucks string support.
*/
exports.nunjucks.render = function(str, options, cb) {
...
if (typeof options.nunjucks.loader === 'string') {
env = new engine.Environment(new engine.FileSystemLoader(options.nunjucks.loader));
}
...
}
发现上述代码,也就是说 从我上面koa-views的配置会走到上述代码中,每次请求都创建一个新的对象,这里是我的第二个疑问?
本着先解决问题的态度,直接重写了这个render中间件
const nunjucks = require('nunjucks')
/**
* nunjucks render
* @param {[type]} rootPath [view文件根路径]
*/
const render = rootPath => {
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(rootPath))
return (viewName, data) => {
return new Promise((resolve, reject) => {
env.render(viewName + '.html', data, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
}
}
module.exports = ({ viewRoot }) => {
const renderHtml = render(viewRoot)
return (ctx, next) => {
if (ctx.render) {
return next()
}
ctx.render = (viewName, data = {}) => {
return renderHtml(viewName, Object.assign(ctx.state, data)).then(htmlString => {
ctx.body = htmlString
})
}
return next()
}
}
再次运行压测5s,数据正常了!结果如下:
事情是解决了,但是疑问还没有结束,再回过头来看下上面的疑问,我对代码做了相应的修改如下:
ctx.response.render = ctx.render = function(relPath, locals = {}) {
const stime = new Date().getTime()
return getPaths(path, relPath, extension).then(paths => {
....
return render(resolve(path, paths.rel), state).then(html => {
const etime = new Date().getTime()
console.log(`cost time: ${etime-stime}`)
...
ctx.body = html
})
}
})
}
再次执行wrk
压测不出所料,从getPath,到render 平稳下来每次大概消耗:
cost time: 440~500 ms
到这里,问题已经很清楚了,所以这个中间件还是不推荐大家在生产环境使用。
结论总结
在选择开源中间件的时候,还是需要具体的针对业务。并不是完成功能实现就完成了,至少要关注整体的性能。