看完这篇文章我奶奶都懂Opentracing了 (二)

二. 概念分析

1. Span和SpanContext

结合上述示例,我们从Span开始入手来进行概念分析,但是说在最前面,Span在不同的分布式链路实现中,其定义是不全一样的,尽管Opentracing已经进行了概念的统一,但是具体到各家实现,有时候差异还是有的,故此,我们这里解释Span概念时,会更倾向于Jaeger

一次请求,通常会由分布式系统中的多个工作单元组成,这些工作单元按照调用顺序串起来会形成一条链路,那么一条链路中的某个工作单元,就用Span来表示。但工作单元具体指什么呢,可以这么理解,服务B收到请求并开始处理,这就是一个工作单元,我们可以为这件事情创建一个Span来表示,又比如服务B调用了服务C,这又是一个工作单元,我们可以为调用服务C这件事情创建一个Span。所有Span串联起来就组成一条分布式链路,例如下图这样。

Span千万别想太复杂,简单点想Span就是链路中的被调用或调用行为。通常一个Span需要包含如下的主要信息。

  • Operation Name。可以粗略理解为Span的名称,用于描述当前Span是做什么的;
  • StartTimestampFinishTimestamp。分别表示Span的开始处理时间和结束处理时间;
  • Tags。是一个Map,用于以键值对的形式存储Span在处理业务的过程中的一些数据;
  • Logs。最本质还是一个Map,用于以键值对的形式存储一些数据,在Span处理结束并打印链路日志时会用到;
  • SpanContext。表示Span的上下文,这个说法有点抽象,下面会单独把这个SpanContext拎出来讲一下。

一个Span,最主要的就是上述的这些内容,其中TagsLogs有点抽象,SpanContext较为抽象,但是不慌,首先是TagsLogs,这两兄弟本质就是存储Span在处理业务过程中的一些需要记录的信息,所有内容都可以记录,想记啥就记啥,想记在Tags还是记在Logs全凭自己心情,反正最终要用到这些数据的时候,我们都可以通过Span来拿到,但是TagsLogs也不是完全没有区别,先看一下Jaeger中的Span对象的tags字段的签名,如下所示。

private final Map<String, Object> tags;

是一个简单的Map,然后logs字段要复杂一点,字段签名如下所示。

private List<LogData> logs;@Getter
public final class LogData {private final long time;private final String message;private final Map<String, ?> fields;LogData(long time, String message, Map<String, ?> fields) {this.time = time;this.message = message;this.fields = fields;}
}

乍一看是一个LogData的集合,但其实LogData本质就是对一个Map做了包装,所以一个LogData就是一组键值对,这就让logs相较于tags有一个天然的分组的优势,例如我当前Span某次调用下游的操作的相关数据可以存成一个LogData,我当前Span某次SQL执行的相关数据可以存成一个LogData

现在再来揭开SpanContext的面纱。首先Span是一次请求链路中的某个工作单元的表示,那么对于某个Span来说,有两个东西很重要,其一是标识这次请求链路的traceId,毕竟Span得知道自己是在哪条链路上对吧,其二是标识当前工作单元的spanId,相当于Span在这条链路上的身份证。既然SpanContext作为Span上下文,理所应当的SpanContext就应该保存着对应的链路的traceId以及SpanContext所属SpanspanId,但是仅有traceIdspanId还不够,还得保存一个parentSpanId,当前Span可以通过parentSpanId表示对应链路中自己的上一个工作单元是哪个Span,最后,SpanContext还有一个字段叫做baggage,是一个Map,可以将数据从当前Span传递到下一个Span,即跨进程数据传递。

那么总结一下SpanContext里面的主要信息。

  • traceIdSpan所属链路的唯一标识;
  • spanIdSpan在对应链路上的唯一标识;
  • parentSpanIdSpan在对应链路上的父Span的唯一标识;
  • baggage。以键值对的形式存储数据并进行跨进程传输。

其实无论是traceIdspanId还是baggage,都是需要从一个Span传递到下一个Span的,所以SpanContext主要就是负责跨进程数据传递。

那么到这里,Span是什么,想必在纸面上已经是说明白了七七八八了,下面再来看一下我愿称之为Span发射器的Tracer,先看一下io.opentracing包中提供的Tracer接口的注释,如下所示。

Tracer is a simple, thin interface for Span creation and propagation across arbitrary transports.

