const crypto = require('crypto') const Paloma = require('paloma') const app = new Paloma() const users = {} app.route({ method: 'GET', path: '/newUser', controller(ctx) { const username = ctx.query.username || 'test' const password = ctx.query.password || 'test' const salt = crypto.randomBytes(128).toString('base64') const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex') users[username] = { salt, ctx.status = 204 app.route({ method: 'GET', path: '/auth', controller(ctx) { const username = ctx.query.username || 'test' const password = ctx.query.password || 'test' if (!users[username]) { throw (400) const hash = crypto.pbkdf2Sync(password, users[username].salt, 10000, 64, 'sha512').toString('hex') if (users[username].hash === hash) { ctx.status = 204 } else { throw (403) }) app.listen(3000)

2. 通过 perf 参数运行 node.js 程序

$ node --perf_basic_prof app.js &
[1] 3590
$ tail /tmp/perf-3590.map
51b87a7b93e 18 Function:~emitListeningNT net.js:1375
51b87a7b93e 18 LazyCompile:~emitListeningNT net.js:1375
51b87a7bad6 39 Function:~emitAfterScript async_hooks.js:443
51b87a7bad6 39 LazyCompile:~emitAfterScript async_hooks.js:443
51b87a7bcbe 77 Function:~tickDone internal/process/next_tick.js:88
51b87a7bcbe 77 LazyCompile:~tickDone internal/process/next_tick.js:88
51b87a7bf36 12 Function:~clear internal/process/next_tick.js:42
51b87a7bf36 12 LazyCompile:~clear internal/process/next_tick.js:42
51b87a7c126 b8 Function:~emitPendingUnhandledRejections internal/process/promises.js:86
51b87a7c126 b8 LazyCompile:~emitPendingUnhandledRejections internal/process/promises.js:86

注意:使用 —perf_basic_prof_only_functions 参数可以将代码执行时的符号表转换为人能看懂的函数名

3. ab 压测

$ curl "<http://localhost:3000/newUser?username=admin&password=123456>"
$ ab -k -c 10 -n 2000 "<http://localhost:3000/auth?username=admin&password=123456>"

4. 抓取数据绘制火焰图

$ sudo perf record -F 99 -p 28671 -g -- sleep 30
$ sudo chown root /tmp/perf-28671.map
$ sudo perf script > perf.stacks
	$ ./stackcollapse-perf.pl --kernel < perf.stacks | ./flamegraph.pl --color=js --hash > flamegrap1.svg

perf record 会将记录的信息保存到当前执行目录的 perf.data 文件,使用 perf script 读取 perf.data 的 trace 信息写入 perf.stacks, 然后通过FlameGraph 绘制火焰图。

—color=js 指定生成针对 js 配色的 svg,即:

green:JavaScript。 blue:Builtin。 yellow:C++。 red:System(native user-level, and kernel)。

ab 压测用了 30s 左右,浏览器打开 flamegraph.svg,截取关键的部分如下图所示:

从上图可以看出:最上面的绿色小块(即 JavaScript 代码)指向 test/app.js 第 18 行,即 GET /auth 这个路由。再往上看,黄色的小块(即 C++ 代码) node::crypto::PBKDF2 占用了大量的 CPU 时间。

解决方法:将同步改为异步,即将 crypto.pbkdf2Sync 改为 crypto.pbkdf2,修改如下:

const crypto = require('crypto') 
const Paloma = require('paloma')
const app = new Paloma() const users = {}
app.route({
    method: 'GET',
    path: '/newUser',
    controller(ctx) {
        const username = ctx.query.username || 'test'
        const password = ctx.query.password || 'test'
        const salt = crypto.randomBytes(128).toString('base64') 
        const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex') 
        users[username] = {
            salt,
        ctx.status = 204
app.route({
	method: 'GET',
	path: '/auth',
	async controller(ctx) {
		const username = ctx.query.username || 'test'const password = ctx.query.password || 'test'
		if (!users[username]) {
			throw (400)
		const hash = await new Promise((resolve, reject) = >{
			crypto.pbkdf2(password, users[username].salt, 10000, 64, 'sha512', (err, derivedKey) = >{
				if (err) {
					return reject(err)
				resolve(derivedKey.toString('hex'))
		}) if (users[username].hash === hash) {
			ctx.status = 204
		} else {
			throw (403)
app.listen(3000)
复制代码
  • 私信