Opentelemetry 核心原理篇 :怎麼理解分佈式鏈路追蹤技術?

爲什麼需要鏈路追蹤

在學習分佈式鏈路追蹤之前,我們需要先理解這項技術產生的背景,以及它能夠幫我們解決哪些棘手問題。

提到分佈式鏈路追蹤,我們要先提到微服務。相信很多人都接觸過微服務,這裏再回顧一下基本概念。

微服務是一種開發軟件的架構和組織方法,它側重將服務解耦,服務之間通過 API 通信。使應用程序更易於擴展和更快地開發,從而加速新功能上線。

微服務演變 - 亞馬遜雲

加速研發快速迭代,讓微服務在業務驅動的互聯網領域全面普及,獨領風騷。但是,隨之而來也產生了新問題:當生產系統面對高併發,或者解耦成大量微服務時,以前很容易就能實現的監控、預警、定位故障就變困難了。

我拿研發的搜索系統的經歷,結合場景給大家聊聊(曾經這套系統要去抗峯值到日均 10 億 PV、5 千萬 UV 的高併發)。

機票搜索演示圖

比如搜索機票這樣一個行爲,其實是對上百個查詢服務器發起了一個查詢,這個查詢會被髮送到多個微服務系統,這些微服務系統分別用來處理航班信息、查詢有無艙位、機票價格、機場關鍵字匹配,查找圖片資源等等。每個子系統的查詢最終聚合得到的結果,會彙總到搜索結果頁上。

用戶一次查詢,做了這麼一個 “全局搜索”。任何一個子系統變慢,都會導致最終的搜索變得慢,那用戶體驗就會很差了。

看到這裏,你可能會想,體驗差我們做搜索優化不就好了麼?確實如此,但一般情況,一個前端或者後端工程師雖然知道系統查詢耗時,但是他無從知曉這個問題到底是由哪個服務調用造成的,或者爲什麼這個調用性能差強人意。

首先,這個工程師可能無法準確定位到這次全局搜索是調用了哪些服務,因爲新的服務、乃至服務上的某個功能,都有可能在任何時間上過線或修改過。其次,你不能苛求這個工程師對所有參與這次全局搜索的服務都瞭如指掌,每一個服務都有可能是由不同的團隊開發或維護的。再次,搜索服務還同時還被其他客戶端使用,比如手機端,這次全局搜索的性能問題甚至有可能是由其他應用造成的。

這是過去我的團隊面臨的問題,微服務讓我們的工程師觀察用戶行爲,定位服務故障很棘手。

什麼是分佈式鏈路追蹤

剛纔說的情況,我們迫切需要一些新工具,幫我們理解微服務分佈式系統的行爲、精準分析性能問題。於是,分佈式系統下鏈路追蹤技術(Distributed Tracing)出現了。

它的核心思想是:在用戶一次請求服務的調⽤過程中,無論請求被分發到多少個子系統中,子系統又調用了更多的子系統,我們把系統信息和系統間調用關係都追蹤記錄下來。最終把數據集中起來可視化展示。它會形成一個有向圖的鏈路,看起來像下面這樣。

電商系統的鏈路追蹤圖

後來,鏈路追蹤技術相關係統慢慢成熟,湧現了像 Dapper、Zipkin、HTrace、OpenTelemetry 等優秀開源系統。他們也被業界,特別是互聯網普遍採用。

目前 Dapper(誕生於 Google 團隊)應用影響最大,OpenTelemetry 已經成爲最新業界標準。

鏈路 Trace 的核心結構

快速入門

我們看看一個例子,某商家給(顧客)開賬單(要求付款),在系統中大體的流程:

一個開賬單的例子

當商家從 client 發起開賬單服務,請求從 client 程序先後進行了一系列操作:

例子中,我們把開賬單服務一個流程或者叫一個事務稱爲 Trace。這裏面有幾個操作,分別是請求網關、身份認證、生成賬單、加載資源,我們把每個操作(Operation)稱爲一個 Span。

