pplgin / blogs

分享资料整理

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

一次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 会出现如下信息:

Image of 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,数据正常了!结果如下:

Image

事情是解决了,但是疑问还没有结束,再回过头来看下上面的疑问,我对代码做了相应的修改如下:

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

到这里,问题已经很清楚了,所以这个中间件还是不推荐大家在生产环境使用。

结论总结

在选择开源中间件的时候,还是需要具体的针对业务。并不是完成功能实现就完成了,至少要关注整体的性能。