Tracer主要干两件事情,其一是创建Span,其二是传递Span,创建Span没什么好说的,传递Span又分同一进程和跨进程传递,至于怎么传递,后面可以结合Jaeger提供的相关实现来看一下。

到此为止,我们已经基本具备去阅读JaegerOpentracing相关实现的源码的基础了,所以现在就接合第一节中的客户端和服务端的示例代码,过一下相关源码实现,这样能更加理解Opentracing的相关概念。

首先再重温一下客户端这边的RestTemplate的拦截器的实现,如下所示。

public class RestTemplateTracingInterceptor implements ClientHttpRequestInterceptor {private final Tracer tracer;public RestTemplateTracingInterceptor(Tracer tracer) {this.tracer = tracer;}@NotNullpublic ClientHttpResponse intercept(@NotNull HttpRequest request, @NotNull byte[] body,ClientHttpRequestExecution execution) throws IOException {Span span = tracer.buildSpan(REST_TEMPLATE_SPAN_NAME).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).start();span.setBaggageItem(REST_TEMPLATE_SPAN_TAG_URI, request.getURI().toString());tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));try (Scope scope = tracer.activateSpan(span)) {return execution.execute(request, body);} catch (IOException e) {Tags.ERROR.set(span, Boolean.TRUE);throw e;} finally {span.finish();}}}

首先我们通过Tracer创建了一个Span,因为使用的是Jaeger,这里的Tracer实际是JaegerTracer,创建出来的Span实际是JaegerSpan。在创建Span时,实际调用的是JaegerTracer.SpanBuilder#start方法,这个方法实现如下所示。

@Override
public JaegerSpan start() {JaegerSpanContext context;if (references.isEmpty() && !ignoreActiveSpan && null != scopeManager.activeSpan()) {asChildOf(scopeManager.activeSpan());}if (references.isEmpty() || !references.get(0).getSpanContext().hasTrace()) {context = createNewContext();} else {context = createChildContext();}long startTimeNanoTicks = 0;boolean computeDurationViaNanoTicks = false;if (startTimeMicroseconds == 0) {startTimeMicroseconds = clock.currentTimeMicros();if (!clock.isMicrosAccurate()) {startTimeNanoTicks = clock.currentNanoTicks();computeDurationViaNanoTicks = true;}} else {verifyStartTimeInMicroseconds();}JaegerSpan jaegerSpan = getObjectFactory().createSpan(JaegerTracer.this,operationName,context,startTimeMicroseconds,startTimeNanoTicks,computeDurationViaNanoTicks,tags,references);if (context.isSampled()) {metrics.spansStartedSampled.inc(1);} else {metrics.spansStartedNotSampled.inc(1);}return jaegerSpan;
}

上面的JaegerTracer.SpanBuilder#start方法在创建Span对象时,关键的地方其实是创建SpanContext,创建有两种情况,如下所示。

  1. 创建新SpanContext。在没有显式指定父Span,并且当前线程之前没有已经存在的Span时,就需要创建新SpanContext,也就意味着会生成新的traceIdspanId,对应的Span也会作为整个链路的首个节点,相应的parentSpanId会置为0,表示没有父Span
  2. 创建子SpanContext。如果显式指定了父Span,或者当前线程之前有已经存在的Span且不能忽略这个已经存在的Span,就需要创建子SpanContext

创建新SpanContext的情况没什么好说的,下面重点分析一下创建子SpanContext,对应方法为SpanBuilder#createChildContext,如下所示。

private JaegerSpanContext createChildContext() {JaegerSpanContext preferredReference = preferredReference();if (isRpcServer()) {if (isSampled()) {metrics.tracesJoinedSampled.inc(1);} else {metrics.tracesJoinedNotSampled.inc(1);}if (zipkinSharedRpcSpan) {return preferredReference;}}return getObjectFactory().createSpanContext(preferredReference.getTraceIdHigh(),preferredReference.getTraceIdLow(),Utils.uniqueId(),preferredReference.getSpanId(),preferredReference.getFlags(),getBaggage(),null);
}

在创建子SpanContext时,Jaeger在满足一定条件下会开启Zipkin的兼容模式,这个时候父SpanContext会直接作为子SpanContext返回,至于这么做有什么用,其实质就是改变了Span的理论模型,但我们这里不必深究,知道有这么回事即可。在更多的时候,创建子SpanContext时其实就是父SpanContexttraceId传递给子SpanContext,然后父SpanContextspanId作为子SpanContextparentSpanId,最后子SpanContext会新生成一个自己的spanId

