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 Error
Error.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;
};