笔者以前有过zipkin的经验,但愿扩展到Opentracing,因而在学习Jaeger基础上总结出此文,与你们分享。html
Jaeger
是 Uber
开发的一款调用链服务端产品,开发语言为 golang
,可以兼容接收 OpenTracing
格式的数据。根据其发展历史,能够说是 Zipkin
的升级版。另外,其基于 udp
(也能够 http
)的传输协议,更加定位了其高效、迅速的特色。java
在前文 [业界方案] 用SOFATracer学习分布式追踪系统Opentracing ,咱们使用SOFATracer来进行学习,本次咱们选择了Jaeger,这又是什么缘由?具体以下:nginx
并且咱们正好能够和SOFATracer进行对比印证。git
让咱们用问题来引导阅读。github
Jaeger主要由如下几部分组成:golang
本文只讨论 Jaeger Client 功能。web
全链路跟踪分红三个跟踪级别:数据库
本文只讨论 跨进程跟踪 (cross-process),由于跨进程跟踪是最简单的 ^_^。对于跨进程跟踪,你能够编写拦截器或过滤器来跟踪每一个请求,它只须要编写极少的代码。segmentfault
由于前文已经对背景知识作了较详细的介绍,本文只是提一下几个必要概念。后端
分布式追踪系统发展很快,种类繁多,但核心步骤通常有三个:代码埋点,数据存储、查询展现
在数据采集过程,须要侵入用户代码作埋点,不一样系统的API不兼容会致使切换追踪系统须要作很大的改动。为了解决这个问题,诞生了opentracing 规范。
+-------------+ +---------+ +----------+ +------------+ | Application | | Library | | OSS | | RPC/IPC | | Code | | Code | | Services | | Frameworks | +-------------+ +---------+ +----------+ +------------+ | | | | | | | | v v v v +-----------------------------------------------------+ | · · · · · · · · · · OpenTracing · · · · · · · · · · | +-----------------------------------------------------+ | | | | | | | | v v v v +-----------+ +-------------+ +-------------+ +-----------+ | Tracing | | Logging | | Metrics | | Tracing | | System A | | Framework B | | Framework C | | System D | +-----------+ +-------------+ +-------------+ +-----------+
大多数分布式追踪系统的思想模型都来自Google's Dapper论文,OpenTracing也使用类似的术语。有几个基本概念咱们须要提早了解清楚:
Client Server +--------------+ Request +--------------+ | Client Send | +----------------> |Server Receive| +------+-------+ +------+-------+ | | | v | +------+--------+ | |Server Business| | +------+--------+ | | | | v v +------+--------+ Response +------+-------+ |Client Receive | <---------------+ |Server Send | +------+--------+ +------+-------+ | | | | v v
SpanContext
更像是一个“概念”,而不是通用 OpenTracing 层的有用功能。在建立Span
、向传输协议Inject
(注入)和从传输协议中Extract
(提取)调用链信息时,SpanContext
发挥着重要做用。代码所有来自 https://github.com/yurishkuro/opentracing-tutorial,你们能够本身去下载。
这里的tracer使用的是 JaegerTracer。
public class Hello { private final Tracer tracer; private final OkHttpClient client; private Hello(Tracer tracer) { this.tracer = tracer; this.client = new OkHttpClient(); } private String getHttp(int port, String path, String param, String value) { try { HttpUrl url = new HttpUrl.Builder().scheme("http").host("localhost").port(port).addPathSegment(path) .addQueryParameter(param, value).build(); Request.Builder requestBuilder = new Request.Builder().url(url); Span activeSpan = tracer.activeSpan(); Tags.SPAN_KIND.set(activeSpan, Tags.SPAN_KIND_CLIENT); Tags.HTTP_METHOD.set(activeSpan, "GET"); Tags.HTTP_URL.set(activeSpan, url.toString()); tracer.inject(activeSpan.context(), Format.Builtin.HTTP_HEADERS, Tracing.requestBuilderCarrier(requestBuilder)); Request request = requestBuilder.build(); Response response = client.newCall(request).execute(); Tags.HTTP_STATUS.set(activeSpan, response.code()); if (response.code() != 200) { throw new RuntimeException("Bad HTTP result: " + response); } return response.body().string(); } catch (Exception e) { Tags.ERROR.set(tracer.activeSpan(), true); tracer.activeSpan().log(ImmutableMap.of(Fields.EVENT, "error", Fields.ERROR_OBJECT, e)); throw new RuntimeException(e); } } private void sayHello(String helloTo, String greeting) { Span span = tracer.buildSpan("say-hello").start(); try (Scope scope = tracer.scopeManager().activate(span)) { span.setTag("hello-to", helloTo); span.setBaggageItem("greeting", greeting); String helloStr = formatString(helloTo); printHello(helloStr); } finally { span.finish(); } } private String formatString(String helloTo) { Span span = tracer.buildSpan("formatString").start(); try (Scope scope = tracer.scopeManager().activate(span)) { String helloStr = getHttp(8081, "format", "helloTo", helloTo); span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); return helloStr; } finally { span.finish(); } } private void printHello(String helloStr) { Span span = tracer.buildSpan("printHello").start(); try (Scope scope = tracer.scopeManager().activate(span)) { getHttp(8082, "publish", "helloStr", helloStr); span.log(ImmutableMap.of("event", "println")); } finally { span.finish(); } } public static void main(String[] args) { try (JaegerTracer tracer = Tracing.init("hello-world")) { new Hello(tracer).sayHello("helloTo", "greeting"); } } }
此处虽然不是SOFATracer和Jaeger的本质区别,可是也挺有趣,即SOFATracer是使用SprintBoot来作示例代码,而此处是使用dropwizard来作示例。
可能有人对dropwizard不熟悉,如今大体讲解以下:
Coda Hale
在Yammer
公司时创立的,它旨在提高公司分布式系统的架构(如今叫:微服务)。虽然它最先被用来构建REST Web 服务,而如今它具有了愈来愈多的功能,可是它的目标始终是做为轻量化、为生产环境准备且容易使用的web框架。Dropwizard在优秀的三方库协助下,提供了不错的抽象层,使之更有效率,更简单的编写生产用途的微服务。
Jetty
Jersey
Jackson
Hibernate Validator
JDBI
Dropwizard偏执的认为框架就是用来写代码的,所以对于框架的底层技术栈的调整,原则上Dropwizard是拒绝的。正由于它这么作,使得Dropwizard开发起代码来更快,并且配置更加容易。
对于咱们的示例代码,对Dropwizard使用举例以下,即便用 Dropwizard 创建了两个服务和一个测试client。
io.dropwizard.Application public class Formatter extends Application<Configuration> { private final Tracer tracer; private Formatter(Tracer tracer) { this.tracer = tracer; } @Path("/format") @Produces(MediaType.TEXT_PLAIN) public class FormatterResource { @GET public String format(@QueryParam("helloTo") String helloTo, @Context HttpHeaders httpHeaders) { Span span = Tracing.startServerSpan(tracer, httpHeaders, "format"); try (Scope scope = tracer.scopeManager().activate(span)) { String greeting = span.getBaggageItem("greeting"); if (greeting == null) { greeting = "Hello"; } String helloStr = String.format("%s, %s!", greeting, helloTo); span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); return helloStr; } finally { span.finish(); } } } @Override public void run(Configuration configuration, Environment environment) throws Exception { environment.jersey().register(new FormatterResource()); } public static void main(String[] args) throws Exception { System.setProperty("dw.server.applicationConnectors[0].port", "8081"); System.setProperty("dw.server.adminConnectors[0].port", "9081"); try (JaegerTracer tracer = Tracing.init("formatter")) { new Formatter(tracer).run("server"); } } }
对于一个组件来讲,一次处理过程通常是产生一个 Span;这个 Span 的生命周期是从接收到请求到返回响应这段过程。
这里须要考虑的问题是如何与上下游链路关联起来呢?在 Opentracing 规范中,能够在 Tracer 中 extract 出一个跨进程传递的 SpanContext 。而后经过这个 SpanContext 所携带的信息将当前节点关联到整个 Tracer 链路中去,固然有提取(extract)就会有对应的注入(inject)。
链路的构建通常是 client-server-client-server 这种模式的,那这里就很清楚了,就是会在 client 端进行注入(inject),而后再 server 端进行提取(extract),反复进行,而后一直传递下去。
在拿到 SpanContext 以后,此时当前的 Span 就能够关联到这条链路中了,那么剩余的事情就是收集当前组件的一些数据;整个过程大概分为如下几个阶段:
Jaeger中的Tracer控制了一个完整的服务的追踪,包括注册服务名(serviceName),发送span(reporter),采样(sampler),对span的序列化与反序列化以及传输(registry的injector,extractor),统计追踪系统的信息(metrics,如发送span成功数量等)。
所以opentracing建议每一个服务使用一个Tracer,除此以外Tracer还担负构造span,获取当前span以及获取scopeManager的功能。
经过opentracing的规范亦能够看出,opentracing对Tracer的功能描述为:Tracer is a simple, thin interface for Span creation and propagation across arbitrary transports。而jaeger只是在其基础上增长了其余功能。
Tracer是opentracing给出的接口。
package io.opentracing; public interface Tracer extends Closeable { ScopeManager scopeManager(); Span activeSpan(); Scope activateSpan(Span var1); Tracer.SpanBuilder buildSpan(String var1); <C> void inject(SpanContext var1, Format<C> var2, C var3); <C> SpanContext extract(Format<C> var1, C var2); void close(); }
JaegerTracer 实现了 io.opentracing.Tracer。
public class JaegerTracer implements Tracer, Closeable { private final String version; private final String serviceName; private final Reporter reporter; private final Sampler sampler; private final Map<String, ?> tags; private final boolean zipkinSharedRpcSpan; private final boolean expandExceptionLogs; private final boolean useTraceId128Bit; private final PropagationRegistry registry; private final Clock clock; private final Metrics metrics; private final ScopeManager scopeManager; private final BaggageSetter baggageSetter; private final JaegerObjectFactory objectFactory; private final int ipv4; }
io.opentracing.Span 是 Opentracing 给出的概念。
public interface Span { SpanContext context(); Span setTag(String var1, String var2); Span setTag(String var1, boolean var2); Span setTag(String var1, Number var2); <T> Span setTag(Tag<T> var1, T var2); Span setBaggageItem(String var1, String var2); String getBaggageItem(String var1); Span setOperationName(String var1); void finish(); void finish(long var1); }
JaegerSpan 实现了 io.opentracing.SPan。
public class JaegerSpan implements Span { private final JaegerTracer tracer; private final long startTimeMicroseconds; private final long startTimeNanoTicks; private final boolean computeDurationViaNanoTicks; private final Map<String, Object> tags; private long durationMicroseconds; // span durationMicroseconds private String operationName; private final List<Reference> references; private JaegerSpanContext context; private List<LogData> logs; private boolean finished = false; // to prevent the same span from getting reported multiple times }
在jaeger的实现中,Span
的信息分为以下几方面:
其中span的核心信息存储在SpanContext
中。
JaegerSpanContext 实现了 io.opentracing.SpanContext
public interface SpanContext { String toTraceId(); String toSpanId(); Iterable<Entry<String, String>> baggageItems(); }
public class JaegerSpanContext implements SpanContext { protected static final byte flagSampled = 1; protected static final byte flagDebug = 2; private final long traceIdLow; private final long traceIdHigh; private final long spanId; private final long parentId; private final byte flags; private final Map<String, String> baggage; private final String debugId; private final JaegerObjectFactory objectFactory; private final String traceIdAsString; private final String spanIdAsString; }
span的核心信息存储在SpanContext
中,在构建span时候就会建立,为了防止用户擅自修改核心信息,spanContext中的全部成员都是final修饰的。
根据opentracing的规范, SpanContext
represents Span state that must propagate to descendant Spans and across process boundaries. SpanContext is logically divided into two pieces:
(1) the user-level "Baggage" that propagates across Span boundaries and
(2) any Tracer-implementation-specific fields that are needed to identify or otherwise contextualize the associated Span instance (e.g., a tuple).
上面是说SpanContext表明的是span中必须传递的信息,在逻辑上分为两部分,一分部分是普通的traceId,spanId等信息,另外一部分是baggage这种用户自定义须要传递的信息。
JaegerSpanContext这里只是保存了上下文环境应有的信息,与 SofaTraceContext 不一样,SofaTraceContext 里面还存有Span,可是在 Jaeger,这个功能是在 ScopeManager中完成的。
默认的 RemoteReporter 实现了 Reporter,功能就是咱们在前文中所说的发送报告。
public class RemoteReporter implements Reporter { private static final int DEFAULT_CLOSE_ENQUEUE_TIMEOUT_MILLIS = 1000; public static final int DEFAULT_FLUSH_INTERVAL_MS = 1000; public static final int DEFAULT_MAX_QUEUE_SIZE = 100; private final Sender sender; private final int closeEnqueueTimeout; @ToString.Exclude private final BlockingQueue<Command> commandQueue; @ToString.Exclude private final Timer flushTimer; @ToString.Exclude private final Thread queueProcessorThread; @ToString.Exclude private final QueueProcessor queueProcessor; @ToString.Exclude private final Metrics metrics; }
OpenTracing 抽象了Scope(active span) 和 ScopeManager(设置Scope与获取当前Scope)概念。简单来讲,OpenTracing-Java的实现中, 用Scope和ScopeManager 来处理了OpenTracing中的上下文 (即:get_current_span 过程);
为何要抽象出Scope的概念?直接使用ThreadLocal 存储Span不就能够了吗?
答: 首先理解Scope是什么?Scope 是Active Span的一个容器, Scope 表明着当前活跃的Span; 是对当前活跃Span的一个抽象, 表明了当前上下文所处于的一个过程;
另外, ThreadLocalScope 还记录了 toRestore Span, 这样结束时,能够恢复到上一个Span的状态;
我理解若是只是 get_current_span() 逻辑的话,直接把 span 塞到 ThreadLocal里就能够在线程内传递了;可是ScopeManager看代码是这样实现的,ScopeManager 包含一个 Scope, Scope 又包含了 当前Span, recover Scope;我理解它的好处是: 这样就保证了,若是开启一个子Span(子span 会产生孙子span), 这样 子span 结束后,还能够回到 父span (这样能够继续产生以 父span 为基础的兄弟span), 若是只是ThreadLocal 里塞一个当前span的话,是解决不了这种状况的。
或者说
在多线程环境下
ScopeManager
管理着各个线程的Scope
,而每一个线程中的Scope
管理着该线程中的Span
。这样当某个线程须要获取其线程中当前 活动的 span时,能够经过ScopeManager
找到对应该线程的Scope
,并从Scope
中取出该线程 活动的 span。
Scope 对象是 Active Span的容器;经过Scope能拿到当前上下文内的Active Span;
io.opentracing.util.ThreadLocalScope 是Scope的一个实现,经过ThreadLocal 来存储;
toRestore
来存储,并将当前scope设置到scopeManager中做为当前线程最新的scope。具体定义以下:
public class ThreadLocalScope implements Scope { private final ThreadLocalScopeManager scopeManager; private final Span wrapped; // 当前 Active Span private final ThreadLocalScope toRestore; // 上一Active Span,wrapped 结束时,会恢复到此Span ThreadLocalScope(ThreadLocalScopeManager scopeManager, Span wrapped) { this.scopeManager = scopeManager; this.wrapped = wrapped; // 这两句设置了当前活动Scope this.toRestore = scopeManager.tlsScope.get(); scopeManager.tlsScope.set(this); } @Override public void close() { if (scopeManager.tlsScope.get() != this) { // This shouldn't happen if users call methods in the expected order. Bail out. return; } scopeManager.tlsScope.set(toRestore); } Span span() { return wrapped; } }
Scope是站在CPU角度激活或者失效Span。ScopeManager管理Scope。一个Scope里能够有多个span,可是只有一个激活的span。
在多线程环境下
ScopeManager
管理着各个线程的Scope
,而每一个线程中的Scope
管理着该线程中的Span
。这样当某个线程须要获取其线程中当前活动的 span时,能够经过ScopeManager
找到对应该线程的Scope
,并从Scope
中取出该线程 活动的 span。
有了ScopeManager, 咱们就能够经过 scopeManager.activeSpan()
方法获取到当前Span, 而且经过scopeManager().activate(span)
方法设置当前上下文active span;
io.opentracing.util.ThreadLocalScopeManager 是opentracing提供的ScopeManager的实现,Jaeger并无本身重写一个新类,而是直接使用ThreadLocalScopeManager。
activate 函数的做用是 激活当前 Span。返回Scope(能够理解为 表明当前 Span 活跃的一个阶段)。即调用ThreadLocalScope
的构造方法,将传入的span激活为 当前活动的 span。咱们看一下ThreadLocalScope构造函数就能发现,与其说是激活传入的span倒不如说是激活包裹(wrapped)该span的scope为 当前活动的 scope。
Span 活跃期结束后,须要关闭 Scope, 推荐使用 try-with-resources 关闭。
activeSpan函数则是返回当前 激活(active)状态Span, 无则返回null。
public class ThreadLocalScopeManager implements ScopeManager { // 使用原始的ThreadLocal 来存储 Active Span; ScopeManager中仅包含一个 Scope( Active Span), 即当前上下文中的 active span final ThreadLocal<ThreadLocalScope> tlsScope = new ThreadLocal<ThreadLocalScope>(); // 能够看到,activate 函数就是把span放进一个新生成的 ThreadLocalScope 中,其实就是tlsScope 成员变量中。 @Override public Scope activate(Span span) { return new ThreadLocalScope(this, span); } @Override public Span activeSpan() { ThreadLocalScope scope = tlsScope.get(); return scope == null ? null : scope.span(); } }
Jaeger使用scopeManager来处理管理了上下文,能够从 scopeManager中拿到当前上下文Span;那具体是在哪里设置的父子关系呢?
在OpenTracing-Java实现中, 是在 tracer.start()
方法中处理的;start()
方法中经过 scopeManager 判断是存在active span,若存在则生成CHILD_OF关系的上下文, 若是不存在则createNewContext;
这点和SOFATtacer不一样,SOFATtacer把这个上下文管理功能放在了SofaTraceContext之中,确实在分析代码时候感到有些许混乱。
SpanId 和 TraceID 都是在构建SpanContext 时候生成的。
private JaegerSpanContext createNewContext() { String debugId = getDebugId(); long spanId = Utils.uniqueId(); // span long traceIdLow = spanId; // trace long traceIdHigh = isUseTraceId128Bit() ? Utils.uniqueId() : 0; ...... }
具体规则以下:
public static long uniqueId() { long val = 0; while (val == 0) { val = Java6CompatibleThreadLocalRandom.current().nextLong(); } return val; }
而后是调用到了ThreadLocalRandom # current。
public static Random current() { if (threadLocalRandomPresent) { return ThreadLocalRandomAccessor.getCurrentThreadLocalRandom(); } else { return threadLocal.get(); } } static class ThreadLocalRandomAccessor { @IgnoreJRERequirement private static Random getCurrentThreadLocalRandom() { return ThreadLocalRandom.current(); } }
最后格式以下:
context = {JaegerSpanContext@1701} "c29c9e0f4a0a681c:36217443515fc248:c29c9e0f4a0a681c:1" traceIdLow = -4423486945480775652 traceIdHigh = 0 spanId = 3900526584756421192 parentId = -4423486945480775652 flags = 1 baggage = {HashMap@1693} size = 1 debugId = null objectFactory = {JaegerObjectFactory@1673} traceIdAsString = "c29c9e0f4a0a681c" spanIdAsString = "36217443515fc248"
要经过Jaeger将Java应用数据上报至链路追踪控制台,首先须要完成埋点工做。本示例为手动埋点。
pom.xml中添加了对Jaeger客户端的依赖。
<dependency> <groupId>io.jaegertracing</groupId> <artifactId>jaeger-client</artifactId> <version>${jaeger.version}</version> </dependency>
示例代码并无使用注入的组件,而是手动启动,具体启动/初始化代码以下:
public final class Tracing { private Tracing() { } public static JaegerTracer init(String service) { SamplerConfiguration samplerConfig = SamplerConfiguration.fromEnv() .withType(ConstSampler.TYPE) .withParam(1); ReporterConfiguration reporterConfig = ReporterConfiguration.fromEnv() .withLogSpans(true); // 这里启动 Configuration config = new Configuration(service) .withSampler(samplerConfig) .withReporter(reporterConfig); return config.getTracer(); } }
示例中启动的 io.dropwizard.Application 都会调用init进行初始化。
try (JaegerTracer tracer = Tracing.init("publisher")) { new Publisher(tracer).run("server"); }
具体启动逻辑都是在 io.jaegertracing.Configuration 中完成的。咱们能够看到其中实现了众多配置和一个tracer。
上节代码中有 config.getTracer();
,这就是 jaeger采用builder模式来构建Tracer
。
public class Configuration { private String serviceName; private Configuration.SamplerConfiguration samplerConfig; private Configuration.ReporterConfiguration reporterConfig; private Configuration.CodecConfiguration codecConfig; private MetricsFactory metricsFactory; private Map<String, String> tracerTags; private boolean useTraceId128Bit; private JaegerTracer tracer; public synchronized JaegerTracer getTracer() { if (tracer != null) { return tracer; } tracer = getTracerBuilder().build(); // 构建 return tracer; } ...... }
build()
方法最终完成了Tracer
对象的构造。
RemoteReporter
来report Span
到agent,RemoteControlledSampler
。metrics
是在Builder内部类中的有默认值的成员变量metrics
。public JaegerTracer build() { if (reporter == null) { reporter = new RemoteReporter.Builder() .withMetrics(metrics) .build(); } if (sampler == null) { sampler = new RemoteControlledSampler.Builder(serviceName) .withMetrics(metrics) .build(); } return createTracer(); } protected JaegerTracer createTracer() { return new JaegerTracer(this); }
Tracer对象能够用来建立Span对象以便记录分布式操做时间、经过Extract/Inject方法跨机器透传数据、或设置当前Span。Tracer对象还配置了上报数据的网关地址、本机IP地址、采样率、服务名等数据。用户能够经过调整采样率来减小因上报数据产生的开销。
在启动以后,用户获得 Tracer 来进行后续手动埋点。
JaegerTracer tracer = Tracing.init("hello-world")
下面都是手动埋点。
构造Span
对象是一件很简单的事情,经过opentracing对Tracer
接口的规定可知Span
是由Tracer
负责构造的,以下咱们“启动”了一个Span
(实际上只是构造了该对象而已):
Span span = tracer.buildSpan("printHello").start();
Tracer中的start方法(开启一个Span) 使用了scopeManager 来获取上下文,从而来处理父子关系;
public JaegerSpan start() { // 此处从ScopeManager获取上下文(线程)中,获取到激活的Span, 然后建立父子关系 if (this.references.isEmpty() && !this.ignoreActiveSpan && null != JaegerTracer.this.scopeManager.activeSpan()) { this.asChildOf(JaegerTracer.this.scopeManager.activeSpan()); } JaegerSpanContext context; if (!this.references.isEmpty() && ((Reference)this.references.get(0)).getSpanContext().hasTrace()) { context = this.createChildContext(); } else { context = this.createNewContext(); } ... return jaegerSpan; }
本示例中会涉及到两个Span:Parent Span 和 Child Span。咱们首先介绍 Parent Span。
其大体策略是:
reference
属性中找到该span的parent span(根据是否为child_of的关系来判断)获取其traceId做为本身的traceId,获取其spanId做为本身的parentId。具体代码以下:
private void sayHello(String helloTo, String greeting) { Span span = tracer.buildSpan("say-hello").start(); try (Scope scope = tracer.scopeManager().activate(span)) { span.setTag("hello-to", helloTo); span.setBaggageItem("greeting", greeting); String helloStr = formatString(helloTo); printHello(helloStr); } finally { span.finish(); } }
获得的运行时Span以下:
span = {JaegerSpan@1685} startTimeMicroseconds = 1598707136698000 startTimeNanoTicks = 1018098763618500 computeDurationViaNanoTicks = true tags = {HashMap@1700} size = 2 durationMicroseconds = 0 operationName = "say-hello" references = {ArrayList@1701} size = 0 context = {JaegerSpanContext@1666} "c8b87cc5fb01ef31:c8b87cc5fb01ef31:0:1" traceIdLow = -3983296680647594191 traceIdHigh = 0 spanId = -3983296680647594191 parentId = 0 flags = 1 baggage = {Collections$EmptyMap@1704} size = 0 debugId = null objectFactory = {JaegerObjectFactory@994} traceIdAsString = "c8b87cc5fb01ef31" spanIdAsString = "c8b87cc5fb01ef31" logs = null finished = false
示例代码而后在 formatString 中会:
具体代码以下:
private String getHttp(int port, String path, String param, String value) { HttpUrl url = new HttpUrl.Builder().scheme("http").host("localhost").port(port).addPathSegment(path) .addQueryParameter(param, value).build(); Request.Builder requestBuilder = new Request.Builder().url(url); Span activeSpan = tracer.activeSpan(); Tags.SPAN_KIND.set(activeSpan, Tags.SPAN_KIND_CLIENT); Tags.HTTP_METHOD.set(activeSpan, "GET"); Tags.HTTP_URL.set(activeSpan, url.toString()); tracer.inject(activeSpan.context(), Format.Builtin.HTTP_HEADERS, Tracing.requestBuilderCarrier(requestBuilder)); Request request = requestBuilder.build(); Response response = client.newCall(request).execute(); }
上文中的 tracer.inject 函数,是用来把 SpanContext 的信息序列化到 Request.Builder 之中。这样后续操做就能够把序列化以后的信息转换到 Header之中。
tracer.inject(activeSpan.context(), Format.Builtin.HTTP_HEADERS, Tracing.requestBuilderCarrier(requestBuilder));
具体序列化代码以下:
public void inject(JaegerSpanContext spanContext, TextMap carrier) { carrier.put(contextKey, encodedValue(contextAsString(spanContext))); for (Map.Entry<String, String> entry : spanContext.baggageItems()) { carrier.put(keys.prefixedKey(entry.getKey(), baggagePrefix), encodedValue(entry.getValue())); } }
当服务端返回以后,在Client端,jaeger会进行后续操做:finish,report。
调用span.finish()
方法标志着span的结束。finish方法应该是对应span实例的最后一个调用的方法。在span中finish方法还只是校验和记录的做用,真正发送span的就是开头提到的tracer,tracer包含了sampler、report等全局的功能,所以在finish中调用了tracer.report(span)
方法。而tracer中的report方法是使用其成员report
的report方法,上面讲过默认实现是RemoteReporter
,它默认使用的是UdpSender
。
span.finish会触发span上报。调用了 JaegerSpan.finishWithDuration。其中会判断本次Trace是否采样。若是是采样了,就会上报。
@Override public void finish(long finishMicros) { finishWithDuration(finishMicros - startTimeMicroseconds); } private void finishWithDuration(long durationMicros) { synchronized (this) { if (finished) { log.warn("Span has already been finished; will not be reported again."); return; } finished = true; this.durationMicroseconds = durationMicros; } if (context.isSampled()) { tracer.reportSpan(this); } }
上报是在 RemoteReporter 中。
在RemoteReporter
中有一个BlockingQueue
队列其做用是接收Command接口的实现类,其长度可在构造方法中传入。在RemoteReporter
的构造函数中开启了两个守护线程。一个线程定时往BlockingQueue
队列中添加flush命令,另一个线程不停的从BlockingQueue
队列中take数据,而后执行Command.excute()方法。而report(span)方法就是往BlockingQueue
队列中添加AppendCommand
类。
@Override public void report(JaegerSpan span) { // Its better to drop spans, than to block here boolean added = commandQueue.offer(new AppendCommand(span)); if (!added) { metrics.reporterDropped.inc(1); } }
能够看到若是返回的added变量为false,也就是队列满了没法再加入数据,就会抛弃该span的,最终该span的信息不会发送到agent中。所以队列的长度也是有必定的影响。
而AppendCommand
类的excute()方法为:
class AppendCommand implements Command { private final Span span; public AppendCommand(Span span) { this.span = span; } @Override public void execute() throws SenderException { sender.append(span); } }
因此,咱们看到,execute()方法并非真正的发送span了,而只是把span添加到sender中去,由sender实现span的发送,reporter类只负责发送刷新与发送的命令。
若是咱们继续深刻下去,会发现UdpSender
是抽象类ThriftSender
的实现类,sender.append(span)
方法调用的是ThriftSender
的append(Span)
方法,而该方法又会调用ThriftSender
的flush()
方法,最后这个flush()
方法会调用抽象类ThriftSender
的抽象方法send(Process process, List spans)
。
Jaeger中其余Reporter以下 :
CompositeReporter
顾名思义就是将各个reporter组合起来,内部有一个list,它所实现的接口的 report(Span span)
方法也只是把list中的全部reporter依次调用report(Span span)
方法而已。InMemoryReporter
类是将Span
存到内存中,该类含有一个list用于存储span,该类中的report方法即为将span经过add方法添加到list中,经过getSpans()
方法获取到list,同时有clear()
方法清除list数据。LoggingReporter
类做用是将span做为日志内容打印出来,其report方法即为log.info()
打印span的内容。NoopReporter
是一个实现了Reporter
接口可是实现方法为空的一个类,表示使用该类report span将毫无影响。服务端也是手动埋点。
public class FormatterResource { @GET public String format(@QueryParam("helloTo") String helloTo, @Context HttpHeaders httpHeaders) { Span span = Tracing.startServerSpan(tracer, httpHeaders, "format"); try (Scope scope = tracer.scopeManager().activate(span)) { String greeting = span.getBaggageItem("greeting"); if (greeting == null) { greeting = "Hello"; } String helloStr = String.format("%s, %s!", greeting, helloTo); span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); return helloStr; } finally { span.finish(); } } }
业务逻辑在 startServerSpan 之中:
具体代码以下:
public static Span startServerSpan(Tracer tracer, javax.ws.rs.core.HttpHeaders httpHeaders, String operationName) { // format the headers for extraction MultivaluedMap<String, String> rawHeaders = httpHeaders.getRequestHeaders(); final HashMap<String, String> headers = new HashMap<String, String>(); for (String key : rawHeaders.keySet()) { headers.put(key, rawHeaders.get(key).get(0)); } Tracer.SpanBuilder spanBuilder; try { SpanContext parentSpanCtx = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(headers)); if (parentSpanCtx == null) { spanBuilder = tracer.buildSpan(operationName); } else { spanBuilder = tracer.buildSpan(operationName).asChildOf(parentSpanCtx); } } catch (IllegalArgumentException e) { spanBuilder = tracer.buildSpan(operationName); } // TODO could add more tags like http.url return spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER).start(); }
解析代码以下:
public JaegerSpanContext extract(TextMap carrier) { JaegerSpanContext context = null; Map<String, String> baggage = null; String debugId = null; for (Map.Entry<String, String> entry : carrier) { // TODO there should be no lower-case here String key = entry.getKey().toLowerCase(Locale.ROOT); if (key.equals(contextKey)) { context = contextFromString(decodedValue(entry.getValue())); } else if (key.equals(Constants.DEBUG_ID_HEADER_KEY)) { debugId = decodedValue(entry.getValue()); } else if (key.startsWith(baggagePrefix)) { if (baggage == null) { baggage = new HashMap<String, String>(); } baggage.put(keys.unprefixedKey(key, baggagePrefix), decodedValue(entry.getValue())); } else if (key.equals(Constants.BAGGAGE_HEADER_KEY)) { baggage = parseBaggageHeader(decodedValue(entry.getValue()), baggage); } } if (debugId == null && baggage == null) { return context; } return objectFactory.createSpanContext( context == null ? 0L : context.getTraceIdHigh(), context == null ? 0L : context.getTraceIdLow(), context == null ? 0L : context.getSpanId(), context == null ? 0L : context.getParentId(), context == null ? (byte)0 : context.getFlags(), baggage, debugId); }
Jaeger 和 SOFATracer 对好比何?
spanId是怎么生成的,有什么规则?
traceId是怎么生成的,有什么规则?
最终都是调用到 ThreadLocalRandom # current # nextLong 完成,举例以下:
traceIdLow = -4423486945480775652 traceIdHigh = 0 spanId = 3900526584756421192 parentId = -4423486945480775652
客户端哪里生成的Span?
ParentSpan 从哪儿来?
在 客户端发送阶段,先从 scopeManager.activeSpan 获取当前活动span。若是不为空,则须要给新span设置父亲Span。
if (references.isEmpty() && !ignoreActiveSpan && null != scopeManager.activeSpan()) { asChildOf(scopeManager.activeSpan()); }
ChildSpan由ParentSpan建立,那么何时建立?
tracer.start()
方法中处理的;start()
方法中经过 scopeManager 判断是存在active span ,若存在则生成CHILD_OF关系的上下文, 若是不存在则createNewContext;Trace信息怎么传递?
服务器接收到请求以后作什么?
SpanContext在服务器端怎么处理?见上问题回答。
链路信息如何搜集?
开放分布式追踪(OpenTracing)入门与 Jaeger 实现
OpenTracing Java Library教程(3)——跨服务传递SpanContext
OpenTracing Java Library教程(1)——trace和span入门
蚂蚁金服分布式链路跟踪组件 SOFATracer 总览|剖析
蚂蚁金服开源分布式链路跟踪组件 SOFATracer 链路透传原理与SLF4J MDC 的扩展能力剖析
蚂蚁金服开源分布式链路跟踪组件 SOFATracer 采样策略和源码剖析
https://github.com/sofastack-guides/sofa-tracer-guides
The OpenTracing Semantic Specification
OpenTracing Java Library教程(2)——进程间传递SpanContext
OpenTracing Java Library教程(4)——Baggage介绍
https://github.com/yurishkuro/opentracing-tutorial
分布式链路追踪系列番外篇一(jaeger异步批量发送span)
OpenTracing-Java Scope与ScopeManager
OpenTracing实现思路(附OpenTracing-Jaeger-Java实例)
jaegeropentracing的Java-client完整分布式追踪链