洞若觀火:使用OpenTracing增強Istio的調用鏈跟蹤

出處:https://zhaohuabing.com/post/2019-06-22-using-opentracing-with-istio

頭條號:

大數據與雲原生

微信公眾號:大數據與雲原生

創作不易,在滿足創作共用版權協議的基礎上可以轉載,但請以超鏈接形式註明出處。

為了方便閱讀,微信公眾號已按分類排版,後續的文章將在移動端首發,想學習WebAssembly相關知識,請關注我

分佈式調用跟蹤和Opentracing規範

什麼是分佈式調用跟蹤?

相比傳統的“巨石”應用,微服務的一個主要變化是將應用中的不同模塊拆分為了獨立的進程。在微服務架構下,原來進程內的方法調用成為了跨進程的RPC調用。相對於單一進程的方法調用,跨進程調用的調試和故障分析是非常困難的,很難用傳統的調試器或者日誌打印來對分佈式調用進行查看和分析。

洞若觀火:使用OpenTracing增強Istio的調用鏈跟蹤

上圖所示,一個來自客戶端的請求經過了多個微服務進程。如果要對該請求進行分析,則必須將該請求經過的所有服務的相關信息都收集起來並關聯在一起,這就是“分佈式調用跟蹤”。

什麼是Opentracing?

CNCF Opentracing項目

Opentracing是CNCF(雲原生計算基金會)下的一個項目,其中包含了一套分佈式調用跟蹤的標準規範,各種語言的API,編程框架和函數庫。Opentracing的目的是定義一套分佈式調用跟蹤的標準,以統一各種分佈式調用跟蹤的實現。目前已有大量支持Opentracing規範的Tracer實現,包括Jager,Skywalking,LightStep等。在微服務應用中採用Opentracing API實現分佈式調用跟蹤,可以避免vendor locking,以最小的代價和任意一個兼容Opentracing的基礎設施進行對接。

Opentracing概念模型

Opentracing的概念模型參見下圖:

洞若觀火:使用OpenTracing增強Istio的調用鏈跟蹤

圖所示,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:

<code>        [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)/<code>

上圖的trace也可以按照時間先後順序表示如下:

<code>––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]/<code>

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之間存在直接的依賴關係。例PRC服務端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的示例如下:

<code>X-B3-TraceId: 80f198ee56343ba864fe8b2a57d3eff7
X-B3-ParentSpanId: 05e3ac9a4f6e3b90
X-B3-SpanId: e457b5a2e4d86bd1
X-B3-Sampled: 1/<code>

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幾個微服務組成,結構如下圖所示:shop微服務接收來自客戶端的請求,然後調用inventory,billing,delivery這幾個後端微服務的REST接口來實現用戶購買商品的checkout業務邏輯。本例的代碼可以從github下載:https://github.com/zhaohuabing/istio-opentracing-demo.git

洞若觀火:使用OpenTracing增強Istio的調用鏈跟蹤

如下面的代碼所示,我們需要在eshop微服務的應用代碼中傳遞b3 HTTP Header。

<code> @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 += "
"; result += restTemplate.exchange("http://billing:8080/payment", HttpMethod.GET, new HttpEntity<>(passTracingHeader(headers)), String.class).getBody(); result += "
"; 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; } /<code>

在Kubernets中部署該程序,查看Istio分佈式調用跟蹤的效果。

  • 首先部署Kubernets cluster,注意需要啟用API Server的Webhook選項
  • 在Kubernets cluster中部署Istio,並且啟用default namespace的sidecar auto injection
  • 在Kubernets cluster中部署eshop應用
<code>git clone https://github.com/zhaohuabing/istio-opentracing-demo.git
kubectl apply -f istio-opentracing-demo/k8s/eshop.yaml/<code>
  • 在瀏覽器中打開地址:http://${NODE_IP}:31380/checkout ,以觸發調用eshop示例程序的REST接口。
  • 在瀏覽器中打開Jaeger的界面,查看生成的分佈式調用跟蹤信息。

