Node.js APM实现分析

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

整体结构

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


程序框架.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等。具体的分析后续加上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335

推荐阅读更多精彩内容