至此,我们已经了解了Tracer如何创建出Span,那么现在再来看一下io.opentracing中定义的Span接口长什么样,这决定了我们可以基于Span做一些什么操作。

public interface Span {SpanContext context();Span setTag(String key, String value);Span setTag(String key, boolean value);Span setTag(String key, Number value);<T> Span setTag(Tag<T> tag, T value);Span log(Map<String, ?> fields);Span log(long timestampMicroseconds, Map<String, ?> fields);Span log(String event);Span log(long timestampMicroseconds, String event);Span setBaggageItem(String key, String value);String getBaggageItem(String key);Span setOperationName(String operationName);void finish();void finish(long finishMicros);
}

基本就是Span持有什么,我们就能操作什么,比如在客户端的RestTemplate拦截器的示例代码中,我们就手动添加了Baggage键值对,而我们知道,Baggage是存在于SpanContext中并用于跨进程传输的,那么这里要如何做到跨进程传输呢,这里就要引出Opentracing中另外一套很重要的概念,即InjectExtract

2. Inject和Extract

要了解InjectExtract,首先需要看一下Tracer接口中的如下两个方法。

<C> void inject(SpanContext spanContext, Format<C> format, C carrier);<C> SpanContext extract(Format<C> format, C carrier);

上述的inject() 方法和extract() 方法乍一看有点抽象,但是如果用第一节示例代码来解释应该就比较好理解,即客户端这边通过inject() 方法可以将SpanContext中的内容,注入到HTTP请求头中,怎么注入,就是formatcarrier配合完成,然后服务端这边通过extract() 方法可以从HTTP请求头中提取出SpanContext,怎么提取,也是formatcarrier配合完成。在大概知道inject() 方法和extract() 方法的作用后,我们再来看一下formatcarrier,通过方法签名,我们可以知道carrier的类型是由format来决定的,所以formatcarrier肯定是要配合使用的,这里以示例代码中的客户端的RestTemplate拦截器的实现举例,先看如下这一段示例代码。

tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));

先看一下Format.Builtin.HTTP_HEADERS是什么,如下所示。

public final static Format<TextMap> HTTP_HEADERS = new Builtin<TextMap>("HTTP_HEADERS");

上述能说明两个事情:第一就是Format.Builtin.HTTP_HEADERS的名称叫做HTTP_HEADERS,表明这个Format和注入信息到HTTP请求头有关,第二就是和Format.Builtin.HTTP_HEADERS配合使用的carrier的类型是TextMap。不过如果仔细看一下Format.Builtin.HTTP_HEADERS,发现其是一个Builtin对象,这个对象是一个很简单的对象,如下所示。

final class Builtin<C> implements Format<C> {private final String name;private Builtin(String name) {this.name = name;}public final static Format<TextMap> TEXT_MAP = new Builtin<TextMap>("TEXT_MAP");public final static Format<TextMapInject> TEXT_MAP_INJECT = new Builtin<TextMapInject>("TEXT_MAP_INJECT");public final static Format<TextMapExtract> TEXT_MAP_EXTRACT = new Builtin<TextMapExtract>("TEXT_MAP_EXTRACT");public final static Format<TextMap> HTTP_HEADERS = new Builtin<TextMap>("HTTP_HEADERS");public final static Format<Binary> BINARY = new Builtin<Binary>("BINARY");public final static Format<BinaryInject> BINARY_INJECT = new Builtin<BinaryInject>("BINARY_INJECT");public final static Format<BinaryExtract> BINARY_EXTRACT = new Builtin<BinaryExtract>("BINARY_EXTRACT");@Overridepublic String toString() {return Builtin.class.getSimpleName() + "." + name;}
}

那反正我是无论如何都无法想明白Builtin对象怎么和TextMap扯上关系,直到我发现其实每一个Format都有一个配套的Injector以及Extractor,例如默认情况下,Format.Builtin.HTTP_HEADERS配套的InjectorExtractor都是TextMapCodec,所以下面就分析一下TextMapCodec,先看一下类图,如下所示。

首先TextMapCodec分别实现了InjectorExtractor接口,对应的实现方法签名如下所示。

