趙化冰,騰訊雲高級工程師,Istio Member,ServiceMesher 管理委員,Istio 項目貢獻者,熱衷於開源、網絡和雲計算。目前主要從事服務網格的開源和研發工作。
引言
TCM(Tencent Cloud Mesh)是騰訊雲上提供的基於Istio 進行增強,和 Istio API 完全兼容的 Service Mesh 托管服務,可以幫助用戶以較小的遷移成本和維護代價快速利用到 Service Mesh 提供的流量管理和服務治理能力。本系列文章將介紹 TCM 上的最佳實踐,本文將介紹如何利用 Spring 和 OpenTracing 簡化應用程序的Tracing 上下文傳遞,以及如何在 Istio 提供的進程間調用跟蹤基礎上實現方法級別的細粒度調用跟蹤。
分布式調用跟蹤和 OpenTracing 規范
什么是分布式調用跟蹤?
相比傳統的“巨石”應用,微服務的一個主要變化是將應用中的不同模塊拆分為了獨立的進程。在微服務架構下,原來進程內的方法調用成為了跨進程的RPC調用。相對於單一進程的方法調用,跨進程調用的調試和故障分析是非常困難的,很難用傳統的調試器或者日志打印來對分布式調用進行查看和分析。
如上圖所示,一個來自客戶端的請求經過了多個微服務進程。如果要對該請求進行分析,則必須將該請求經過的所有服務的相關信息都收集起來並關聯在一起,這就是“分布式調用跟蹤”。
什么是OpenTracing?
CNCF OpenTracing項目
OpenTracing是CNCF(雲原生計算基金會)下的一個項目,其中包含了一套分布式調用跟蹤的標准規范,各種語言的API,編程框架和函數庫。OpenTracing的目的是定義一套分布式調用跟蹤的標准,以統一各種分布式調用跟蹤的實現。目前已有大量支持OpenTracing規范的Tracer實現,包括Jager,Skywalking,LightStep等。在微服務應用中采用OpenTracing API實現分布式調用跟蹤,可以避免vendor locking,以最小的代價和任意一個兼容OpenTracing的基礎設施進行對接。
OpenTracing概念模型
OpenTracing的概念模型參見下圖:
圖源自 https://opentracing.io/
如圖所示,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/aeraki-framework/method-level-tracing-with-istio
如下面的代碼所示,我們需要在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;
}
下面我們來測試一下eshop實例程序。我們可以自己搭建一個Kubernetes集群並安裝Istio以用於測試。這里為了方便,直接使用騰訊雲上提供的全托管的服務網格 TCM,並在創建的 Mesh 中加入了一個容器服務TKE 集群來進行測試。
在 TKE 集群中部署該程序,查看Istio分布式調用跟蹤的效果。
git clone git@github.com:aeraki-framework/method-level-tracing-with-istio.git
cd method-level-tracing-with-istio
git checkout without-opentracing
kubectl apply -f k8s/eshop.yaml
- 在瀏覽器中打開地址:http://${INGRESS_EXTERNAL_IP}/checkout ,以觸發調用eshop示例程序的REST接口。
- 在瀏覽器中打開 TCM 的界面,查看生成的分布式調用跟蹤信息。
TCM 圖形界面直觀地展示了這次調用的詳細信息,可以看到客戶端請求從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的依賴;另外由於Istio 采用了Zipkin的上報接口,我們也需要引入Zipkin的相關依賴。
- 在Spring Application中聲明一個Tracer bean。如下所示,注意我們需要把 Istio 中的zpkin上報地址設置到OKHttpSernder中。
@Bean
public io.opentracing.Tracer zipkinTracer() {
String zipkinEndpoint = System.getenv("ZIPKIN_ENDPOINT");
if (zipkinEndpoint == null || zipkinEndpoint == ""){
zipkinEndpoint = "http://zipkin.istio-system:9411/api/v2/spans";
}
OkHttpSender sender = OkHttpSender.create(zipkinEndpoint);
Reporter spanReporter = AsyncReporter.create(sender);
Tracing braveTracing = Tracing.newBuilder()
.localServiceName("my-service")
.propagationFactory(B3Propagation.FACTORY)
.spanReporter(spanReporter)
.build();
Tracing braveTracer = Tracing.newBuilder()
.localServiceName("spring-boot")
.spanReporter(spanReporter)
.propagationFactory(B3Propagation.FACTORY)
.traceId128Bit(true)
.sampler(Sampler.ALWAYS_SAMPLE)
.build();
return BraveTracer.create(braveTracer);
}
部署采用OpenTracing進行HTTP header傳遞的程序版本,其調用跟蹤信息如下所示:
從上圖中可以看到,相比在應用代碼中直接傳遞HTTP header的方式,采用OpenTracing進行代碼埋點后,相同的調用增加了7個名稱前綴為spring-boot的Span,這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的分布式調用跟蹤中。