Node.js APM实现分析

APM(Application Performance Management),中文名称应用性能管理。百度百科上给了比较系统的介绍,有兴趣的同学可以查看:应用性能管理;知乎上也有对这个方向的简单探讨,链接:APM(应用性能管理)在中国前景如何?。
本文从听云的Nodejs监控源码入手,探索其实现的的一些原理和细节。源码可以在听云官网上找到。

整体结构

从下图可以看到整个源码结构。metrics主要处理性能指标,负责把收集到的指标转成服务器识别的格式;serve负责服务器的连接建立、数据传送等和服务器交互的逻辑;options是处理配置信息,如服务器的地址、用户的key等;util是方法集合,如日志处理等,它的功能比较复杂;parsers是程序的核心,对用户使用到的框架或者API进行封装处理。


Node.js APM实现分析_第1张图片
程序框架.png
启动

index.js中给出来程序的启动流程:

var init = function init() {
    ...
    // 1. 初始化配置信息
    var config = require('./options/config.js').init();
    var Agent = require('./agent.js');

    // 2. 根据配置信息生成agent对象 agent对象会携带性能相关的信息
    agent = new Agent(config);

    // 3. shimmer提供函数的封装模块
    var shimmer = require('./util/shimmer.js');
    // 对Node中的加载做一层封装
    shimmer.patchModule(agent);
    // 对http进行封装
    shimmer.bootstrapInstrumentation(agent);

    // 收集应用基本信息 如物理内存使用情况、事件队列排队
    // 启动定时器  50s间隔发送运行参数
    return agent.start();
}
var start_message = init();
运行

启动流程中,shimmer. patchModule会对Node模块加载的_load方法进行了处理:

patchModule : function patchModule(agent) {
    logger.debug("Wrapping module loader.");
    var Module = require('module');

    shimmer.wrapMethod(Module, 'Module', '_load', function cb_wrapMethod(load) {
      return function cls_wrapMethod(file) {
        return _postLoad(agent, load.apply(this, arguments), file);
      };
    });
  }

Node在模块加载时都会调用到Module的_load方法。当require一个模块时,程序会根据模块的名字决定加载执行哪一个封装逻辑;如果没有封装逻辑,那么直接执行原模块:

function _postLoad(agent, nodule, name) {
    var base = path.basename(name);
    // 原生express的base为express,封装的parsers/wrappers/express.js,其base为express.js。 依据此避免了循环依赖
    var wrapper_module = (name === 'pg.js') ? 'pg': base;
    //  WRAPPERS是由express、redis、mysql等模块名组成的数组
    if (WRAPPERS.indexOf(wrapper_module) !== -1) {
        logger.debug('wrap %s.', base);
        var filename = path.join(__dirname, '../parsers/wrappers', wrapper_module + '.js');
        instrument(agent, base, filename, nodule);
    }
    if (FUN_WRAPPERS.indexOf(wrapper_module) !== -1) {
        logger.debug('wrap %s.', base);
        var filename = path.join(__dirname, '../parsers/wrappers', wrapper_module + '.js');
        return retInstrument(agent, base, filename, nodule);
    }
    return nodule;
}

instrument函数的逻辑就是把封装模块加载执行:

function instrument(agent, shortName, fileName, nodule, param) {
  try {
    require(fileName)(agent, nodule, param);
  }
  catch (error) {
    logger.verbose(error, "wrap module %s failed.",  path.basename(shortName, ".js"));
  }
}

Express探针

其对Express的封装逻辑基本包括以下几个步骤:1. 判断应用程序中的Express版本; 2. 依据版本执行封装逻辑。 以Express4.0为例,程序对下面几个方法做了封装处理:

shimmer.wrapMethodOnce(express.application, 'express.application', 'init', app_init);
shimmer.wrapMethodOnce(express.response, 'express.response', 'render', wrapRender.bind(null, 4));
shimmer.wrapMethodOnce(express.Router, 'express.Router', 'process_params', wrapProcessParams.bind(null, 4));
shimmer.wrapMethodOnce(express.Router, 'express.Router', 'use', wrapMiddlewareStack.bind(null, 'use'));
shimmer.wrapMethodOnce(express.Router, 'express.Router', 'route', wrapMiddlewareStack.bind(null, 'route'));