public void inject(JaegerSpanContext spanContext, TextMap carrier)public JaegerSpanContext extract(TextMap carrier)

那么现在就逐渐清晰明了了,我们之前困惑format对象怎么和carrier扯上关系,其实就是首先根据format对象拿到与之配对的InjectorExtractor,然后在InjectorExtractor中会调用到carrier对象来完成注入和提取的操作,那么具体到我们的示例里,format对象是Format.Builtin.HTTP_HEADERS,所以对应的carrier类型是TextMap,且其配对的InjectorExtractor均是TextMapCodec,然后TextMapCodec实现的inject()extract() 的入参carrier的类型也均是TextMap,所以Format.Builtin.HTTP_HEADERS就可以处理类型是TextMapcarrier

那么示例代码中,客户端RestTemplate拦截器中,使用的formatFormat.Builtin.HTTP_HEADERScarrierHttpHeadersCarrier,所以如何将SpanContext的内容写到HTTP请求头,需要分析一下TextMapCodec#inject方法,如下所示。

@Override
public void inject(JaegerSpanContext spanContext, TextMap carrier) {carrier.put(contextKey, contextAsString(spanContext));for (Map.Entry<String, String> entry : spanContext.baggageItems()) {carrier.put(keys.prefixedKey(entry.getKey(), baggagePrefix), encodedValue(entry.getValue()));}
}

我们知道上述方法中的carrier实际类型为HttpHeadersCarrier,所以看一下HttpHeadersCarrier#put方法干了什么,如下所示。

@Override
public void put(String key, String value) {httpHeaders.add(key, value);
}

真相大白了,就是HttpHeadersCarrier#put方法其实就是往HTTP请求头里塞键值对嘛,那么如法炮制,在示例代码服务端的TracingFilter中,使用的formatFormat.Builtin.HTTP_HEADERScarrierHttpServletRequestExtractAdapter,所以其实最终会调用到HttpServletRequestExtractAdapter#iterator方法,如下所示。

@Override
public Iterator<Map.Entry<String, String>> iterator() {return new MultivaluedMapFlatIterator<>(headers.entrySet());
}

我感觉我又懂了,这肯定就是外层会for循环HttpServletRequestExtractAdapter,从而HttpServletRequestExtractAdapteriterator() 方法就把HTTP请求头的迭代器返回出去了,然后外层就能拿到从客户端传递过来的数据了,包括SpanContextBaggage中的内容都能拿到,从而完成了跨进程传递链路信息。

现在知道如何跨进程传递链路信息了,但是我们站在服务端的视角来审视一下,服务端收到一个携带链路信息的请求,然后这个请求在送到业务代码前,先经过了TracingFilter,我们在TracingFilter中通过formatcarrier配合,将客户端传递过来的SpanContext获取了出来,然后基于SpanContext创建出来了当前这一个工作单元的Span,那么后续就应该进入到业务代码中了对吧,但是这个热乎乎的Span对象怎么传递下去呢,如果是通过方法参数的形式传递下去,那侵入性可太大了,例如Go语言,可能真的只能把Span放在context里面,然后把context当传家宝一样的逐层转递下去,但若是Java语言,我们应该想到一个很好使的东西,叫做ThreadLocal,没错,Span在线程中的传递,本质是依赖ThreadLocal,我们接着来分析是怎么一回事。

3. Scope和ScopeManager

现在来到示例的服务端TracingFilter的代码,注意到在获取到Span对象后,后续执行了如下这么一行代码。

Scope scope = tracer.activateSpan(span)

好的,目标出现了,上述代码通过TraceractivateSpan() 方法传入Span并得到了一个ScopeSpan我们已经很熟了,但是Scope又是什么妖魔鬼怪呢,我们先看一下Tracer接口中activateSpan() 方法的签名。

Scope activateSpan(Span span);

Tracer接口中的activateSpan() 方法用于在当前线程中激活一个Span,这里激活其实可以理解为绑定,即将一个Span绑定到当前线程上,然后得到一个Scope对象,那么再继续看一下Scope接口长啥样,如下所示。

public interface Scope extends Closeable {@Overridevoid close();}

