Node.js APM 调研及原理分析

个人介绍



  • Node.js工程师 @微医集团-消费事业群

  • GitHub @Claude-Ray

APM


Application Performance Management

监控服务的一套技术手段,致力于监控并管理程序的性能和可用性

APM Coneptual Framework

APM的定义


  • 终端用户体验
  • 应用架构映射
  • 应用事务分析
  • 深度应用诊断
  • 分析与报告

2016 年, Gartner 又将上述 5 个维度更新为 3 个新维度。


  • 数字体验监控 (DEM)
  • 应用发现、跟踪、诊断 (ADTD)
  • 应用分析 (AA)

市场调研-商业软件

New Relic


Node.js APM 产业的龙头,虽然监控服务需要付费,数据上传到云端才能使用,但其 Agent 源代码完全开放。

付费用户的首选。

AppDynamics


专攻企业级服务,支持部署在公司内部数据中心。

其 Agent 代码不完全开放,使用了经过编译的 jar 包和二进制文件。

Dynatrace


在 Node.js 的市场占有率和热度较低,不开放源代码,无法深度化定制。

Atatus


支持功能一般,SDK 做了代码混淆。

听云


部分功能(代码)借鉴自 New Relic,针对国内市场做了一些本地化。

OneAPM


定位类似听云,支持私有部署。

市场调研-开源/免费软件

Alinode


Runtime? 数据安全?


争议较大...

Alinode


Alinode 对 Node Runtime 增加了哪些改动?引自hyj1991知乎回答

• 增加了一些 V8 没有对外暴露的接口,比如 GC Trace 来动态输出 GC 日志

• 埋了一些点以性能损耗更低的方式采集进程级别的 CPU 和 Memory 数据

• 增加了动态开启 CPU / Memory / GC 状态采集的开关

对于负责开发者业务相关的 API 和功能逻辑,并无任何改动,AliNode 和官方的 Runtime 可以无缝对切的原因。

Alinode


安全问题

主要是担心会采集业务数据上报,但是实际上 AliNode 内核的上述改动,都不会直接向云端发送任何数据,而都是以本地 Log 的方式写入大家配置的 NODE_LOG_DIR 目录下,日志文件以 node-日志.log 的形式命名。

控制台看到的数据均是通过 agentx 这个库采集上报的,这个库是开源的。

Easy Monitor


hyj1991 的作品,功能相对简单,仅供性能监控与分析。目前维护度较低,适合作为学习项目。

Pandora.js


来自淘宝 midwayjs 团队的进程启动器,阿里内部已经落地,正在重构 2.0 版本。

目前不支持平滑重启,Dashboard 只能单机部署单机监控,无法集群监控,midway 的使用方案是结合 ElasticSearch。

Prometheus


在国外非常流行,更多地被 k8s 圈熟知,是一种监控和报警的开源生态。Agent 和界面有多重组合方式,Node.js 一般结合 prom-client(非官方 npm 包) + Granfana 使用。

只做性能采集,不支持 trace 跟踪。目前已知缺陷是内存占用较高和日志量巨大,数据可以选择本地存储或远程接口存储。

Elastic APM


Elastic 体系下的完全开源的 APM 解决方案,也提供商业付费服务。

选型概述

主要维度





  • • 性能监控

  • • 代码级监控

  • • 事务监控

  • • 框架支持

  • • 链路追踪

  • • 分布式部署

  • • 代码侵入

  • • 社区活跃度

  • • 数据安全

  • • 外部依赖

名称ExpressKoa性能代码级事务链路分布式侵入实现方式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

  • ...

数据上报





Data Reporting

目录结构


  • 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.tracenew 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;
};

踩过的坑



  Error Trace

  SSR Router

  Egg.js

dashboard

未来工作



  本地化

  性能优化

  ...

Q&A


qrcode

Thank you!