分布式調用跟蹤和Opentracing規範
什麼是分布式調用跟蹤?
相比傳統的“巨石”應用,微服務的一個主要變化是将應用中的不同子產品拆分為了獨立的程序。在微服務架構下,原來程序内的方法調用成為了跨程序的RPC調用。相對于單一程序的方法調用,跨程序調用的調試和故障分析是非常困難的,很難用傳統的調試器或者日志列印來對分布式調用進行檢視和分析。

如上圖所示,一個來自用戶端的請求經過了多個微服務程序。如果要對該請求進行分析,則必須将該請求經過的所有服務的相關資訊都收集起來并關聯在一起,這就是“分布式調用跟蹤”。
什麼是Opentracing?
CNCF Opentracing項目
Opentracing是CNCF(雲原生計算基金會)下的一個項目,其中包含了一套分布式調用跟蹤的标準規範,各種語言的API,程式設計架構和函數庫。Opentracing的目的是定義一套分布式調用跟蹤的标準,以統一各種分布式調用跟蹤的實作。目前已有大量支援Opentracing規範的Tracer實作,包括Jager,Skywalking,LightStep等。在微服務應用中采用Opentracing API實作分布式調用跟蹤,可以避免vendor locking,以最小的代價和任意一個相容Opentracing的基礎設施進行對接。
Opentracing概念模型
Opentracing的概念模型參見下圖:
如圖所示,Opentracing中主要包含下述幾個概念:
- Trace: 描述一個分布式系統中的端到端事務,例如來自用戶端的一個請求。
- Span:一個具有名稱和時間長度的操作,例如一個REST調用或者資料庫操作等。Span是分布式調用跟蹤的最小跟蹤機關,一個Trace由多段Span組成。
- Span context:分布式調用跟蹤的上下文資訊,包括Trace id,Span id以及其它需要傳遞到下遊服務的内容。一個Opentracing的實作需要将Span context通過某種序列化機制(Wire Protocol)在程序邊界上進行傳遞,以将不同程序中的Span關聯到同一個Trace上。這些Wire Protocol可以是基于文本的,例如HTTP header,也可以是二進制協定。
Opentracing資料模型
一個Trace可以看成由多個互相關聯的Span組成的有向無環圖(DAG圖)。下圖是一個由8個Span組成的Trace:
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C is a `ChildOf` Span A)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G `FollowsFrom` Span F)
複制
上圖的trace也可以按照時間先後順序表示如下:
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
複制
Span的資料結構中包含以下内容:
- name: Span所代表的操作名稱,例如REST接口對應的資源名稱。
- Start timestamp: Span所代表操作的開始時間
- Finish timestamp: Span所代表的操作的的結束時間
- Tags:一系列标簽,每個标簽由一個key value鍵值對組成。該标簽可以是任何有利于調用分析的資訊,例如方法名,URL等。
- SpanContext:用于跨程序邊界傳遞Span相關資訊,在進行傳遞時需要結合一種序列化協定(Wire Protocol)使用。
- References:該Span引用的其它關聯Span,主要有兩種引用關系,Childof和FollowsFrom。
- Childof: 最常用的一種引用關系,表示Parent Span和Child Span之間存在直接的依賴關系。例RPC服務端Span和RPC用戶端Span,或者資料庫SQL插入Span和ORM Save動作Span之間的關系。
- FollowsFrom:如果Parent Span并不依賴Child Span的執行結果,則可以用FollowsFrom表示。例如網上商店購物付款後會向使用者發一個郵件通知,但無論郵件通知是否發送成功,都不影響付款成功的狀态,這種情況則适用于用FollowsFrom表示。
跨程序調用資訊傳播
SpanContext是Opentracing中一個讓人比較迷惑的概念。在Opentracing的概念模型中提到SpanContext用于跨程序邊界傳遞分布式調用的上下文。但實際上Opentracing隻定義一個SpanContext的抽象接口,該接口封裝了分布式調用中一個Span的相關上下文内容,包括該Span所屬的Trace id,Span id以及其它需要傳遞到downstream服務的資訊。SpanContext自身并不能實作跨程序的上下文傳遞,需要由Tracer(Tracer是一個遵循Opentracing協定的實作,如Jaeger,Skywalking的Tracer)将SpanContext序列化後通過Wire Protocol傳遞到下一個程序中,然後在下一個程序将SpanContext反序列化,得到相關的上下文資訊,以用于生成Child Span。
為了為各種具體實作提供最大的靈活性,Opentracing隻是提出了跨程序傳遞SpanContext的要求,并未規定将SpanContext進行序列化并在網絡中傳遞的具體實作方式。各個不同的Tracer可以根據自己的情況使用不同的Wire Protocol來傳遞SpanContext。
在基于HTTP協定的分布式調用中,通常會使用HTTP Header來傳遞SpanContext的内容。常見的Wire Protocol包含Zipkin使用的b3 HTTP header,Jaeger使用的uber-trace-id HTTP Header,LightStep使用的"x-ot-span-context” HTTP Header等。Istio/Envoy支援b3 header和x-ot-span-context header,可以和Zipkin,Jaeger及LightStep對接。其中b3 HTTP header的示例如下:
X-B3-TraceId: 80f198ee56343ba864fe8b2a57d3eff7
X-B3-ParentSpanId: 05e3ac9a4f6e3b90
X-B3-SpanId: e457b5a2e4d86bd1
X-B3-Sampled: 1
複制
Istio對分布式調用跟蹤的支援
Istio/Envoy為微服務提供了開箱即用的分布式調用跟蹤功能。在安裝了Istio和Envoy的微服務系統中,Envoy會攔截服務的入向和出向請求,為微服務的每個調用請求自動生成調用跟蹤資料。通過在服務網格中接入一個分布式跟蹤的後端系統,例如zipkin或者Jaeger,就可以檢視一個分布式請求的詳細内容,例如該請求經過了哪些服務,調用了哪個REST接口,每個REST接口所花費的時間等。
需要注意的是,Istio/Envoy雖然在此過程中完成了大部分工作,但還是要求對應用代碼進行少量修改:應用代碼中需要将收到的上遊HTTP請求中的b3 header拷貝到其向下遊發起的HTTP請求的header中,以将調用跟蹤上下文傳遞到下遊服務。這部分代碼不能由Envoy代勞,原因是Envoy并不清楚其代理的服務中的業務邏輯,無法将入向請求和出向請求按照業務邏輯進行關聯。這部分代碼量雖然不大,但需要對每一處發起HTTP請求的代碼都進行修改,非常繁瑣而且容易遺漏。當然,可以将發起HTTP請求的代碼封裝為一個代碼庫來供業務子產品使用,來簡化該工作。
下面以一個簡單的網上商店示例程式來展示Istio如何提供分布式調用跟蹤。該示例程式由eshop,inventory,billing,delivery幾個微服務組成,結構如下圖所示:
eshop微服務接收來自用戶端的請求,然後調用inventory,billing,delivery這幾個後端微服務的REST接口來實作使用者購買商品的checkout業務邏輯。本例的代碼可以從github下載下傳:https://github.com/zhaohuabing/istio-opentracing-demo.git
如下面的代碼所示,我們需要在eshop微服務的應用代碼中傳遞b3 HTTP Header。
@RequestMapping(value = "/checkout")
public String checkout(@RequestHeader HttpHeaders headers) {
String result = "";
// Use HTTP GET in this demo. In a real world use case,We should use HTTP POST
// instead.
// The three services are bundled in one jar for simplicity. To make it work,
// define three services in Kubernets.
result += restTemplate.exchange("http://inventory:8080/createOrder", HttpMethod.GET,
new HttpEntity<>(passTracingHeader(headers)), String.class).getBody();
result += "<BR>";
result += restTemplate.exchange("http://billing:8080/payment", HttpMethod.GET,
new HttpEntity<>(passTracingHeader(headers)), String.class).getBody();
result += "<BR>";
result += restTemplate.exchange("http://delivery:8080/arrangeDelivery", HttpMethod.GET,
new HttpEntity<>(passTracingHeader(headers)), String.class).getBody();
return result;
}
private HttpHeaders passTracingHeader(HttpHeaders headers) {
HttpHeaders tracingHeaders = new HttpHeaders();
extractHeader(headers, tracingHeaders, "x-request-id");
extractHeader(headers, tracingHeaders, "x-b3-traceid");
extractHeader(headers, tracingHeaders, "x-b3-spanid");
extractHeader(headers, tracingHeaders, "x-b3-parentspanid");
extractHeader(headers, tracingHeaders, "x-b3-sampled");
extractHeader(headers, tracingHeaders, "x-b3-flags");
extractHeader(headers, tracingHeaders, "x-ot-span-context");
return tracingHeaders;
}
複制
在Kubernets中部署該程式,檢視Istio分布式調用跟蹤的效果。
- 首先部署Kubernets cluster,注意需要啟用API Server的Webhook選項
- 在Kubernets cluster中部署Istio,并且啟用default namespace的sidecar auto injection
- 在Kubernets cluster中部署eshop應用
git clone https://github.com/zhaohuabing/istio-opentracing-demo.git
cd istio-opentracing-demo
git checkout without-opentracing
kubectl apply -f k8s/eshop.yaml
複制
- 在浏覽器中打開位址:http://${NODE_IP}:31380/checkout ,以觸發調用eshop示例程式的REST接口。
- 在浏覽器中打開Jaeger的界面 http://${NODE_IP}:30088 ,檢視生成的分布式調用跟蹤資訊。
注意:為了能在Kubernets Cluster外部通路到Jaeger的界面,需要修改Istio的預設安裝腳本,為Jaeger Service指定一個NodePort。修改方式參見下面的代碼:
apiVersion: v1
kind: Service
metadata:
name: jaeger-query
namespace: istio-system
annotations:
labels:
app: jaeger
jaeger-infra: jaeger-service
chart: tracing
heritage: Tiller
release: istio
spec:
ports:
- name: query-http
port: 16686
protocol: TCP
targetPort: 16686
nodePort: 30088
type: NodePort
selector:
app: jaeger
複制
Jaeger用圖形直覺地展示了這次調用的詳細資訊,可以看到用戶端請求從Ingressgateway進入到系統中,然後調用了eshop微服務的checkout接口,checkout調用有三個child span,分别對應到inventory,billing和delivery三個微服務的REST接口。
使用Opentracing來傳遞分布式跟蹤上下文
Opentracing提供了基于Spring的代碼埋點,是以我們可以使用Opentracing Spring架構來提供HTTP header的傳遞,以避免這部分寫死工作。在Spring中采用Opentracing來傳遞分布式跟蹤上下文非常簡單,隻需要下述兩個步驟:
- 在Maven POM檔案中聲明相關的依賴,一是對Opentracing SPring Cloud Starter的依賴;另外由于後端接入的是Jaeger,也需要依賴Jaeger的相關jar包。
- 在Spring Application中聲明一個Tracer bean。
@Bean
public Tracer jaegerTracer() {
// The following environment variables need to set
// JAEGER_ENDPOINT="http://10.42.126.171:28019/api/traces"
// JAEGER_PROPAGATION="b3"
// JAEGER_TRACEID_128BIT="true" Use 128bit tracer id to be compatible with the
// trace id generated by istio/envoy
return Configuration.fromEnv("eshop-opentracing").getTracer();
}
複制
注意:
- Jaeger tracer預設使用的是uber-trace-id header,而Istio/Envoy不支援該header。是以需要指定Jaeger tracer使用b3 header格式,以和Istio/Envoy相容。
- Jaeger tracer預設使用64 bit的trace id, 而Istio/Envoy使用了128 bit的trace id。是以需要指定Jaeger tracer使用128 bit的trace id,以和Istio/Envoy生成的trace id相容。
部署采用Opentracing進行HTTP header傳遞的程式版本,其調用跟蹤資訊如下所示:
從上圖中可以看到,相比在應用代碼中直接傳遞HTTP header的方式,采用Opentracing進行代碼埋點後,相同的調用增加了7個Spa,這7個Span是由Opentracing的tracer生成的。雖然我們并沒有在代碼中顯示建立這些Span,但Opentracing的代碼埋點會自動為每一個REST請求生成一個Span,并根據調用關系關聯起來。
Opentracing生成的這些Span為我們提供了更詳細的分布式調用跟蹤資訊,從這些資訊中可以分析出一個HTTP調用從用戶端應用代碼發起請求,到經過用戶端的Envoy,再到服務端的Envoy,最後到服務端接受到請求各個步驟的耗時情況。從圖中可以看到,Envoy轉發的耗時在1毫秒左右,相對于業務代碼的處理時長非常短,對這個應用而言,Envoy的處理和轉發對于業務請求的處理效率基本沒有影響。
在Istio調用跟蹤鍊中加入方法級的調用跟蹤資訊
Istio/Envoy提供了跨服務邊界的調用鍊資訊,在大部分情況下,服務粒度的調用鍊資訊對于系統性能和故障分析已經足夠。但對于某些服務,需要采用更細粒度的調用資訊來進行分析,例如一個REST請求内部的業務邏輯和資料庫通路分别的耗時情況。在這種情況下,我們需要在服務代碼中進行埋點,并将服務代碼中上報的調用跟蹤資料和Envoy生成的調用跟蹤資料進行關聯,以統一呈現Envoy和服務代碼中生成的調用資料。
在方法中增加調用跟蹤的代碼是類似的,是以我們用AOP + Annotation的方式實作,以簡化代碼。 首先定義一個Traced注解和對應的AOP實作邏輯:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Traced {
}
複制
@Aspect
@Component
public class TracingAspect {
@Autowired
Tracer tracer;
@Around("@annotation(com.zhaohuabing.demo.instrument.Traced)")
public Object aroundAdvice(ProceedingJoinPoint jp) throws Throwable {
String class_name = jp.getTarget().getClass().getName();
String method_name = jp.getSignature().getName();
Span span = tracer.buildSpan(class_name + "." + method_name).withTag("class", class_name)
.withTag("method", method_name).start();
Object result = jp.proceed();
span.finish();
return result;
}
}
複制
然後在需要進行調用跟蹤的方法上加上Traced注解:
@Component
public class DBAccess {
@Traced
public void save2db() {
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複制
@Component
public class BankTransaction {
@Traced
public void transfer() {
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複制
demo程式的master branch已經加入了方法級代碼跟蹤,可以直接部署。
git checkout master
kubectl apply -f k8s/eshop.yaml
複制
效果如下圖所示,可以看到trace中增加了transfer和save2db兩個方法級的Span。
可以打開一個方法的Span,檢視詳細資訊,包括Java類名和調用的方法名等,在AOP代碼中還可以根據需要添加出現異常時的異常堆棧等資訊。
總結
Istio/Envoy為微服務應用提供了分布式調用跟蹤功能,提高了服務調用的可見性。我們可以使用Opentracing來代替應用寫死,以傳遞分布式跟蹤的相關http header;還可以通過Opentracing将方法級的調用資訊加入到Istio/Envoy預設提供的調用鍊跟蹤資訊中,以提供更細粒度的調用跟蹤資訊。
下一步
除了同步調用之外,異步消息也是微服務架構中常見的一種通信方式。在下一篇文章中,我将繼續利用eshop demo程式來探讨如何通過Opentracing将Kafka異步消息也納入到Istio的分布式調用跟蹤中。
參考資料
- 本文中eshop示例程式的源代碼
- Opentracing docs
- Opentracing specification
- Opentracing wire protocols
- Istio Trace context propagation
- Using OpenTracing with Istio/Envoy
- Zipkin-b3-propagation
- Istio 調用鍊埋點原理剖析—是否真的“零修改”?
- OpenTracing Project Deep Dive