[業界方案]用Jaeger來學習分散式追蹤系統Opentracing
0x00 摘要
筆者之前有過zipkin的經驗,希望擴充套件到Opentracing,於是在學習Jaeger基礎上總結出此文,與大家分享。
0x01 緣由 & 問題
1.1 選擇Jaeger
Jaeger
是 Uber
開發的一款呼叫鏈服務端產品,開發語言為 golang
,能夠相容接收 OpenTracing
格式的資料。根據其發展歷史,可以說是 Zipkin
的升級版。另外,其基於 udp
(也可以 http
)的傳輸協議,更加定位了其高效、迅速的特點。
在前文 [業界方案] 用SOFATracer學習分散式追蹤系統Opentracing ,我們使用SOFATracer來進行學習,本次我們選擇了Jaeger,這又是什麼原因?具體如下:
- Jaeger是Opentracing官方推薦的。
- Jaeger支援Opentracing高版本。
而且我們正好可以和SOFATracer進行對比印證。
1.2 問題
讓我們用問題來引導閱讀。
- Jaeger 和 SOFATracer 對比如何?
- spanId是怎麼生成的,有什麼規則?
- traceId是怎麼生成的,有什麼規則?
- 客戶端哪裡生成的Span?
- ParentSpan 從哪兒來?
- ChildSpan由ParentSpan建立,那麼什麼時候建立?
- Trace資訊怎麼傳遞?
- 伺服器接收到請求之後做什麼?
- SpanContext在伺服器端怎麼處理?
- 鏈路資訊如何蒐集?
1.3 本文討論範圍
1.3.1 Jaeger構成
Jaeger主要由以下幾部分組成:
- Jaeger Client: 為了不同語言實現了符合OpenTracing標準的SDK。應用程式通過API寫入資料, client library把trace資訊按照應用程式制定的取樣策略傳遞給jaeger-agent。
- Agent: 他是一個監聽在UDP埠上接收span資料的網路守護程式,它會將資料批量傳送給collector。他被設計成一個基礎元件,部署到所有的宿主機上。Agent將client library和collector解耦,為client library遮蔽了路由和發現collector的細節。
- Collector:接收jaeger-agent傳送來的資料,然後將資料寫入後端儲存。Collector被設計成無狀態的元件,因此使用者可以執行任意數量的Collector。
- Data Store:後端儲存被設計成一個可插拔的元件,支援資料寫入cassandra, elastic search。
- Query:接收查詢請求,然後從後端儲存系統中檢索tarce並通過UI進行展示。Query是無狀態的,可以啟動多個例項。把他們部署在nginx這樣的負載均衡器後面。
本文只討論 Jaeger Client 功能。
1.3.2 全鏈路跟蹤
全鏈路跟蹤分成三個跟蹤級別:
- 跨程式跟蹤 (cross-process)(呼叫另一個微服務)
- 資料庫跟蹤
- 程式內部的跟蹤 (in-process)(在一個函式內部的跟蹤)
本文只討論 跨程式跟蹤 (cross-process),因為跨程式跟蹤是最簡單的 ^_^。對於跨程式跟蹤,你可以編寫攔截器或過濾器來跟蹤每個請求,它只需要編寫極少的程式碼。
0x02 背景知識
因為前文已經對背景知識做了較詳細的介紹,本文只是提一下幾個必要概念。
分散式追蹤系統發展很快,種類繁多,但核心步驟一般有三個:程式碼埋點,資料儲存、查詢展示
在資料採集過程,需要侵入使用者程式碼做埋點,不同系統的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也使用相似的術語。有幾個基本概念我們需要提前瞭解清楚:
- Trace(追蹤) :Dapper 將一個呼叫過程構建成一棵呼叫樹(稱為Tracer),Tracer樹中的每個節點表示鏈路呼叫中的一個模組或系統。 通過一個全域性唯一的 traceId 來標識一個請求呼叫鏈。在廣義上,一個trace代表了一個事務或者流程在(分散式)系統中的執行過程。在OpenTracing標準中,trace是多個span組成的一個有向無環圖(DAG),每一個span代表trace中被命名並計時的連續性的執行片段。
- Span(跨度) :一個span代表系統中具有開始時間和執行時長的邏輯執行單元,即應用中的一個邏輯操作。span之間通過巢狀或者順序排列建立邏輯因果關係。一個span可以被理解為一次方法呼叫,一個程式塊的呼叫,或者一次RPC/資料庫訪問,只要是一個具有完整時間週期的程式訪問,都可以被認為是一個span。Dapper中,一個span 包含以下階段(不同軟體可能有不同的實現 ,比如有的會細分為 Client Span 和 Server Span):
- Start: 發起呼叫
- cleint send(cs): 客戶端傳送請求
- Server Recv(sr):服務端收到請求
- Server Send(ss): 服務端傳送響應
- Client Recv(cr) : 客戶端收到服務端響應
- End: 整個鏈路完成。
Client Server
+--------------+ Request +--------------+
| Client Send | +----------------> |Server Receive|
+------+-------+ +------+-------+
| |
| v
| +------+--------+
| |Server Business|
| +------+--------+
| |
| |
v v
+------+--------+ Response +------+-------+
|Client Receive | <---------------+ |Server Send |
+------+--------+ +------+-------+
| |
| |
v v
- Logs :每個span可以進行多次Logs操作,每一次Logs操作,都需要一個帶時間戳的時間名稱,以及可選的任意大小的儲存結構。比較適合記錄日誌、異常棧等一些和時間相關的資訊。
- Tags :每個span可以有多個鍵值對(key :value)形式的Tags,Tags是沒有時間戳的,支援簡單的對span進行註解和補充。記錄的資訊適用於span從建立到完成的任何時刻。再說直白點就是記錄和時間點無關的資訊,這個主要是和下面的Logs作區分。
- Baggage Items:這個主要是用於跨程式全域性傳輸資料
- SpanContext :
SpanContext
更像是一個“概念”,而不是通用 OpenTracing 層的有用功能。在建立Span
、向傳輸協議Inject
(注入)和從傳輸協議中Extract
(提取)呼叫鏈資訊時,SpanContext
發揮著重要作用。
0x03 示例程式碼
3.1 程式碼
程式碼全部來自 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");
}
}
}
3.2 dropwizard
此處雖然不是SOFATracer和Jaeger的本質區別,但是也挺有趣,即SOFATracer是使用SprintBoot來做示例程式碼,而此處是使用dropwizard來做示例。
可能有人對dropwizard不熟悉,現在大致講解如下:
- Dropwizard是
Coda Hale
在Yammer
公司時創立的,它旨在提升公司分散式系統的架構(現在叫:微服務)。雖然它最早被用來構建REST Web 服務,而現在它具備了越來越多的功能,但是它的目標始終是作為輕量化、為生產環境準備且容易使用的web框架。 - Dropwizard與Spring Boot類似,也是構建微服務可選的工具,但是它顯得比Spring Boot更加規範一些。它使用的元件一般不會做可選替換,而好處是可以不需要那麼多的修飾,比如寫基於REST的web服務。
- Dropwizard預設也不具備依賴注入的容器(像Spring或者CDI),你當然可以自行新增,但是Dropwizard推薦你把微服務弄的簡單一些,不需要這些額外的元件。
- 就像Spring Boot一樣,Dropwizard推薦將整個工程打包成一個可執行的jar,通過這種方式開發人員不用在擔心程式執行的應用伺服器是什麼,需要什麼額外的配置,應用再也不需要被構建成war包了,而且也不會有那麼多複雜層級的類載入器了。
Dropwizard在優秀的三方庫協助下,提供了不錯的抽象層,使之更有效率,更簡單的編寫生產用途的微服務。
- Servlet容器使用
Jetty
- REST/JAX-RS實現使用
Jersey
- JSON序列化使用
Jackson
- 整合
Hibernate Validator
- Guava
- Metrics
- SLF4J + Logback
- 資料訪問層上使用
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");
}
}
}
0x04 鏈路邏輯
對於一個元件來說,一次處理過程一般是產生一個 Span;這個 Span 的生命週期是從接收到請求到返回響應這段過程。
這裡需要考慮的問題是如何與上下游鏈路關聯起來呢?在 Opentracing 規範中,可以在 Tracer 中 extract 出一個跨程式傳遞的 SpanContext 。然後通過這個 SpanContext 所攜帶的資訊將當前節點關聯到整個 Tracer 鏈路中去,當然有提取(extract)就會有對應的注入(inject)。
鏈路的構建一般是 client-server-client-server 這種模式的,那這裡就很清楚了,就是會在 client 端進行注入(inject),然後再 server 端進行提取(extract),反覆進行,然後一直傳遞下去。
在拿到 SpanContext 之後,此時當前的 Span 就可以關聯到這條鏈路中了,那麼剩餘的事情就是收集當前元件的一些資料;整個過程大概分為以下幾個階段:
- 從請求中提取 spanContext
- 構建 Span,並將當前 Span 存入當前 tracer上下文中(SofaTraceContext.push(Span)) 。
- 設定一些資訊到 Span 中
- 返回響應
- Span 結束&上報
0x05 資料模型
5.1 Tracer & JaegerTracer
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;
}
5.2 Span & JaegerSpan
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核心資訊,如:traceId,spanId,parentId,baggage等
- log資訊 與tag的區別是帶有時間戳
- tag資訊
- span的其他資訊,如:startTime,duration
其中span的核心資訊儲存在SpanContext
中。
5.3 SpanContext & JaegerSpanContext
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中完成的。
5.4 Reporter
預設的 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;
}
5.5 Scope
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 來儲存;
- 建構函式就是把目前Span暫存,然後把傳入的引數Span設定為當前Span。即 將之前活動的scope作為當前scope的屬性
toRestore
來儲存,並將當前scope設定到scopeManager中作為當前執行緒最新的scope。 - 在操作當span操作完成(span.finish)時,需要呼叫scope.close方法做恢復,觸發關聯新的啟用span,否則呼叫鏈條會出錯。
具體定義如下:
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;
}
}
5.6 ScopeManager
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之中,確實在分析程式碼時候感到有些許混亂。
5.7 SpanID & TraceID
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"
0x06 啟動
6.1 手動埋點
要通過Jaeger將Java應用資料上報至鏈路追蹤控制檯,首先需要完成埋點工作。本示例為手動埋點。
6.2 pom配置
pom.xml中新增了對Jaeger客戶端的依賴。
<dependency>
<groupId>io.jaegertracing</groupId>
<artifactId>jaeger-client</artifactId>
<version>${jaeger.version}</version>
</dependency>
6.3 啟動
示例程式碼並沒有使用注入的元件,而是手動啟動,具體啟動/初始化程式碼如下:
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。
6.4 構建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
來reportSpan
到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")
0x07 客戶端傳送
下面都是手動埋點。
7.1 構建Span
構造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;
}
7.2 Parent Span
本示例中會涉及到兩個Span:Parent Span 和 Child Span。我們首先介紹 Parent Span。
其大致策略是:
- 呼叫 tracer.buildSpan("say-hello").start() 生成Span
- asChildOf(scopeManager.activeSpan()); 這裡構建了Span之間的關係,即本 span在初始化時就先構建了與之前span的關係。
- createNewContext() 或者 createChildContext()。如果是root span就隨機生成id作為traceId與spanId,如果不是root span則使用
reference
屬性中找到該span的parent span(根據是否為child_of的關係來判斷)獲取其traceId作為自己的traceId,獲取其spanId作為自己的parentId。
- 呼叫 tracer.scopeManager().activate 函式就是把span放進一個新生成的 ThreadLocalScope 中,其實就是 tlsScope 成員變數中。 結果是後續可以通過tracer.scopeManager.activeSpan();獲取span資訊。
- setTag
- setBaggageItem
- 最後finish
具體程式碼如下:
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
7.3 Child Span
示例程式碼然後在 formatString 中會:
- 生成一個子 Span
- 加入了Tag
- 呼叫Inject方法傳入Context資訊。
- 並且會呼叫http請求。
具體程式碼如下:
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();
}
7.4 Inject
上文中的 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()));
}
}
7.5 Finish
當服務端返回之後,在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);
}
}
7.6 Reporter
上報是在 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將毫無影響。
0x08 服務端接受
8.1 手動埋點
服務端也是手動埋點。
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();
}
}
}
8.2 業務邏輯
業務邏輯在 startServerSpan 之中:
- 呼叫Extract方法解析Context資訊。
- 根據是否有Parent Context 來進行Span構建,其中會用到SpanContext。
具體程式碼如下:
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();
}
8.3 解析Context
解析程式碼如下:
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);
}
0x09 問題解答
-
Jaeger 和 SOFATracer 對比如何?
- Jaeger對OpenTracing支援的更完備,版本更高。
-
spanId是怎麼生成的,有什麼規則?
-
traceId是怎麼生成的,有什麼規則?
-
最終都是呼叫到 ThreadLocalRandom # current # nextLong 完成,舉例如下:
-
traceIdLow = -4423486945480775652 traceIdHigh = 0 spanId = 3900526584756421192 parentId = -4423486945480775652
-
-
客戶端哪裡生成的Span?
- 本示例程式碼是手動呼叫 tracer.buildSpan("say-hello").start() 生成Span。
-
ParentSpan 從哪兒來?
-
在 客戶端傳送階段,先從 scopeManager.activeSpan 獲取當前活動span。如果不為空,則需要給新span設定父親Span。
-
if (references.isEmpty() && !ignoreActiveSpan && null != scopeManager.activeSpan()) { asChildOf(scopeManager.activeSpan()); }
-
-
-
ChildSpan由ParentSpan建立,那麼什麼時候建立?
- 在OpenTracing-Java實現中, 是在
tracer.start()
方法中處理的;start()
方法中通過 scopeManager 判斷是存在active span ,若存在則生成CHILD_OF關係的上下文, 如果不存在則createNewContext;
- 在OpenTracing-Java實現中, 是在
-
Trace資訊怎麼傳遞?
- 把 SpanContext 的資訊序列化到 Request.Builder 之中。後續操作把序列化之後的資訊轉換到 Header之中,然後就可以傳遞。
-
伺服器接收到請求之後做什麼?
- 呼叫Extract方法解析Context資訊。
- 根據是否有Parent Context 來進行Span構建,其中會用到SpanContext。
- 進行具體其他業務。
-
SpanContext在伺服器端怎麼處理?見上問題回答。
-
鏈路資訊如何蒐集?
- 取樣是對於整條鏈路來說的,也就是說從 RootSpan 被建立開始,就已經決定了當前鏈路資料是否會被記錄了。
- 如果已經確定本次Trace被取樣,就會傳送報告。
0xFF 參考
開放分散式追蹤(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完整分散式追蹤鏈