只有一个close() 方法,这里其实就是反激活一个Span,即将一个Span从当前线程取消绑定,那么到这里,Scope基本可以理解为控制一个Span在线程中的活动时间段,当基于某个Span创建得到一个Scope起,这个Span在当前线程就开始活跃了,也就是当前线程任何一个地方都可以操作这个Span,当调用Scopeclose() 方法时,对应Span就不活跃了,也就是当前线程不再能够任意操作这个Span了。

基本了解Scope的概念后,还是得结合具体的实现来加深理解,我们还是以Jaeger的实现进行举例,看一下JaegerTracer#activateSpan方法的实现,如下所示。

@Override
public Scope activateSpan(Span span) {return scopeManager().activate(span);
}

上述scopeManager() 方法默认情况下会返回一个ThreadLocalScopeManager,先简单将ScopeManager理解为管理Scope的管理器即可,下面先看一下ThreadLocalScopeManager的实现。

public class ThreadLocalScopeManager implements ScopeManager {final ThreadLocal<ThreadLocalScope> tlsScope = new ThreadLocal<ThreadLocalScope>();@Overridepublic Scope activate(Span span) {return new ThreadLocalScope(this, span);}@Overridepublic Span activeSpan() {ThreadLocalScope scope = tlsScope.get();return scope == null ? null : scope.span();}
}

我突然感觉自己就悟了,我们传进来的Span会被封装为一个ThreadLocalScope对象,同时ThreadLocalScopeManager中又有一个类型为ThreadLocal<ThreadLocalScope>的tlsScope字段,所以创建出来的ThreadLocalScope对象肯定就是放在了tlsScope中,这样就完成和当前线程绑定了嘛,但是上面的activate() 方法好像没有看到那里有将创建出来的ThreadLocalScope对象往tlsScope放,所以玄机肯定就在ThreadLocalScope的构造函数中,如下所示。

ThreadLocalScope(ThreadLocalScopeManager scopeManager, Span wrapped) {this.scopeManager = scopeManager;this.wrapped = wrapped;this.toRestore = scopeManager.tlsScope.get();scopeManager.tlsScope.set(this);
}

ThreadLocalScope在构造函数中做了两件事情,其一是将之前激活的Span获取出来以备后续恢复,也就是从ThreadLocal中把之前绑定的Span获取出来保存起,其二是将当前的Span放到ThreadLocal中,这会覆盖之前放的Span,这就表明同一线程同一时刻只能同时激活一个Span

到这里就清晰明了了,我们要激活的Span其实是被包装成了一个ThreadLocalScope对象,然后所谓的激活,就是把ThreadLocalScope对象设置到ThreadLocalScopeManager持有的一个ThreadLocal中。

现在Span是激活了,那么我们要怎么获取到激活的Span呢,这就得看一下TraceractiveSpan() 方法,Tracer接口中activeSpan() 方法的签名如下所示。

Span activeSpan();

直接看一下JaegerTraceractiveSpan() 方法的实现,如下所示。

@Override
public Span activeSpan() {return this.scopeManager.activeSpan();
}

继续跟进ThreadLocalScopeManageractiveSpan() 方法,如下所示。

@Override
public Span activeSpan() {ThreadLocalScope scope = tlsScope.get();return scope == null ? null : scope.span();
}

其实就是从ThreadLocal中把激活的Span获取出来。

最后再来分析一下ThreadLocalScopeclose() 方法,如下所示。

@Override
public void close() {if (scopeManager.tlsScope.get() != this) {return;}scopeManager.tlsScope.set(toRestore);
}

因为每个Span在激活时,都会把之前已经激活的Span保存下来,而如果当前Span对应的ThreadLocalScopeclose() 方法被调用,则表明当前Span已经结束活动,那么此时就应该将保存下来的之前Span再重新与当前线程绑定,所以ThreadLocalScopeclose() 方法干的就是这么一个事情,而且ThreadLocalScopeclose() 方法一定要记得调用,否则会引发内存泄漏。

4. Tracer

其实前文有提到Tracer是什么,之所以在这里又重新分析一次Tracer,是因为只有在知道SpanSpanContextInjectExtract,以及ScopeScopeManager之后,才能明白Tracer是干什么的。在io.opentracing中定义的Tracer接口上,有如下这么一段注释。

Tracer is a simple, thin interface for Span creation and propagation across arbitrary transports.

也就是Tracer主要完成两件事情,其一是创建Span,其二是传递Span

