本篇文章介绍下,我在实现OpenTracing过程中,思考过的一些问题及经验总结;
灵魂十问,你能答得上来吗?
注: 以OpenTracing-Jaeger-Java实现为例进行分析;
注: 转载请注明🙂,喜欢请一键三连哦 😊
一、OpenTracing实现过程
1.1 OpenTracing 实现过程是怎样的?
1.2 OpenTracing Java实现有哪些模块?
主要模块设计请见下图, 图片来自网络,链接:https://www.jianshu.com/p/82cd923191fb
二、上下文如何处理?
上下文的处理,在OpenTracing中算是重要的一环了。在OpenTracing-Java中, 开启一个Span, 结束一个Span, 请求间传递上下文等等这些过程都是有标准API了,但是对于get_current_span过程却没有统一地实现, 我们就重点分析下这一过程。
比如:上下文中都包含那些信息? 同一上下文(通常是一个线程)如何传递Span? 服务间调用如何实现上下文传递? 如何生成Parent,Child关系?
2.1 上下文中包含那些信息?
答: 其实SpanContext 已经打印在了日志中,我相信如下日志,你已经非常熟悉;
OpenTracing 日志:
- 客户端:
2020-08-02 18:45:41.537 INFO 13084 --- [ scheduling-1] i.j.internal.reporters.LoggingReporter : Span reported: 39e6f67f630a6a30:39e6f67f630a6a30:0:1 - GET
INFO 13084 --- [ scheduling-1] i.j.internal.reporters.LoggingReporter : Span reported: 2ba6c0c35115b366:2ba6c0c35115b366:0:1 - GET
- 服务端:
2020-08-02 18:45:41.534 INFO 15088 --- [nio-8080-exec-3] i.j.internal.reporters.LoggingReporter : Span reported: 39e6f67f630a6a30:c9200632cc254ee9:39e6f67f630a6a30:1 - describeProviderStatus
2020-08-02 18:45:46.010 INFO 15088 --- [nio-8080-exec-5] i.j.internal.reporters.LoggingReporter : Span reported: 2ba6c0c35115b366:2c319cdcfbc94eac:2ba6c0c35115b366:1 - describeProviderStatus
这些日志打印的都是什么呢? 我们找到代码出处, 就会发现,其实打印在日志中的就是上下文 + operationName;
//LoggingReporter.report:
@Override
public void report(JaegerSpan span) {
logger.info("Span reported: {}", span);
}
//JaegerSpan.toString
@Override
public String toString() {
synchronized (this) {
return context.toString() + " - " + operationName;
}
}
// JaegerSpanContext.toString
@Override
public String toString() {
return TextMapCodec.contextAsString(this);
}
//TextMapCodec.contextAsString
/**
* Encode context into a string.
* @param context Span context to encode.
* @return Encoded string representing span context.
*/
public static String contextAsString(JaegerSpanContext context) {
// flag: 1 or 2 , 采样:1, debug模式 2
int intFlag = context.getFlags() & 0xFF;
return new StringBuilder()
.append(context.getTraceId()).append(":")
.append(Long.toHexString(context.getSpanId())).append(":")
.append(Long.toHexString(context.getParentId())).append(":")
.append(Integer.toHexString(intFlag))
.toString();
}
由此,我们可以看到,SpanContext 上下文其实就是 { traceId } : { spanId } : { parentId } : { flag};
ps: flag= 1 or 2 。 采样:1; debug: 2;
2.2 进程内如何传递上下文中的Span?
-
OpenTracing Java中,如何获取上下文(Context)中的Span?
答:OpenTracing 是抽象了Scope(active span) 和 ScopeManager(设置Scope与获取当前Scope)概念;
有了ScopeManager, 我们就可以通过scopeManager.activeSpan()
方法获取到当前Span, 并且通过scopeManager().activate(span)
方法设置当前上下文active span;
Scope 和 ScopeManager 详细介绍: OpenTracing-Java Scope与ScopeManager
PS: ThreadLocal 是Java 提供的一种保存线程私有信息的机制,当前线程都是可见的;
2.3 同一进程内,如何处理Span的Parent&Child 关系?
答: 在上一问题中, 我们提到OpenTracing-Java中,使用scopeManager来处理管理了上下文,可以从 scopeManager中拿到当前上下文Span; 那具体是在哪里设置的父子关系呢?
在OpenTracing-Java实现中, 是在 tracer.start()
方法中处理的;start()
方法中通过 scopeManager 判断是存在active span ,若存在则生成CHILD_OF关系的上下文, 如果不存在则createNewContext;
@Override
public JaegerSpan start() {
JaegerSpanContext context;
// Check if active span should be established as CHILD_OF relationship
if (references.isEmpty() && !ignoreActiveSpan && null != scopeManager.activeSpan()) {
asChildOf(scopeManager.activeSpan());
}
// 根据情况生成上下文
if (references.isEmpty() || !references.get(0).getSpanContext().hasTrace()) {
context = createNewContext();
} else {
context = createChildContext();
}
//...
return jaegerSpan;
}
2.4 什么时候会出现 follows_from关系?
答: SpringBoot Web 中仅出现过一次, SpringBoot Error 处理时,由 Dispather Handler 转至 ErrorHandler时,使用此关系;
// spring boot default error handling, executes interceptor after processing in the filter (ugly huh?)
serverSpan = tracer.buildSpan(httpServletRequest.getMethod())
.addReference(References.FOLLOWS_FROM, TracingFilter.serverSpanContext(httpServletRequest))
.start();
再来看一下官方文档中,OpenTracing数据模型,对follows_from关系的介绍:
FollowsFrom references: Some parent Spans do not depend in any way on the result of their child Spans. In these cases, we say merely that the child Span FollowsFrom the parent Span in a causal sense. There are many distinct FollowsFrom reference sub-categories, and in future versions of OpenTracing they may be distinguished more formally.
parent Span结果不取决于Child Span,这时我们可能认为child Span 与parent span 仅仅是 FollowsFrom 关系;
2.5 跨进程服务调用,如何传递上下文?
In order to trace across process boundaries and RPC calls in distributed systems, spanContext needs to propagated over the wire. The OpenTracing Java API provides two methods in the Tracer interface to do just that, inject(SpanContext, format, carrier) and extract(format, carrier). – OpenTracing 官网
Tracing Java Api 提供了Inject and Extract 方法, 封装在 Tracer 接口中, 详见:Inject and Extract
- Inject
* Inject a SpanContext into a `carrier` of a given type, presumably for propagation across process boundaries.
*
Example:
* Tracer tracer = ...
* Span clientSpan = ...
* TextMap httpHeadersCarrier = new AnHttpHeaderCarrier(httpRequest);
* tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, httpHeadersCarrier);
- Extract
* Tracer tracer = ...
* TextMap httpHeadersCarrier = new AnHttpHeaderCarrier(httpRequest);
* SpanContext spanCtx = tracer.extract(Format.Builtin.HTTP_HEADERS, httpHeadersCarrier);
* ... = tracer.buildSpan('...').asChildOf(spanCtx).start();
常见HTTP(TextMapCodec)/二进制 这些传递格式, 官方SDK 已经有默认实现了;
2.6 自定义的协议, 应该如何跨进程传递上下文呢?
答: 自己实现 **Injector **和 Extractor 接口;
Injector: You should implement this class if you want to add possibility to inject information aboutJaegerSpanContext that is passed between services in your custom propagation scheme. Otherwise youshould probably use built-in {@link TextMapCodec} or {@link B3TextMapCodec}
Extractor: You should implement this class if you want to add possibility to extract information aboutJaegerSpanContext that is provided in your custom propagation scheme. Otherwise you should probably usebuilt-in {@link TextMapCodec} or {@link B3TextMapCodec}
2.7 如何对异步请求结果正确记录?
答: 可以通过添加回调方法,对结果进行处理, 而后上报; 如(TracingAsyncRestTemplateInterceptor实现):
try (Scope scope = tracer.activateSpan(span)) {
ListenableFuture<ClientHttpResponse> future = execution.executeAsync(httpRequest, body);
future.addCallback(new ListenableFutureCallback<ClientHttpResponse>() {
@Override
public void onSuccess(ClientHttpResponse httpResponse) {
...
span.finish();
}
@Override
public void onFailure(Throwable ex) {
...
span.finish();
}
});
return future;
}
三、Opentracing-Java-Spring-Jaeger 有哪些问题没有处理?
3.1 包含异步方法(启动新线程)是否能被正常记录?
一个请求过来,我可能会调用异步方法(开新线程去处理),是否能被记录到?
答: 默认实现中,并没有对新线程进行上下文的Copy, 所以当前的上下文也就获取不到,也就无法记录下来;我们可以在启用新线程时,传递上下文;