Eggjs接入Jaeger和OpenTracing

背景

OpenTracing是一套标准的分布式链路追踪协议, 主要概念有TraceSpan两个类型, 功能比较全面, 在业界内使用比较广泛, 由于我们这边的服务也存在长链路互相调用的过程, 接入OpenTracing后可以很方便的分析调用过程

JaegerUber开发的分布式追踪系统, 兼容OpenTracing标准, 我们基于它来搭建整个服务

数据流

由于我们的接口可能是被前端直接调用, 也可能是由其他服务调用过来的, 那么我们需要在请求解析中加入判断是否正在一个trace中, 如果不在, 那么需要自己创建一个trace

同时, 我们的服务端也需要调用其他的后端服务, 那么我们需要在调用时传递给其他服务完整的trace信息, 并做好相关记录, 实现完整的链路追踪

Egg 接入

  1. Egg.js启动时, 我们将tracer实例注入到app中, 这样可以在全局任意地方都可以获取使用
  2. 在全局中间件中加入trace相关判断和日志上传, 中间件开发可以参考此文档
  3. app.httpclient的相关事件中加入对应header, 即可将目前的trace信息传给其他服务
  4. 由于内部服务互通, 所以用的不是标准的trace-header, 这里用x-custom-trace-id来代替
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// app.js - 全局注入`tracer`实例和`app.httpclient`事件
class AppBootHook {
constructor(app) {
this.app = app;

if (app.config.jaeger.enable) {
jeagetUtils.setupJaegerClient(app);
}
}
}

// jeagetUtils
const Jaeger = require('jaeger-client');

class JaegerUtils {
constructor() {}
setupJaegerClient(app) {
const { config, options } = app.config.jaeger;
app.tracer = Jaeger.initTracer(config, options);

app.httpclient.on('request', (req) => {
req.reqSpan = req.ctx.startSpan(req.url);
const traceId = req.reqSpan._spanContext.toString();
req.args.headers = {
...req.args.headers,
'x-custom-trace-id': traceId,
};
});

app.httpclient.on('response', ({ req, res, ctx }) => {
if (req.reqSpan) {
req.reqSpan.setTag('status', res.status);
req.reqSpan.finish();
}
});
}
}

const jeagetUtils = new JaegerUtils();
module.exports = jeagetUtils;

ctx里封装相关trace方法, 方便外部调用, 并保存rootSpan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// extend/context.js
module.exports = {
getTraceId() {
if (!this.__trace_id) {
this.__trace_id = this.header['x-custom-trace-id'] || uuidv4().replace(/-/gi, '');
}
return this.__trace_id;
},
createRootSpan(operationName) {
if (!this.app.tracer) {
return null;
}
if (!this.__rootSpan) {
const lbcSpan = this.app.tracer.extract(FORMAT_HTTP_HEADERS, {
'x-custom-trace-id': this.getTraceId(),
});

this.__rootSpan = this.app.tracer.startSpan(operationName, { childOf: lbcSpan });
}

return this.__rootSpan;
},
startSpan(operationName = '', options = {}) {
if (!this.app.tracer) {
return null;
}
if (!options.childOf) {
if (!this.__rootSpan) {
this.app.logger.error(`Not found root span`);
} else {
options.childOf = this.__rootSpan;
}
}
if (!operationName) {
this.app.logger.error(`Not set operationName`);
operationName = 'unknow';
}
return this.app.tracer.startSpan(operationName, options);
},
};

wrapper里创建rootSpan, 并最终完成trace记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// wrapper.js
module.exports = (config, app) => {
return async function wrapper(ctx, next) {
const rootSpan = ctx.createRootSpan(ctx.path);

try {
let result = await next();
return result;
} catch (e) {
// ...
} finally {
if (rootSpan) {
rootSpan.setTag('status', ctx.status);
rootSpan.finish();
}
}
};
};

定时任务

上面步骤完成之后, 外部发起的请求已经可以完整的打印调用链路日志了, 但是内部的定时任务调用因为不走wrapper中间件, 所以没有rootSpan, 就无法记录trace信息

这个问题解决起来也很简单, 我们只需要在schedule初始化的时候创建rootSpan就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// schedule/test-task.js

module.exports = (app) => {
return {
schedule: {
immediate: false,
type: 'worker',
},
async task(ctx) {
const rootSpan = ctx.createRootSpan(`test-task`);
const startTime = Date.now();
const job = new Job(ctx, app);
await job.do();
app.logger.info(`use time: ${Date.now() - startTime}ms to finish test-task`);
if (rootSpan) {
rootSpan.finish();
}
},
};
};

搭建 jaeger

代码开发完成之后, 我们需要测试一下整体流程和观察一下数据上报的效果, 需要自己搭建一个测试的Jaeger服务, 由于Jaeger提供Docker镜像, 这一步也非常简单, 参考官方命令启动即可

1
2
3
4
5
6
7
8
9
docker run -d --name jaeger-all-in-one \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 14250:14250 \
jaegertracing/all-in-one

然后将配置的collectorEndpoint指向本地服务, 将采样率调整为1, 打开http://localhost:16686, 随便造几个请求, 就能看到完整的链路调用信息了

作者

Mosby

发布于

2018-11-19

许可协议

评论