很多情況,trace
是分佈在不同的應用中的,最常用的遠端呼叫方式就是Http
。
在這種情況下,我們通常通過增加額外的Http Header
傳遞Trace資訊,然後將其組織起來。
本部分通過構建一個目前最火的SpringBoot
服務端,然後通過OkHttp3
進行呼叫,來展示分散式呼叫鏈的組織方式。
需要的知識:
- 建立一個簡單的SpringBoot應用
- 使用OkHttp3發起一個Post請求
- 瞭解OpenTracing的inject和extract函式
inject & extract函式
這是兩個為了跨程式追蹤而生的兩個函式,力求尋找一種通用的trace傳輸方式。這是兩個強大的函式,它進行了一系列抽象,使得OpenTracing協議不用和特定的實現進行耦合。
- Carrier 攜帶trace資訊的載體,下文中將自定義一個
- inject 將額外的資訊
注入
到相應的載體中 - extract 將額外的資訊從載體中
提取
出來
其實,這個載體大多數都是用一個Map(具體是text map)來實現;或者是其他二進位制方式實現。
在本文中,我們就是用了text map,載體的底層就是http頭資訊(也可以通過request params進行傳遞)。
建立一個Server端
maven依賴
首先,通過bom方式import進spring boot的相關配置。
- spring-boot-dependencies 2.1.3.RELEASE
然後,引入其他依賴
- opentracing-util 0.32.0
- jaeger-client 0.35.0
- logback-classic 1.2.3
- spring-boot-starter-web 2.1.3.RELEASE
- okhttp 3.14.1
SpringBoot應用
建立一個SpringBoot應用,埠指定為8888,並初始化預設的Tracer
。
@SpringBootApplication
@EnableAutoConfiguration
@ComponentScan(basePackages = { "com.sayhiai.example.jaeger.totorial03.controller",
})
public class App extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Bean
public JaegerTracer getJaegerTracer() {
return JaegerTracerHelper.initTracer("LoveYou");
}
}
複製程式碼
在controller目錄下建立一個簡單的服務/hello
,通過request body傳遞引數。
關鍵程式碼如下:
@PostMapping("/hello")
@ResponseBody
public String hello(@RequestBody String name,HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String header = headerNames.nextElement();
headers.put(header, request.getHeader(header));
}
System.out.println(headers);
Tracer.SpanBuilder builder = null;
SpanContext parentSpanContext = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(headers));
if (null == parentSpanContext) {
builder = tracer.buildSpan("hello");
} else {
builder = tracer.buildSpan("hello").asChildOf(parentSpanContext);
}
Span span = builder.start();
複製程式碼
首先拿到頭資訊,並進行extract
,如果得到的SpanContext
不為空,則代表當前的請求是另外一個應用發起的。在這種情況下,我們把請求的來源,作為當前請求的parent
。
使用Curl進行呼叫,確保服務能正常執行。
curl -XPOST http://localhost:8888/hello -H "Content-Type:text/plain;charset=utf-8" -d "小姐姐味道"
複製程式碼
建立OkHttp3客戶端呼叫
建立載體
OkHttp3是一個非常輕量級的類庫,它的header資訊可以通過以下程式碼設定。
Request.Builder builder;
builder.addHeader(key, value);
複製程式碼
我們在上面提到,將要建立一個自定義的Carrier
,這裡通過繼承TextMap
,來實現一個。
public class RequestBuilderCarrier implements io.opentracing.propagation.TextMap {
private final Request.Builder builder;
RequestBuilderCarrier(Request.Builder builder) {
this.builder = builder;
}
@Override
public Iterator<Map.Entry<String, String>> iterator() {
throw new UnsupportedOperationException("carrier is write-only");
}
@Override
public void put(String key, String value) {
builder.addHeader(key, value);
}
}
複製程式碼
發起呼叫
使用OkHttp3發起一個簡單的Post請求即可。
public static void main(String[] args) {
Tracer tracer = JaegerTracerHelper.initTracer("Main");
String url = "http://localhost:8888/hello";
OkHttpClient client = new OkHttpClient();
Request.Builder request = new Request.Builder()
.url(url)
.post(RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "小姐姐味道"));
Span span = tracer.buildSpan("okHttpMainCall").start();
Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_CLIENT);
Tags.HTTP_METHOD.set(span, "POST");
Tags.HTTP_URL.set(span, url);
tracer.activateSpan(span);
tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new RequestBuilderCarrier(request));
client.newCall(request.build()).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(response.body().string());
}
});
span.finish();
}
複製程式碼
注意,在方法中間,我們使用inject函式,將trace資訊附著在RequestBuilderCarrier
上進行傳遞。
這兩個函式,使用的就是jaeger的實現。見:
io.jaegertracing.internal.propagation.TextMapCodec
複製程式碼
執行Main方法,檢視Jaeger的後臺,可以看到,我們的分散式Trace已經生成了。
End
本文展示了建立分散式呼叫鏈的一般方式。類比此法,可以很容易的寫出基於HttpClient
元件的客戶端元件。
接下來,我們將使用Spring的拿手鐗Aop,來封裝通過Feign介面呼叫的SpringCloud服務。你會發現,實現一個類似Sleuth的客戶端收集器,還是蠻簡單的。