Node.js APM 调研及原理分析
个人介绍
Node.js工程师 @微医集团-消费事业群
GitHub @Claude-Ray

APM的定义
- 终端用户体验
- 应用架构映射
- 应用事务分析
- 深度应用诊断
- 分析与报告
2016 年, Gartner 又将上述 5 个维度更新为 3 个新维度。
- 数字体验监控 (DEM)
- 应用发现、跟踪、诊断 (ADTD)
- 应用分析 (AA)
市场调研-商业软件
市场调研-开源/免费软件
Alinode
安全问题
主要是担心会采集业务数据上报,但是实际上 AliNode 内核的上述改动,都不会直接向云端发送任何数据,而都是以本地 Log 的方式写入大家配置的 NODE_LOG_DIR 目录下,日志文件以 node-日志.log 的形式命名。
控制台看到的数据均是通过 agentx 这个库采集上报的,这个库是开源的。
Pandora.js
来自淘宝 midwayjs 团队的进程启动器,阿里内部已经落地,正在重构 2.0 版本。
目前不支持平滑重启,Dashboard 只能单机部署单机监控,无法集群监控,midway 的使用方案是结合 ElasticSearch。
Prometheus
在国外非常流行,更多地被 k8s 圈熟知,是一种监控和报警的开源生态。Agent 和界面有多重组合方式,Node.js 一般结合 prom-client(非官方 npm 包) + Granfana 使用。
只做性能采集,不支持 trace 跟踪。目前已知缺陷是内存占用较高和日志量巨大,数据可以选择本地存储或远程接口存储。
选型概述
主要维度
• 性能监控
• 代码级监控
• 事务监控
• 框架支持
• 链路追踪
• 分布式部署
• 代码侵入
• 社区活跃度
• 数据安全
• 外部依赖
| 名称 | Express | Koa | 性能 | 代码级 | 事务 | 链路 | 分布式 | 侵入 | 实现方式 | npm周下载量(+) |
|---|---|---|---|---|---|---|---|---|---|---|
| NewRelic | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | 低 | 探针 | 32.2k |
| AppDynamics | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | 低 | 探针 | 9.5k |
| Dynatrace | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | 低 | 探针 | 0.2k |
| Atatus | ⭕️️ | ⭕️️ | ⭕️️ | ❎️ | ⭕️️ | ⭕️️ | ⭕️️ | 低 | 探针 | 0.6k |
| Tingyun | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | 低 | 探针 | 0.1k |
| OneAPM | ⭕️️ | ❎️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | 低 | 探针 | 0.2k |
| AliNode | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | 无 | 运行时 | - |
| Easy Monitor | ⭕️️ | ⭕️️ | ⭕️️ | ❎️ | ❎️ | ❎️ | ⭕️️ | 低 | 探针 | 0.2k |
| Pandora | ⭕️️ | ⭕️️ | ⭕️️ | ❎️ | ⭕️️ | ⭕️️ | ❎️ | 极低 | 进程启动器 | 0.7k |
| Prometheus | ⭕️️ | ⭕️️ | ⭕️️ | ❎️ | ⭕️️ | ⭕️️ | ⭕️️ | 低 | 探针 | 22.4k |
| Elastic APM | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | ⭕️️ | 低 | 探针 | 27.6k |
Elastic APM
项目背景
2011 年 11 月开工,至今基本都是单人维护的状态。
两任作者:
Matt Robenolt, @Sentry core member
Thomas Watson, @Elastic Node.js dev, @Node.js core member
文档建设
- APM: https://www.elastic.co/guide/en/apm/get-started/current/index.html
- Node Agent: https://www.elastic.co/guide/en/apm/agent/nodejs/current/index.html
- Kibana APM: https://www.elastic.co/guide/en/kibana/current/xpack-apm.html
一如 Elastic 其他技术栈,官方文档是相当细致了,使用前推荐阅读一遍。
基本功能
自定义 Node.js 框架和路由。
上报错误 stack,支持 source map 。
支持采集 http 请求的 body 参数(默认关闭)。
过滤敏感信息,根据请求头、或自定义维度。
定制上报 Transaction、Span、额外的 Custom 数据。
性能优化:调整采样率、上报频率、请求体的限制。
opentracing
kubernetes
数据上报