wrapMethodOnce的逻辑如下,主要是对原有的方法做一层封装:

wrapMethodOnce : function wrapMethodOnce(nodule, noduleName, method, wrapper ) {
    if (!noduleName) noduleName = '[unknown]';
    var method_name = noduleName + '.' + method;
    var original = nodule[method];
    if (!original) {
      return logger.debug("%s not defined, skip wrapping.", method_name);
    }
    if ( original.__TY_unwrap ) return;
    var wrapped = wrapper(original);
    wrapped.__TY_original = original;
    wrapped.__TY_unwrap = function __TY_unwrap() {
      nodule[method] = original;
      logger.debug("Removed instrumentation from %s.", method_name);
    };

    nodule[method] = wrapped;
    if (shimmer.debug) instrumented.push(wrapped);
  }

以wrapRender为例,它在原来的方法上的基础上了增加了指标收集的逻辑:

function wrapRender(version, render) {
        return function wp_Render(view, options, cb, parent, sub) {
            if ( ! agent.config.enabled ) return render.apply(this, arguments);
            if (!tracer.getAction()) return render.apply(this, arguments);
            var classname = (version < 3)?'http.ServerResponse':'express.response';
//            var name = "Express/" + view.replace(/\//g, "%2F") + '/' + classname + '.render';
            var name = "Express/" + classname + '/render';
            var segment_info = {
                metric_name : name,
                call_url:"",
                call_count:1,
                class_name: classname,
                method_name: "render",
                params : {}
            }
            var segment = tracer.addSegment(segment_info, record);
            if ( typeof options === 'function' ) {
                cb = options;
                options = null;
            }
            var self = this;
            var wrapped = tracer.callbackProxy(function render_cb(err, rendered){
                segment.end();
                if ( typeof cb === 'function' ) return cb.apply(this, arguments);
                if (err) {
                    logger.debug(err, "Express%d %s Render failed @action %s:", version, name, segment.trace.action.id);
                    return self.req.next(err);
                }
                var returned = self.send(rendered);
                logger.debug("Express%d %s Rendered @action %s.", version, name, segment.trace.action.id);
                return returned;
            });
            return render.call(this, view, options, wrapped, parent, sub);
        };
    }

因为和请求有关,上述逻辑执行完会进入到http的封装逻辑中。当一个请求发送后,会进入到http.ServerResponse.prototype的end方法中:

shimmer.wrapMethod(response, 'http.ServerResponse.prototype', 'end', function wrspe(end) {
            return wrapEnd(agent, end, action);
        });

wrapWrite = wrapEnd = function(agent, original, action) {
    return function(data, encoding, callback) {
        ...

        // 性能跟踪
        if (!action.head_writed) {
            setTraceData(action, this)
        }
        if (this.statusCode != 200) {
            logger.debug('statusCode is %s, skip injecting code.', this.statusCode);
            return original.call(this, resultData || data, encoding, callback);
        }

        if (data && _tingyun.needInject(agent, this._headers || this._header) && !_tingyun.injected) {
            // 如果需要对前端模块嵌入代码 执行...
        }
        
        return original.call(this, resultData || data, encoding, callback);
    }
};

指标收集

特定时间间隔发送一次性能指标,默认为50s

// 启动定时器
Agent.prototype._startTimer = function _startTimer(interval) {
    var agent = this;
    this.hTimer = setInterval(function () { agent._on_timer(); }, interval * 1000);
    if (this.hTimer.unref) this.hTimer.unref();
};

// 发送指标
Agent.prototype._on_timer = function _on_timer() {
    ...
    // 发送时 先收集 然后传给服务器
    this._send_metrics(on_upload_ret);
};

具体的指标收集是个比较复杂的逻辑,类型分为action、sql等;和性能有关的类包括: traceactionsegment等。具体的分析后续加上。

你可能感兴趣的:(Node.js APM实现分析)