学习APM

127 阅读4分钟

关于 APM

APM 能帮企业以及应用开发者提供很多帮助,它的功能集中在监控、分析、优化上面,从应用中部署探针直接采集信息,集中处理,从多维度形成报告,简而言之:相当于给了一副看到应用程序不足的眼镜

几个选择

目前几个好用的,并且支持 Node.js 应用的:

  • NewRelic
  • OneAPM
  • Tingyun

从好用程度来说,NewRelic 秒杀全部,只是国内的 APM 其实也发展起来了,有着本地化的优势,能给国内企业一些本地化的解决方案,NewRelic 天高皇帝远,也是有着这方面的弊端的。

目前我们的服务器端监控用的 NewRelic,选择原因主要是强大、速度快、细节清楚。而手机上 APP 的 APM 选择的 Tingyun,因为它本地化优势很明显。

选择的时候不妨多试用比较,从自身需求出发去考察与筛选。

好了,非常简单的介绍了下,不难发现,对于 APM 来说,很重要的一点在于数据的采集,也就是需要部署探针去采集应用的各项数据,那么,探针是如何实现的?

Node.js 探针原理

说明细节之前,不妨来做个小题目:如何记录一个函数的运行时间?

不卖关子,很简单,写个函数,直接包装下即可:

function a() {...}

// 同步
function b() {
  const start = Date.now();
  a();
  const time = Date.now() - start();
}

// 异步
function b() {
  const start = Date.now();
  return a()
    .finally(() => {
      const time = Date.now() - start();
    })
}

现在你已经会做 APM 了,对的,不是开玩笑,原理即是如此。

当然了,实际做的时候,会有很多复杂的实现在那,比如,如何非侵入性地部署这个探针,以及如何在一次请求过程链中记录多个被调用函数的 metrics 等等。

众所周知,Node.js 由于它的异步原理,没有向有线程模型的同步语言中的**『线程本地存储』**,因此不是能很简单的不侵入代码,而做到记录每次请求的所有过程的,那么,现有的 APM 探针是如何做到的?

首先可以看看这个项目:async-listener

简单介绍下,就是把所有的 Node.js 基础模块中每个异步函数中 callback 参数做个包装,即针对以下这几个事件做成 hook,就相当于一个 async listener 了:

  • create: 进入了 Node.js 的事件队列;
  • before: 出了事件队列,马上要执行之前;
  • after: 执行完毕了;
  • error: 出错,执行的函数 throw error;

举个例子,比如 fs.writeFile(file, callback),包装之后:

fs.writeFile = function wrapWriteFile(file, callback) {
  asyncListener.create();
  process.addListener('uncaughtException', asyncListener.error);
  
  function callbackWrap(err, rst) {
    asyncListener.before();
    callback(err, rst);
    asyncListener.after();
  }
  fs.writeFile(file, (err, content) => {
    callbackWrap(err, content);
  });
}

于是每当一个异步函数执行时,对应的 hook 都会把相应的 metrics 记录到一个专门的 storage 中,而这个 storage 在整个调用过程中是共享的,所以最后就可以发送至专门的处理中心去了。

对了,利用这个原理,还可以做很多有意思的事情,比如 Continuation-Local Storage

P.S.1

到目前为止,async-listener 这个项目只是作为一个 monkey patch 的存在,官方至今没有实现,他们给出的解释也很简单:实现这个功能需要损失一定的性能,难度比较高。回到探针上面,其实探针也会让你的应用损失一定的性能,只是这个损失不会很大,而换来的效果收益确是远远高于这个损失的。

P.S.2

再扯一点,async listener 只是针对所有的 Node.js 基础模块做了包装,那么,它是如何在我们自己实现的模块中共享的呢?

这是基础知识点,留给你了。

提示:Node.js 异步的特性来自于何处?传染性是什么意思?

Background Transcation

上面提到的探针,都是直接帮你把每次 Web Transcation 记录下来,那么,如何记录一些离线任务?

基本上,如果官方不提供 wrap 的话,只能自己侵入性的部署探针了,接下来以 NewRelic 为例:

文档在 这里 ,一开始文档里面没有很清楚的使用方法说明,也没有发现其它网站上现成可直接参考的例子,最后我还是从它的源码中发现了 例子

首先是函数定义:

/*
 * 开始 Transcation
 * @param name Transcation name
 * @param [group] Transcation group name, default: NodeJs
 * @param handle 要被追踪的函数
 */
newrelic.createBackgroundTransaction(name, [group], handle)

// 结束 Transcation,一定需要再结束的时候调用,不然整个 Transcation 会超时
newrelic.endTransaction()

然后是以 Automattic/kue 为具体的例子:

const newrelic = require('newrelic');
const queue = require('kue').createQueue();

/*
 * @return Promise
 */
function aLongBgJob() {
  ...
}

const TRAN_NAME = 'example';
queue.process(TRAN_NAME, (job, done) => {
  const wrapedJob = newrelic.createBackgroundTransaction(TRAN_NAME, aLongBgJob);
  aLongBgJob(job.data)
  .then(function(result) {
    newrelic.endTransaction();
    done();
  })
  .catch(function(error) {
    newrelic.noticeError(error);
    newrelic.endTransaction();
    done();
  });
})

当然了,最好是针对这些任务队列框架做个中间件 wrap 适配,统一处理,这里只是举个例子。

另外,千万记得不可以这样:

aLongBgJob = newrelic.createBackgroundTransaction(TRAN_NAME, aLongBgJob);

这样做的话,aLongBgJob 会被反复 wrap ,如果调用次数多的话,相当于被包装了很多次,于是会导致调用栈溢出,最后拖垮整个应用,别问我怎么知道的🙈。