筆者以前有過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完整分佈式追蹤鏈