目录结构
lib
filters
instrumentation
- module
metrics
- platform
middleware
核心功能
Error
Metric
Transaction
Error
Error
var formatter = require('./lib/node-0.10-formatter')
var orig = Error.prepareStackTrace
Error.prepareStackTrace = function (err, callsites) {
Object.defineProperty(err, '__error_callsites', {
enumerable: false,
configurable: true,
writable: false,
value: callsites
})
return (orig || formatter)(err, callsites)
}
module.exports = function (err) {
err.stack
return err.__error_callsites
}
Error
Error.prepareStackTrace(error, structuredStackTrace)
这个接口常常被用来格式化错误信息,structuredStackTrace 包含了一组 CallSite 对象,其支持的方法有:
getThis, getTypeName, getFunction, getFunctionName, getMethodName, getFileName, getLineNumber, getColumnNumber, getEvalOrigin, isToplevel, isEval, isNative, isConstructor, isAsync, isPromiseAll, getPromiseIndex
借此记录 Error 抛出的文件、行列等坐标信息。
Metric
Metric 概述
Node.js 原生接口足够应对基本的性能监控,但需要一些加工:
纯 JS 计算。
C++ 计算。一般还会获取更复杂的指标,如 appmetrics 会获取一部分 GC、Event loop 信息。
Elastic Metric
/proc/meminfo:os.totalmem(), os.freemem()
/proc/stat:times.total, times.idle [os.cpus()]
/proc/self/stat:process.memoryUsage().rss, process.cpuUsage([previousValue]), process.hrtime([time])
Transaction
Transaction 概述
Elastic APM 中的事务,类似于 opentracing 中的 Span,但把一个请求中所有的 Span 抽象为一个概念。Transaction 实现的基础是各种代码钩子。
addPatch
module.exports = function (koa, agent, { version, enabled }) {
if (!enabled) return koa
agent.setFramework({ name: 'koa', version, overwrite: false })
return koa
}
// koa-router
// 配合 require-in-the-middle 模块食用
shimmer.wrap(Router.prototype, 'match', function (orig) {
return function (_, method) {
var matched = orig.apply(this, arguments)
if (typeof method !== 'string') {
agent.logger.debug('unexpected method type in koa-router prototype.match: %s', typeof method)
return matched
}
if (Array.isArray(matched && matched.pathAndMethod)) {
const layer = matched.pathAndMethod.find(function (layer) {
return layer && layer.opts && layer.opts.end === true
})
var path = layer && layer.path
if (typeof path === 'string') {
var name = method + ' ' + path
agent._instrumentation.setDefaultTransactionName(name)
} else {
agent.logger.debug('unexpected path type in koa-router prototype.match: %s', typeof path)
}
} else {
agent.logger.debug('unexpected match result in koa-router prototype.match: %s', typeof matched)
}
return matched
}
})
Async Hooks
// 基于 async_hooks 封装了 Instrumentation 的 `currentTransaction` 方法
// 使异步操作中随时可以拿到当前 async scope id 下的 Transaction 实例。
const asyncHooks = require('async_hooks')
module.exports = function (ins) {
const asyncHook = asyncHooks.createHook({ init, before, destroy })
const contexts = new WeakMap()
const activeTransactions = new Map()
Object.defineProperty(ins, 'currentTransaction', {
get () {
const asyncId = asyncHooks.executionAsyncId()
return activeTransactions.get(asyncId) || null
},
set (trans) {
const asyncId = asyncHooks.executionAsyncId()
if (trans) {
activeTransactions.set(asyncId, trans)
} else {
activeTransactions.delete(asyncId)
}
}
})
// ...
}
Async Hooks
// 下面是 currentTransaction 的一处应用
Instrumentation.prototype.bindFunction = function (original) {
if (typeof original !== 'function' || original.name === 'elasticAPMCallbackWrapper') return original
var ins = this
var trans = this.currentTransaction
var span = this.currentSpan
if (trans && !trans.sampled) {
return original
}
return elasticAPMCallbackWrapper
function elasticAPMCallbackWrapper () {
var prevTrans = ins.currentTransaction
ins.currentTransaction = trans
ins.bindingSpan = null
ins.activeSpan = span
if (trans) trans.sync = false
if (span) span.sync = false
var result = original.apply(this, arguments)
ins.currentTransaction = prevTrans
return result
}
}
Async Hooks
Async Hooks 是 Node.js 8 以后出现的概念,为了兼容旧版本,Elastic APM 借助
async-listener模块做了一些兼容,尽管 Elastic APM 官方不推荐使用低版本 Node.js 接入。虽然 async hook 更进一步可以帮助优化异步调用栈,改善异步 Error 信息的可读性,但 APM 很难从底层判断哪些异步 CallSite 是用户想保留的,所以没有做这种处理。
Span
事务拆解而得,形成调用链
SQL、NoSQL 数据库查询
外部 HTTP、Socket 请求
自定义 Span
Stack Trace
console.trace、new ErrorError.captureStackTrace(error, constructorOpt)
function MyError() {
Error.captureStackTrace(this, MyError);
// Any other initialization goes here.
}
小插曲
结合 TJ 的 callsite 理解 V8 Error trace API
module.exports = function(){
var orig = Error.prepareStackTrace;
Error.prepareStackTrace = function(_, stack){ return stack; };
var err = new Error;
Error.captureStackTrace(err, arguments.callee);
var stack = err.stack;
Error.prepareStackTrace = orig;
return stack;
};
踩过的坑
Error Trace
SSR Router
Egg.js

未来工作
本地化
性能优化
...
Q&A