对于创建Span来说,Tracer主要是通过buildSpan() 方法得到一个SpanBuilder,然后基于SpanBuilder可以设置Span的父SpanTags等信息,最后通过SpanBuilderstart() 方法创建出来Span。创建Span的相关源码,在本节第1小节介绍SpanSpanContext时已经进行了分析,这里就不重复分析了。

对于传递Span来说,又分两种情况,其一是跨进程传递,其二是线程中传递。对于跨进程传递,主要就是依赖Tracer的如下两个方法。

<C> void inject(SpanContext spanContext, Format<C> format, C carrier);<C> SpanContext extract(Format<C> format, C carrier);

跨进程传递Span,传递的主体是SpanSpanContextinject() 方法可以把SpanContext注入到跨进程传递的介质中,例如HTTP请求头,具体一点就是利用format配套的Injector,把SpanContext注入给carrier中的传递介质例如HTTP请求头,然后extract() 方法可以从跨进程传递介质中把SpanContext提取出来,具体一点就是利用format配套的Extractor,从carrier中的传递介质例如HTTP请求头中提取出SpanContext。跨进程传递Span的相关源码,在本节第2小节介绍InjectExtract时已经进行了分析,这里就不重复分析了。

对于线程中传递Span,主要是依赖Tracer的如下两个方法。

Scope activateSpan(Span span);Span activeSpan();

activateSpan() 方法意为激活Span,实际就是将Span和当前线程绑定,activeSpan() 方法意为拿到激活的Span,实际也就是拿到和当前线程绑定的Span,所以通过activateSpan() 方法和activeSpan() 方法,就能实现Span在线程中的传递,并且在Java语言中,绑定一个东西到一个线程,天然的适合使用ThreadLocal来使用,所以通常Span就是放在ThreadLocal中进行线程中传递的,具体的源码实现在本节第3小节介绍ScopeScopeManager时已经进行了分析,这里就不重复分析了。

那么至此,Tracer是干啥的,其实是比较清楚的了,称之为Span发射器,不为过吧。

5. Jaeger中的Reporter

可以注意到,Span接口有定义一个finish() 方法,该方法签名如下所示。

void finish();

由上可知finish() 方法主要完成的事情就是设置Span的完成时间并记录Span,至于如何记录,取决于各家实现,本小节主要是分析一下Jaeger中的SpanReporter,首先看一下JaegerSpan#finish方法的实现,如下所示。

@Override
public void finish() {if (computeDurationViaNanoTicks) {long nanoDuration = tracer.clock().currentNanoTicks() - startTimeNanoTicks;finishWithDuration(nanoDuration / 1000);} else {finish(tracer.clock().currentTimeMicros());}
}

Span的持续时间就等于当前时间减去startTimeNanoTicksstartTimeNanoTicks是在Span被创建的时候完成赋值的,现在继续跟进一下JaegerSpan#finishWithDuration方法,如下所示。

private void finishWithDuration(long durationMicros) {if (durationMicrosUpdater.compareAndSet(this, null, durationMicros)) {if (context.isSampled()) {tracer.reportSpan(this);}} else {log.warn("Span has already been finished; will not be reported again.");}
}

上述方法实际就是基于CAS的方式来更新JaegerSpandurationMicroseconds字段,更新成功则调用TracerreportSpan() 方法来记录Span,更新失败则表示Span已经被完成过,而Span是不能重复被完成的。现在继续看一下JaegerTracer#reportSpan方法的实现,如下所示。

void reportSpan(JaegerSpan span) {reporter.report(span);metrics.spansFinished.inc(1);
}

上述方法中会使用JaegerTracer持有的reporter字段(类型是Reporter)来记录Span,而JaegerTracer持有的reporter是在创建JaegerTracer时设置的,回顾一下示例代码中的TracerConfig,如下所示。

@Configuration
public class TracerConfig {@Autowiredprivate SpanReporter spanReporter;@Autowiredprivate Sampler sampler;@Beanpublic Tracer tracer() {return new JaegerTracer.Builder(TRACER_SERVICE_NAME).withTraceId128Bit().withSampler(sampler).withReporter(spanReporter).build();}}

我们在创建JaegerTracer时,设置了一个我们自己写的SpanReporterJaegerTracer,我们自己写的SpanReporter实现了Reporter接口,如下所示。

public class SpanReporter implements Reporter {public void report(JaegerSpan span) {}public void close() {}}

