背景
OpenTracing是一套标准的分布式链路追踪协议, 主要概念有Trace和Span两个类型, 功能比较全面, 在业界内使用比较广泛, 由于我们这边的服务也存在长链路互相调用的过程, 接入OpenTracing后可以很方便的分析调用过程
Jaeger是Uber开发的分布式追踪系统, 兼容OpenTracing标准, 我们基于它来搭建整个服务
数据流
由于我们的接口可能是被前端直接调用, 也可能是由其他服务调用过来的, 那么我们需要在请求解析中加入判断是否正在一个trace中, 如果不在, 那么需要自己创建一个trace
同时, 我们的服务端也需要调用其他的后端服务, 那么我们需要在调用时传递给其他服务完整的trace信息, 并做好相关记录, 实现完整的链路追踪
Egg 接入
- 在
Egg.js启动时, 我们将tracer实例注入到app中, 这样可以在全局任意地方都可以获取使用
- 在全局中间件中加入
trace相关判断和日志上传, 中间件开发可以参考此文档
- 在
app.httpclient的相关事件中加入对应header, 即可将目前的trace信息传给其他服务
- 由于内部服务互通, 所以用的不是标准的
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
| class AppBootHook { constructor(app) { this.app = app;
if (app.config.jaeger.enable) { jeagetUtils.setupJaegerClient(app); } } }
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
| 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
| 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
|
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, 随便造几个请求, 就能看到完整的链路调用信息了