Trace 數據模型

我們看看 Trace 廣義的定義:Trace 是多個 Span 組成的一個有向無環圖(DAG),每一個 Span 代表 Trace 中被命名並計時的連續性的執行片段。我們一般用這樣數據模型描述 Trace 和 Span 關係:

              [Span user click]  ←←←(the root Span)
                       |         
                 [Span gateway]  
                       |
     +------+----------+-----------------------+
     |                 |                       |
 [Span auth]      [Span billing]     [Span loading resource]

開賬單 Trace 數據模型

數據模型包含了 Span 之間的關係。Span 定義了父級 Span,子 Span 的概念。一個父級的 Span 會並行或者串行啓動多個子 Span。圖三,Gateway 就是 auth、billing 的父級 Span。

上面這種圖對於看清各組件的組合關係是很有用的,但是,它不能很好顯示組件的調用時間,是串行調用還是並行調用。另外,這種圖也無法顯示服務調用的時間和先後順序。因此,在鏈路追蹤系統會用另一種圖展現一個典型的 Trace 過程,如下面所示:

這種展現方式增加顯示了執行時間的上下文,相關服務間的層次關係,任務的串行或並行調用關係。這樣的視圖有助於發現系統調用的關鍵路徑。通過關注關鍵路徑的執行過程,項目團隊可能專注於優化路徑中的關鍵位置,最大幅度提升系統性能。例如:可以通過追蹤一個用戶請求訪問鏈路,觀察底層的子系統調用情況,發現哪些操作有耗時重要關注優化。

Span 基本結構

前面提到 Span 通俗概念:一個操作,它代表系統中一個邏輯運行單元。Span 之間通過嵌套或者順序排列建立因果關係。Span 包含以下對象:

可觀測平臺下的 Span

屬性 Attributes:

我們分析一個 Trace,通過 Span 裏鍵值對 <K,V> 形式的 Attributes 獲取基本信息。爲了統一約定,Span 提供了基礎的 Attributes。比如,Span 有下面常用的 Attributes 屬性:

這些 Attributes 記錄了啓動一個 Span 後相關線程信息。考慮到系統可以是不同開發語言,相應還會記錄相關語言平臺信息。下面是不同語言開發的平臺獲取線程 Id、Name 方法:

記錄線程信息,對於我們排查問題時候非常必要的,當出現一個程序異常,我們至少要知道它什麼語言開發,找到對於研發工程師。研發工程師往往需要線程相關信息快速定位錯誤棧。

Span 關係描述 Links:

我們看看之前 Span 數據模型:

                [Span gateway]
                   |     
     +------+------+------------------+ 
     |             |                  |
 [Span auth]  [Span billing]     [Span loading resource]

一個 Trace 有向無環圖,Span 是圖的節點,鏈接就是節點間的連線。可以看到一個 Span 節點可以有多個 Link,這代表它有多個子 Span。

Trace 定義了 Span 間兩種基本關係:

Span 上下文信息 SpanContext:

字面理解 Span 上下文對象。它作用就是在一個 Trace 中,把當前 Span 和 Trace 相關信息傳遞到下級 Span。它的基本結構類似 <Trace_id, Span_id, sampled> ,每個 SpanContext 包含以下基本屬性:

Trace 鏈路傳遞初探

在一個鏈路追蹤過程中,我們一般會有多個 Span 操作,爲了把調用鏈狀態在 Span 中傳遞下去,期望最終保存下來,比如打入日誌、保存到數據庫。SpanContext 會封裝一個鍵值對集合,然後將數據像行李一樣打包,這個打包的行李 OpenTelemetry 稱爲 Baggage(揹包)。

Baggage 會在一條追蹤鏈路上的所有 Span 內全局傳輸。在這種情況下,Baggage 會隨着整個鏈路一同傳播。我們可以通過 Baggage 實現強大的追蹤功能。