所以在最终记录Span时,每一个Span都会送到我们写的SpanReporter中来,那这个时候所谓的记录Span,就很有可操作性了。

已知一条分布式链路中,实际就是由一个又一个的Span串起来的,每个Span都有一个相同的traceId,表明大家都是同一个分布式链路上的工作单元,同时每个Span都有spanIdparentSpanId,这样可以通过某个SpanspanIdparentSpanId明确这个Span在这条分布式链路中所处的位置,所以我们在记录Span时,其实就是需要读取出SpantraceIdspanIdparentSpanIddurationMicroseconds等参数,然后将这些参数组装成一定格式的数据,最后将Span的格式化后的数据送到分布式链路追踪系统的服务端,后续分布式链路追踪系统的服务端就可以结合指定的格式,解析出一个又一个Span的链路信息,最终给用户呈现出一条又一条的分布式链路。

我们在示例的SpanReporter中,没有对Span做任何操作,这主要是为了简化示例代码,但实际上,我们可以通过各种方式来发送Span数据给到我们的分布式链路追踪系统的服务端,例如发送Kafka,再例如打印日志再进行日志采集,总之可操作性是很强的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/833012.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1-1ARM开发环境搭建(GD32)

1:安装MDK最好是5.27以及以上版本&#xff0c;避免后续学习中出现相关错误 2&#xff1a;安装芯片支持包 双击安装即可&#xff0c;也可以是默认路径&#xff0c;也可以自己更改路径 3&#xff1a;安装jlink下载器驱动&#xff08;下载调试器&#xff09; 具体安装步骤如下所示…

一键解密,网络安全神器现已问世!

一、简介 当前版本V1.1这款工具是一款功能强大的网络安全综合工具&#xff0c;旨在为安全从业者、红蓝对抗人员和网络安全爱好者提供全面的网络安全解决方案。它集成了多种实用功能&#xff0c;包括解密、分析、扫描、溯源等&#xff0c;为用户提供了便捷的操作界面和丰富的功…

一个好用的MQTT客户端软件

软件功能如下&#xff0c;实现的协议版本是 3.1.1 仅实现了常用的 CONNECT , PUBLISH , SUBSCRIBE 及相应的应答报文。支持以 Hex 格式显示接收的原始报文&#xff08;方便初学者学习&#xff09;。支持所有字段的自定义配置。支持保存与加载配置文件。 软件界面如下所示&…

ESP32 IDF linux下开发环境搭建

文章目录 介绍升级Python环境下载Python包配置编译环境及安装Python设置环境变量 ESPIDF环境搭建下载esp-idf 代码编译等待下载烧录成功查看串口打印 介绍 esp32 官方文档给的不是特别详细 参考多方资料 最后才完成开发 主要问题在于github下载的很慢本教程适用于ubuntu deban…

Vue生命周期都有哪些?

定义 Vue的生命周期就是实例从创建到销毁的一个过程&#xff0c;即从创建、初始化数据、编译模板、挂载Dom($el)->渲染、更新->渲染&#xff0c;卸载等一系列的过程。el是挂载点如<div id"app"></div>。 Vue的生命周期分为八个阶段 1.beforeCreate…

ubuntu20安装colmap

系统环境 ubuntu20 &#xff0c;cuda11.8 &#xff0c;也安装了anaconda。因为根据colmap的官方文档说的&#xff0c;如果根据apt-get安装的话&#xff0c;默认是非cuda版本的&#xff0c;而我觉得既然都安装了cuda11.8了&#xff0c;自然也要安装cuda版本的colmap。 安装步骤…

2022 年全国职业院校技能大赛高职组云计算赛项试卷(容器云)

#需要资源&#xff08;软件包及镜像&#xff09;或有问题的&#xff0c;可私聊博主&#xff01;&#xff01;&#xff01; #需要资源&#xff08;软件包及镜像&#xff09;或有问题的&#xff0c;可私聊博主&#xff01;&#xff01;&#xff01; #需要资源&#xff08;软件包…

Flutter开发Dart中的队列(Queue)

文章目录 Dart中的队列&#xff08;Queue&#xff09;基本操作示例队列的类型队列的应用总结 Dart中的队列&#xff08;Queue&#xff09; 队列是一种抽象的数据结构&#xff0c;遵循“先进先出”&#xff08;FIFO&#xff09;的原则。这意味着最早添加的元素将首先被移除。队…

