背景
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
, 随便造几个请求, 就能看到完整的链路调用信息了