方便理解,我們用開賬單服務演示 Baggage 效果:

首先,我們在 LoadBalancer 請求中加一個 Baggage,LoadBalancer 請求了 source 服務。

@GetMapping("/loadBalancer")
@ResponseBody
public String loadBalancer(String tag){
    Span span = Span.current();   
    //保存 Baggage
 Baggage.current()
   .toBuilder()
   .put("app.username""蔣志偉")
   .build()
   .makeCurrent();
......
    ##請求 resource
httpTemplate.getForEntity(APIUrl+"/resource",String.class).getBody();

然後我們從 resource 服務中獲取 Baggage 信息,並把它存儲到 Span 的 Attributes 中。

@GetMapping("/resource")
@ResponseBody
public String resource(){
 String baggage = Baggage.current().getEntryValue("app.username");
 Span spanCur = Span.current(); 
    ##獲取當前的 Span,把 Baggage 寫的 resource
 spanCur.setAttribute("app.username", 
                         "baggage 傳遞過來的 value: "+baggage);

最終,我們從跟蹤系統的鏈路 UI 中點擊 source 這個 Span,找到傳遞的 Baggage 信息。

展示 Baggage 的傳遞

當然,Baggage 擁有強大功能,也會有很大的消耗。由於 Baggage 的全局傳輸,每個鍵值都會被拷貝到每一個本地(local)及遠程的子 Span,如果包含的數量量太大,或者元素太多,它將降低系統的吞吐量或增加 RPC 的延遲。

鏈路添加業務監控

我們進行系統鏈路追蹤,除了 Trace 本身自帶信息,如果我們還希望添加自己關注的監控。Trace 支持用打標籤 Tags 方式來實現。Tags 本質還是 Span 的 Attributes(在 OpenTelemetry 定義中,統稱 Attributes。在 Prometheus、Jaeger 裏面沿襲 Tags 老的叫法)。

打 Tags 的過程,其實就是在 Span 添加我們自定義的 Attributes 信息,這些 Tags 大部分和我們業務息息相關,爲了更方便做業務監控、分析業務。

我們看一個 Java 打 Tags 的例子:頁面定義好了一個 Tag,名字叫 “username”。我們輸入 Tags 的值,然後把 Tags 通過一個 HTTP 請求發送給付賬單的 API。

打 Tags 的演示

API 獲取 Tags 後,把它保存到當前 Span 的 Attribute 中。這個 Span 對應的是代碼裏面的一個 Gateway 方法,如果不重名 Span 名稱,默認使用 Gateway 作爲 Span 名稱。

@GetMapping("/loadBalancer")
public String gateway(String tag){
   Span Span = Span.current();  
   ##獲取當前 Span,添加 username 的 tag
   Span.setAttribute("username", tag);
   ...... }

打了 Tags 後,我們可以在跟蹤系統搜索 Tag 關鍵字。通過 Tag 可以快速找到對應的 Trace。

基於 Tags 的搜索

可以看到,根據 Tags 的 key 我們可以很方便篩選想要的 Span 信息。實際場景裏,我們面臨是從成千上萬鏈路中快速定位訪問異常的請求。打 Tags 對我們診斷程序非常有幫助。

Baggage 和 Span Tags 的區別:

全鏈路兼容性考慮

在不同的平臺、不同的開發語言、不同的部署環境 (容器非容器)下,爲了保證底層追蹤系統實現兼容性,將監控數據記錄到一個可插拔的 Tracer 上。在絕大部分通用的應用場景下,追蹤系統考慮使用某些高度共識的鍵值對,從而對診斷應用系統更有兼容,通用性。

這個共識稱爲語義約定 Semantic conventions。

你會從下面一些語義約定看出 Trace 做了哪些兼容性。

例如,我們訪問 HTTP 的應用服務器。應用系統處理請求中的 URL、IP、HTTP 動作(get/post 等)、返回碼,對於應用系統的診斷是非常有幫助的。監控者可以選擇 HTTP 約定參數記錄系統狀態,像下面 Trace 展示的結果。

Trace 默認的語義約定

鏈路數據如何傳播

在 Trace 傳遞中有一個核心的概念,叫 Carrier(搬運工具)。它表示 “搬運”Span 中 SpanContext 的工具。比方說 Trace 爲了把 Span 信息傳遞下去,在 HTTP 調用場景中,會有 HttpCarrier,在 RPC 的調用場景中會有 RpcCarrier 來搬運 SpanContext。Trace 通過 Carrier 可以把鏈路追蹤狀態從一個進程“搬運” 到另一個進程裏。 

數據傳播基本操作

爲了更清晰看懂數據傳播的過程,我們先了解 Span 在傳播中有的基本操作:

1、StartSpan:Trace 在具體操作中自動生成一個 Span

2、Inject 注入:將 Span 的 SpanContext 寫入到 Carrier 的過程

鏈路數據爲了進行網絡傳輸,需要數據進行序列化和反序列化。這個過程 Trace 通過一個負責數據序列化反序列化上下文的 Formatter 接口實現的。例如在 HttpCarrier 使用中通常就會有一個對應的 HttpFormatter。所以 Inject 注入是委託給 Formatter 將 SpanContext 進行序列化寫入 Carrier。

Formatter 提供不同場景序列化的數據格式,叫做 Format 描述。比如:

一個 Python 程序實現 Inject 注入過程,Formatter 序列化 SpanContext 成 Text Map 格式。

##Trace 生成一個span
    tracer = Tracer()
    span = tracer.start_span(operation_name='test')
    tracer.inject(
        span_context=span.context,
        format=Format.TEXT_MAP,
        carrier=carrier)

3、Extract 提取:將 SpanContext 從 Carrier 中 Extract(提取出來)。

span_ctx = tracer.extract(format=Format.TEXT_MAP, carrier={})

同理,從 Carrier 提取的過程也需要委託 Formatter 將 SpanContext 反序列化。

運行原理

鏈路數據在 HTTP 傳遞

我們基於 HTTP 通信解釋傳播原理。由圖一,這個過程大致分爲兩步:

1、發送端將 SpanContext 注入到請求中,相應僞代碼實現

/**
** 將 SpanContext 中的 TraceId,SpanId,Baggage 等根據 format 參數注入到請求中(Carrier)
** carrier := opentracing.HTTPHeadersCarrier(httpReq.Header)
** err := Tracer.Inject(Span.Context(), opentracing.HTTPHeaders, carrier)
**/
Inject(sm SpanContext, format interface{}, carrier interface{}) error

2、接收端從請求中解析出 SpanContext,相應僞代碼實現

// Inject() takes the `sm` SpanContext instance and injects it for
// propagation within `carrier`. The actual type of `carrier` depends on
/** 根據 format 參數從請求(Carrier)中解析出 SpanContext(包括 TraceId、SpanId、baggage)。
** 例如: 
**  carrier := opentracing.HTTPHeadersCarrier(httpReq.Header)
**  clientContext, err := Tracer.Extract(opentracing.HTTPHeaders, carrier)
**/
Extract(format interface{}, carrier interface{}) (SpanContext, error)

Carrier 負責將追蹤狀態從一個進程 “Carry”(搬運)到另一個進程。對於一個 Carrier,如果已經被 Injected,那麼它也可以被 Extracted(提取),從而得到一個 SpanContext 實例。這個 SpanContext 代表着被 Injected 到 Carrier 的信息。

說到這裏,你可能想知道這個 Carrier 在 HTTP 中具體在哪。其實它就保存到 HTTP 的 Headers 中。而且,W3C 組織爲 HTTP 支持鏈路追蹤專門在 Headers 中定義了 Trace 標準:

https://www.w3.org/TR/trace-context/#trace-context-http-headers-format

W3C 組織是對網絡標準制定的一個非盈利組織,W3C 是萬維網聯盟的縮寫,像 HTML、XHTML、CSS、XML 的標準就是由 W3C 來定製。

跨進程間傳播數據

數據傳播按照場景分爲兩類:進程內傳播、跨進程間傳播 Cross-Process-Tracing。

進程內傳播是指 Trace 在一個服務內部傳遞,監控了服務內部相互調用情況,相當比較簡單。追蹤系統最困難的部分就是在分佈式的應用環境下保持追蹤的正常工作。任何一個追蹤系統,都需要理解多個跨進程調用間的因果關係,無論他們是通過 RPC 框架、發佈 - 訂閱機制、通用消息隊列、HTTP 請求調用、UDP 傳輸或者其他傳輸模式。所以業界談起 Tracing 技術 往往說的是跨進程間的分佈式鏈路追蹤(Distrubute Tracing)。

我們用 OpenTelemetry 實踐一個 HTTP 通信的 Trace 例子:

這是一個本地 Localhost 的 Java 程序,我們向下遊服務 192.168.0.100 發起一個 HTTP 請求。

程序中使用 OpenTelemetry 的 inject 注入,通過 HTTP Headers 把 Localhost 的 Trace 傳遞給 192.168.0.100 的下游服務。傳播前,手動還創建兩個想要一塊傳播的 Attributes。

@GetMapping("/contextR")
@ResponseBody
public String contextR() {
 TextMapSetter<HttpURLConnection> setter = new TextMapSetter<HttpURLConnection>() {
  @Override
  public void set(HttpURLConnection carrier, String key, String value) {
   // 我們把上下文放到 HTTP 的 Header
   carrier.setRequestProperty(key, value);
  }
 };
 Span spanCur = Span.current();
 Span outGoing = tracer.spanBuilder("/resource").setSpanKind(SpanKind.CLIENT).startSpan();
 try {
  URL url = new URL("http://192.168.0.100:8080/resource");
  HttpURLConnection transportLayer = (HttpURLConnection) url.openConnection();
  outGoing.setAttribute("http.method""GET");
  outGoing.setAttribute("http.url", url.toString());   
  // 將當前 Span 的上下文注入到這個 HTTP 請求中
  OpenTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), transportLayer, setter);
  // Make outgoing call
...

運行程序,從監控平臺我們看到,Trace 從本地的程序成功傳遞到了 192.168.0.100。

跨進程傳播數據

手動控制 Trace:自動構建

上面我們提到 Trace,都是鏈路追蹤系統自動完成的。雖然這很通用,但在實際應用中,我們有些時候還想查看更多的跟蹤細節和添加業務監控。鏈路追蹤技術支持應用程序開發人員手工方式在跟蹤的過程中添加額外的信息,甚至手動啓動 Span,以期待監控更高級別的系統行爲,或幫助調試問題。

OpenTelemetry 支持以 SDK 和 API 方式手動構建 Trace。API、SDK 都可以做一些基本 Trace 操作,可以理解 API 是 Min 實現,SDK 是 API 的超集。生產環境根據實際場景選擇用哪一個。

手動構建 Trace 原理圖

創建 Span

要創建 Span,只需指定 Span 的名稱。手動創建 Span 需要顯式結束操作,它的開始和結束時間由鏈路追蹤系統自動計算。Java 代碼實例:

Span Span = Tracer.SpanBuilder("手工創建 SpanOne").startSpan();
try{
......
} finally {
    Span.end(); //手動創建 Span,我們需要手動結束 Span
}

應用程序運行時,我們可以這樣獲取一個 Span。

Span Span = Span.current()

創建帶鏈接 Span

一個 Span 可以連接一個或多個因果相關的其他 Span。實例中我們創建一個 Span。

叫做 “手工創建 SpanOne”,然後分別創建了三個 Span,通過 link 把它們關聯成孩子 Span。最後又創建了一個 Span “childThree-Child”,把它作爲“childThree” 的孩子 Span 關聯:

@GetMapping("/createSpanAndLink")
public String createSpanAndLink() {
    String SpanName = "手工創建 SpanOne";
    //創建一個 Span,然後創建三個 child Span,最後關聯 Span
    Span SpanOne = Tracer.SpanBuilder(SpanName)             
            .startSpan();
    Span childSpan = Tracer.SpanBuilder("childOne")
            .addLink(SpanOne.getSpanContext()).startSpan();
    Span childSpan2 = Tracer.SpanBuilder("childTwo")
            .addLink(SpanOne.getSpanContext()).startSpan();
    Span childSpan3 = Tracer.SpanBuilder("childThree")
            .addLink(SpanOne.getSpanContext()).startSpan();
    //創建一個 Span,關聯 childSpan3,作爲它的 childSpan
    Span childSpan3Child = Tracer.SpanBuilder("childThree-Child")
            .addLink(childSpan3.getSpanContext()).startSpan();
}

我們看看運行程序後,收集的 Trace 的效果:Link 將各個 Span 連接起來。

鏈路 UI 展示 Trace 中 Span 關係

創建帶事件的 Span

Span 可以攜帶零個或多個 Span 屬性的命名事件進行註釋,每一個事件都是一個 key:value 鍵值對,並自動攜帶相應的時間戳。時間戳表示事件的持續時間。

@GetMapping("/event")
public String event(){
 Span span = Span.current();    
 span.updateName("創建 eventDemo"); 
 //手動更新 Event 持續時間
    span.addEvent("timeEvent",System.currentTimeMillis()+2000, 
                  TimeUnit.MILLISECONDS);  
    //給 Event 添加相關信息
    Attributes appInfo = Attributes.of(AttributeKey
                         .stringKey("app.id")"123456",
                    AttributeKey.stringKey("app.name")"應用程序 demo");     span.addEvent("auth.appinfo", appInfo);  
    logger.info("this is a event"); }

在上面程序可以看到,我們還可以給事件手動添加時間戳,這在複雜系統環境下還原真實持續事件很有意義的。看看運行程序後,追蹤平臺下 Span 生成的的效果:

數據如何上報

有了程序的 Trace 數據,包含 TraceId、Span、Spancontext 一系列數據。接下來需要上報到監控系統,通過視圖方式展示出 Trace 整個全貌。我們習慣把 Trace 數據上報到監控過程稱爲數據收集(Data Collection)。看看數據收集基本原理:

從圖中,看到鏈路收集過程中,數據上報核心的幾個組件:

Data Collection 在數據採集時候,Collector 和 Exporters 有兩種實現:Agent 和 Gateway。限於篇幅,我們在後面文章詳細給大家講解。

總結

今天講解了鏈路追蹤的數據傳播、數據上報的原理。如果你想動手實踐文章中的代碼, GitHub 開放了地址:https://github.com/laziobird/opentelemetry-jaeger

思考

一個常見的場景:用戶訪問一個每天上百萬 UV 的電商系統,現在這個用戶告訴我們下單不了,系統也沒有錯誤提示。如果作爲研發系統的工程師,你有哪些思路去排查故障呢?

如用我們提到的鏈路追蹤。能否知道該用戶訪問行爲,快速定位到哪個服務出問題,或者你平時的經驗,是怎樣來快速解決這樣類似的問題?你可以在留言區與我交流。

作者介紹

蔣志偉,愛好技術的架構師,先後就職於阿里、Qunar、美團,前 pmcaffCTO,目前 Opentelemetry 中國社區發起人,https://github.com/open-telemetry/docs-cn 主要維護者。

• 歡迎你們關注我們的 Github

https://github.com/open-telemetry/docs-cn

• 微信羣

歡迎大家關注 “Opentelemetry” 公衆號,這是中國區唯一官方技術公衆號

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bcziZg8RhCrMGYgFeN76cw