76.网络游戏逆向分析与漏洞攻防-移动系统分析-分析角色移动产生的数据包

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果&#xff0c;代码看不懂是正常的&#xff0c;只要会抄就行&#xff0c;抄着抄着就能懂了 内容…

【第6节课笔记】LagentAgentLego

Lagent 最中间部分的是LLM&#xff0c;即为大语言模型模块&#xff0c;他可以思考planning和调用什么action&#xff0c;再将其转发给动作执行器action executer执行。 支持的工具如下&#xff1a; Arxiv 搜索 Bing 地图 Google 学术搜索 Google 搜索 交互式 IPython 解释器 IP…

Java集合简介

单列集合 双列集合

PWN入门--栈溢出

PWN入门–栈溢出 栈概要 介于网上各种wp在栈溢出利用脚本方面浅入浅出&#xff0c;深入讲一下栈溢出利用时&#xff0c;地址如何计算&#xff0c;栈中垃圾数据如何填充&#xff0c;函数调用时 参数 在栈中的分布&#xff0c; 栈帧的生成&#xff0c;函数返回值ip在栈中的摆放…

【强化学习】公平性Actor-Critic算法

Bringing Fairness to Actor-Critic Reinforcement Learning for Network Utility Optimization 阅读笔记 Problem FormulationLearning AlgorithmLearning with Multiplicative-Adjusted RewardsSolving Fairness Utility Optimization Evaluations 在网络优化问题中&#xff…

了解外汇震荡类货币对特征与交易策略

外汇市场是全球最大的金融市场&#xff0c;每天的交易量超过6万亿美元。在这个市场上&#xff0c;货币对之间的价格变动反映了全球经济和政治动态。外汇货币对通常被分为三类&#xff1a;主要货币对、次要货币对和外来货币对。而在交易这些货币对时&#xff0c;市场表现通常分为…

适合小白使用的编译器(c语言和Java编译器专属篇)

本节课主要讲如何安装适合编程小白的编译器 废话不多说&#xff0c;我们现在开始 c/c篇 首先&#xff0c;进入edge浏览器&#xff0c;在搜索框输入visual studio &#xff0c;找到带我画圈的图标&#xff0c;点击downloads 找到community版&#xff08;社区版&#xff09;的下…

BFS Ekoparty 2022 -- Linux Kernel Exploitation Challenge

前言 昨天一个师傅给了我一道 linux kernel pwn 题目&#xff0c;然后我看了感觉非常有意思&#xff0c;题目也不算难&#xff08;在看了作者的提示下&#xff09;&#xff0c;所以就花时间做了做&#xff0c;在这里简单记录一下。这个题是 BFS Lab 2022 年的一道招聘题&#…

原型模式类图与代码

现要求实现一个能够自动生成求职简历的程序&#xff0c;简历的基本内容包括求职者的姓名、性别、年龄及工作经历。希望每份简历中的工作经历有所不同&#xff0c;并尽量减少程序中的重复代码。 采用原型模式(Prototype)来实现上述要求&#xff0c;得到如图 7.25 所示的类图。 原…

FME学习之旅---day26

我们付出一些成本&#xff0c;时间的或者其他&#xff0c;最终总能收获一些什么。 【由于上周&#xff0c;上班状态不是很好&#xff0c;事情多又杂&#xff0c;没有学习的劲头&#xff0c;就短暂的休息了一下下。双休爬山&#xff0c;给自己上了强度&#xff0c;今天才缓过来…

品高虚拟化后端存储的发展演进

在品高虚拟化技术不断发展的过程中&#xff0c;虚拟化的后端存储一直是关注的焦点之一。 本文将从最初的文件存储和NFS开始&#xff0c;追溯到集中式存储SAN&#xff0c;然后选择了Ceph的RBD方式&#xff0c;并最终抵达选择支持vhost协议的后端存储的现状&#xff0c;我们将探…

Android 桌面小组件 AppWidgetProvider

Android 桌面小组件 AppWidgetProvider 简介 小组件就是可以添加到手机桌面的窗口。点击窗口可以进入应用或者进入应用的某一个页面。 widget 组件 如需创建 widget&#xff0c;您需要以下基本组件&#xff1a; AppWidgetProviderInfo 对象 描述 widget 的元数据&#xff0…