注意:為了能在Kubernets Cluster外部訪問到Jaeger的界面,需要修改Istio的缺省安裝腳本,為Jaeger Service指定一個NodePort。修改方式參見下面的代碼:

<code>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/<code>

Jaeger用圖形直觀地展示了這次調用的詳細信息,可以看到客戶端請求從Ingressgateway進入到系統中,然後調用了eshop微服務的checkout接口,checkout調用有三個child span,分別對應到inventory,billing和delivery三個微服務的REST接口。

洞若觀火:使用OpenTracing增強Istio的調用鏈跟蹤

使用Opentracing來傳遞分佈式跟蹤上下文

Opentracing提供了基於Spring的代碼埋點,因此我們可以使用Opentracing Spring框架來提供HTTP header的傳遞,以避免這部分硬編碼工作。在Spring中採用Opentracing來傳遞分佈式跟蹤上下文非常簡單,只需要下述兩個步驟:

  • 在Maven POM文件中聲明相關的依賴,一是對Opentracing SPring Cloud Starter的依賴;另外由於後端接入的是Jaeger,也需要依賴Jaeger的相關jar包。
  • 在Spring Application中聲明一個Tracer bean。
<code>    @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();
    }/<code>

注意:

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傳遞的程序版本,其調用跟蹤信息如下所示:

洞若觀火:使用OpenTracing增強Istio的調用鏈跟蹤

從上圖中可以看到,相比在應用代碼中直接傳遞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實現邏輯:

<code>@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Traced {
}/<code>
<code>@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;
    }
}/<code>

然後在需要進行調用跟蹤的方法上加上Traced註解:

<code>@Component
public class DBAccess {

    @Traced
    public void save2db() {
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}/<code>
<code>@Component
public class BankTransaction {
    @Traced
    public void transfer() {
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}/<code>

效果如下圖所示,可以看到trace中增加了transfer和save2db兩個方法級的Span。

洞若觀火:使用OpenTracing增強Istio的調用鏈跟蹤

以打開一個方法的Span,查看詳細信息,包括Java類名和調用的方法名等,在AOP代碼中還可以根據需要添加出現異常時的異常堆棧等信息。

洞若觀火:使用OpenTracing增強Istio的調用鏈跟蹤

總結

Istio/Envoy為微服務應用提供了分佈式調用跟蹤功能,提高了服務調用的可見性。我們可以使用Opentracing來代替應用硬編碼,以傳遞分佈式跟蹤的相關http header;還可以通過Opentracing將方法級的調用信息加入到Istio/Envoy缺省提供的調用鏈跟蹤信息中,以提供更細粒度的調用跟蹤信息。

下一步

除了同步調用之外,異步消息也是微服務架構中常見的一種通信方式。在下一篇文章中,我將繼續利用eshop demo程序來探討如何通過Opentracing將Kafka異步消息也納入到Istio的分佈式調用跟蹤中。

參考資料

  1. 本文中eshop示例程序的源代碼:https://github.com/zhaohuabing/istio-opentracing-demo
  2. Opentracing docs:https://github.com/opentracing/specification/blob/master/specification.md
  3. Opentracing specification:https://github.com/opentracing/specification/blob/master/specification.md
  4. Opentracing wire protocols:https://github.com/opentracing/specification/blob/master/rfc/trace_identifiers.md
  5. Istio Trace context propagation:https://istio.io/docs/tasks/telemetry/distributed-tracing/overview/#trace-context-propagation
  6. Using OpenTracing with Istio/Envoy:https://medium.com/jaegertracing/using-opentracing-with-istio-envoy-d8a4246bdc15
  7. Zipkin-b3-propagation:https://github.com/apache/incubator-zipkin-b3-propagation
  8. Istio 調用鏈埋點原理剖析—是否真的“零修改”?:https://www.infoq.cn/article/pqy*PFPhox9OQQ9iCRTt
  9. OpenTracing Project Deep Dive:https://www.youtube.com/watch?v=ySR_FVNX4bQ&t=184s


分享到:


相關文章: