分佈式追蹤 - Jaeger

概念

當咱們把系統微服務化後,想查詢某個接口一次請求的耗時信息,須要登陸多臺機器查詢相關日誌才行。 以下圖所示架構,當對應服務集羣化部署後,想要查詢到某一次請求信息更是難上加難。那咱們有什麼辦法能夠解決這個問題麼?html

答案固然是有的,分佈式追蹤系統正是爲了解決這個問題而生。分佈式跟蹤爲描述和分析跨進程事務提供了一種解決方案。如Google Dapper論文 (業界的分佈式追蹤系統基本都是以這篇論文爲基礎進行實現)所述,分佈式跟蹤的一些使用場景包括:java

  1. 異常檢測,問題診斷
  2. 分佈式系統內各組件的調用狀況
  3. 性能/延遲優化
  4. 服務依賴性分析

micro-arch

由於業界分佈式追蹤系統衆多,各家Api定義上有必定的差別,爲了統一標準,因而OpenTracing出現了。node

什麼是OpenTracing?python

OpenTracing經過提供平臺無關、廠商無關的API,使得開發人員可以方便的添加或更換追蹤系統的實現。OpenTracing正在爲全球的分佈式追蹤,提供統一的概念和數據標準。git

除了OpenTracing外,還有OpenCensus 這個項目,OpenCensus 由google發起,它除了包含tracing外,還包含度量(metrics)。github

兩套分佈式追蹤框架,都有不少追隨者,都想統一對方,但最終結果是對峙不下,最後兩個組織一塊兒組隊新建了OpenTelemetry項目。項目的第一宗旨就是:兼容OpenTracing和OpenSensus。對於使用OpenTracing或OpenSensus的應用不須要從新改動就能夠接入OpenTelemetry。sql

模型

tracing-mental-model

從應用角度看分佈式追蹤系統所處的位置docker

示例

先來看兩張效果圖感覺一下(以jaeger ui爲例) traces-jaeger-indexshell

trace-detail-jaeger

語義

這裏的語義以 OpenTracing 爲基礎。數據庫

數據模型

[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··]

trace-flow 這種展示方式增長顯示了執行時間的上下文,相關服務間的層次關係,進程或者任務的串行或並行調用關係。這樣的視圖有助於發現系統調用的關鍵路徑。經過關注關鍵路徑的執行過程,項目團隊可專一於優化路徑中的關鍵位置,最大幅度的提高系統性能。

Trace

一條Trace是指一個請求包含的調用鏈(包含下游全部請求的調用鏈), 一條Trace能夠被認爲是一個由多個Span組成的有向無環圖, SpanSpan的關係被命名爲References

Operation Names

每個Span都有一個操做名稱,這個名稱簡單,並具備可讀性高。(例如:一個RPC方法的名稱,一個函數名,或者一個大型計算過程當中的子任務或階段)

Span

一個Span表明系統中具備開始時間和執行時長的邏輯運行單元 。具體能夠理解爲一次方法調用, 一個程序塊的調用或者一次RPC/數據庫訪問。

每一個Span包含如下的狀態:

  • An operation name,操做名稱
  • A start timestamp,起始時間
  • A finish timestamp,結束時間
  • Span Tags,一組鍵值對構成的Span標籤集合。鍵值對中,鍵必須爲string,值能夠是字符串,布爾,或者數字類型。
  • Span Logs,一組span的日誌集合。 每次log操做包含一個鍵值對,以及一個時間戳。 鍵值對中,鍵必須爲string,值能夠是任意類型。 可是須要注意,不是全部的支持OpenTracing的Tracer,都須要支持全部的值類型。
  • SpanContext,Span上下文對象 (下面會詳細說明)
  • References(Span間關係),相關的零個或者多個Span(Span間經過SpanContext創建這種關係)

每個SpanContext包含如下狀態:

  • 任何一個OpenTracing的實現,都須要將當前調用鏈的狀態(例如: spanID 和traceID ),依賴一個獨特的Span去跨進程邊界傳輸
  • Baggage Items,Trace的隨行數據,是一個鍵值對集合,它存在於trace中,也須要跨進程邊界傳輸

References

一個Span能夠和一個或者多個Span間存在因果關係。 OpenTracing定義了兩種關係:ChildOfFollowsFrom這兩種引用類型表明了子節點和父節點間的直接因果關係

ChildOf 引用

一個Span多是一個父級Span的孩子,即ChildOf 關係。在ChildOf 引用關係下,父級span某種程度上取決於子Span。下面這些狀況會構成ChildOf 關係:

  • 一個RPC調用的服務端的Span,和RPC服務客戶端的Span構成ChildOf 關係
  • 一個sql insert操做的Span,和ORM的save方法的Span構成ChildOf 關係
  • 不少span能夠並行工做(或者分佈式工做)均可能是一個父級的Span的子項,他會合並全部子Span的執行結果,並在指按期限內返回

下面表述一個ChildOf 關係的父子節點關係的時序圖:

FollowsFrom 引用

一些父級節點不以任何方式依然他們子節點的執行結果,這種狀況下,咱們說這些子Span和父Span之間是FollowsFrom 的因果關係。 下面表述一個FollowsFrom 關係的父子節點關係的時序圖:

[-Parent Span-]  [-Child Span-]
    [-Parent Span--]
     [-Child Span-]
    [-Parent Span-]
                [-Child Span-]

Tags

每一個Span能夠有多個鍵值對(key:value)形式的TagsTags是沒有時間戳的,支持簡單的對Span進行註解和補充。 Span的tag不會跨進程傳輸,所以它們不會被子級的span繼承。

必填參數

  • tag key,必須是string類型
  • tag value,類型爲字符串,布爾或者數字 注意,OpenTracing標準包含**"standard tags,標準Tag"**,此文檔中定義了Tag的標準含義。

Logs

每一個Span能夠進行屢次Logs操做,每一次Logs操做,都須要一個帶時間戳的時間名稱,以及可選的任意大小的存儲結構。 必填參數

  • 一個或者多個鍵值對,其中鍵必須是字符串類型,值能夠是任意類型。某些OpenTracing實現,可能支持更多的log值類型。 可選參數
  • 一個明確的時間戳。若是指定時間戳,那麼它必須在span的開始和結束時間以內。 注意,OpenTracing標準包含**"standard log keys,標準log的鍵"**,此文檔中定義了這些鍵的標準含義。

SpanContext

SpanContext更多的是一個「概念」 。每一個Span都必須提供方法訪問SpanContext。SpanContext表明跨越進程邊界,傳遞到下級Span的狀態,並用於封裝Baggage 。 OpenTracing的使用者僅僅須要,在建立Span、向傳輸協議Inject(注入)和從傳輸協議中Extract(提取)時使用 。

Baggage

Baggage元素是一個鍵值對集合,將這些值設置給給定的SpanSpanSpanContext,以及全部和此Span有直接或者間接關係的本地Span 也就是說,baggage元素隨Trace一塊兒應用程序調用過程 一同傳播
Baggage擁有強大功能,也會有很大的消耗。因爲Baggage的全局傳輸,若是包含的數量量太大,或者元素太多,它將下降系統的吞吐量或增長RPC的延遲。

Inject and Extract

SpanContext能夠經過Injected操做向Carrier增長,或者經過ExtractedCarrier中獲取,跨進程通信數據。經過這種方式,SpanContexts能夠跨越進程邊界,並提供足夠的信息來創建跨進程的span間關係(所以能夠實現跨進程連續追蹤)。

  • Inject 類比傳遞序列化後的參數
  • Extract 反序列化Inject的參數值

傳遞方式例如:

  1. 依賴HTTP頭傳遞(B3-header)
  2. Dubbo 定製Filter經過 RpcContext 設置 Attachment 來傳遞

Carrier能夠是一個接口或者一個數據載體,他對於跨進程通信是十分有幫助的。Carrier負責將追蹤狀態從一個進程"carries"傳遞到另外一個進程 。OpenTracing規定全部平臺的實現者支持兩種Carrier格式:基於"text map"(基於字符串的map)的格式和基於"binary"(二進制)的格式。

  • text map 格式的 Carrier是一個平臺慣用的map格式,基於unicode編碼的字符串字符串鍵值對
  • binary 格式的 Carrier 是一個不透明的二進制數組(更緊湊和有效) tracing-extract

Jaeger

Jaeger是 Uber 推出的一款開源分佈式追蹤系統(已從CNCF畢業),兼容 OpenTracing API。 它用於監視和診斷基於微服務的分佈式系統,功能包括:

  1. 分佈式上下文傳播
  2. 分佈式鏈路跟蹤
  3. 服務依賴分析

技術棧

架構

Jaeger能夠做爲單個進程進行部署,也能夠做爲可擴展的分佈式系統進行部署。 Jaeger 主要由如下幾部分組成,架構很是清晰:

  • Jaeger Client - 爲不一樣語言實現了符合 OpenTracing 標準的 SDK。應用程序經過 API 寫入數據,client library 把 trace 信息按照應用程序指定的採樣策略傳遞給 jaeger-agent.
  • Agent - 它是一個監聽在 UDP 端口上接收 span 數據的網絡守護進程,它會將數據批量發送給 collector。它被設計成一個基礎組件,部署到全部的宿主機上。Agent 將 client library 和 collector 解耦,爲 client library 屏蔽了路由和發現 collector 的細節.
  • Collector - 接收 jaeger-agent 發送來的數據,而後將數據寫入後端存儲。Collector 被設計成無狀態的組件,所以您能夠同時運行任意數量的 jaeger-collector。 當前,咱們的管道會分析數據併爲其創建索引,執行任何轉換並最終存儲它們。 Jaeger的存儲設備是一個可插拔組件,目前支持 Cassandra, Elasticsearch and Kafka 存儲.
  • Query - 接收查詢請求,而後從後端存儲系統中檢索 trace 並經過 UI 進行展現.
  • Ingester - 後端存儲被設計成一個可插拔的組件,支持將數據寫入 Cassandra, Elasticsearch.

Jaeger包含兩種架構方案: 1、收集器數據直接寫入存儲架構(tracing數據直接寫入存儲) architecture-v1

2、收集器數據緩衝後異步寫入存儲架構(tracing數據經過kafka緩衝後再異步消費寫入存儲) architecture-v2.png

我的推薦採用第二種架構方式部署

部署

爲了快速搭建Jaeger環境,這裏安裝基於Helm部署(須要先搭建 Kubernetes 集羣),能夠參考前面寫的文章來搭建。從 https://github.com/jaegertracing/helm-charts/tree/master/charts/jaeger 這裏能夠找到詳細的部署流程,能夠一步一步跟着執行部署。這裏採用 收集器數據直接寫入存儲架構 部署

helm install jaeger jaegertracing/jaeger

官方推薦使用jaeger-operator來部署,可參考: https://www.jaegertracing.io/docs/1.17/operator/ 安裝完成後查看服務狀態

kubectl get svc
NAME               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                         AGE
jaeger-agent       ClusterIP   10.97.3.215      <none>        5775/UDP,6831/UDP,6832/UDP,5778/TCP,14271/TCP   7m45s
jaeger-cassandra   ClusterIP   None             <none>        7000/TCP,7001/TCP,7199/TCP,9042/TCP,9160/TCP    7m45s
jaeger-collector   ClusterIP   10.111.141.231   <none>        14250/TCP,14267/TCP,14268/TCP,14269/TCP         7m45s
jaeger-query       ClusterIP   10.97.103.64     <none>        80/TCP,16687/TCP                                7m45s

要訪問jaeger ui 須要查看jaeger-query項目對外暴露的端口,咱們看到經過helm安裝,咱們採用的默認配置,這裏的網絡類型是ClusterIP,若是想外網訪問能夠先臨時改爲NodePort的方式,執行以下命令編輯對應配置:

kubectl edit service jaeger-query

找到最下面的ClusterIP改爲NodePort保存便可,保存後會自動生效

kubectl get svc
NAME               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                         AGE
jaeger-agent       ClusterIP   10.97.3.215      <none>        5775/UDP,6831/UDP,6832/UDP,5778/TCP,14271/TCP   8m38s
jaeger-cassandra   ClusterIP   None             <none>        7000/TCP,7001/TCP,7199/TCP,9042/TCP,9160/TCP    8m38s
jaeger-collector   ClusterIP   10.111.141.231   <none>        14250/TCP,14267/TCP,14268/TCP,14269/TCP         8m38s
jaeger-query       NodePort    10.97.103.64     <none>        80:31067/TCP,16687:31381/TCP                    8m38s

能夠發現如今jaeger-query的網絡類型已經變成了NodePort,如今能夠經過流量訪問Jaeger Ui了 這裏的地址是 http://47.57.100.110:31067/search (注意,IP地址及端口根據本身控制檯的實際輸出填入就行) 進入頁面後能夠到剛纔部署的UI界面,並查詢jaeger-query項目自己的tracing信息。 我在列表頁面找到一個trace_id: 73c00aa573bf1ed0 臨時保存下它,後面分析會用到,打開後界面以下。 jaeger-query-ui.png

traces存儲結構

咱們能夠在jaeger源代碼中找到後端cassandra的存儲結構,具體信息能夠看這裏,位置比較隱蔽:

https://github.com/jaegertracing/jaeger/blob/master/plugin/storage/cassandra/schema/v001.cql.tmpl

不過咱們能夠登陸Pod查看建立後的數據結構信息(cassandra)。讓咱們一探究竟,首先登入cassandra對應的docker鏡像,而後經過cql 鏈接cassandra集羣。

若是對cql不瞭解的能夠查看對應文檔: https://cassandra.apache.org/doc/latest/cql/

kubectl exec -it jaeger-cassandra-0 --container jaeger-cassandra -- /bin/bash
cqlsh

Connected to jaeger at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.6 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.

進入對應的space,查看裏面對應的表信息

cqlsh> desc keyspaces;    #查看有哪些keyspaces

jaeger_v1_test  system_auth  system_distributed
system_schema   system       system_traces     

cqlsh> use jaeger_v1_test;   #切換到jaeger對應的space
cqlsh:jaeger_v1_test> desc tables;    #查看jaeger space下面的表信息

service_name_index  service_names  service_operation_index  traces            
dependencies_v2     tag_index      duration_index           operation_names_v2

咱們能夠一個一個的表信息查看。這裏咱們主要看下保存咱們trace信息的表 service_name_index

cqlsh:jaeger_v1_test> desc traces;

CREATE TABLE jaeger_v1_test.traces (
    trace_id blob,
    span_id bigint,
    span_hash bigint,
    duration bigint,
    flags int,
    logs list<frozen<log>>,
    operation_name text,
    parent_id bigint,
    process frozen<process>,
    refs list<frozen<span_ref>>,
    start_time bigint,
    tags list<frozen<keyvalue>>,
    PRIMARY KEY (trace_id, span_id, span_hash)
) WITH CLUSTERING ORDER BY (span_id ASC, span_hash ASC)
    AND bloom_filter_fp_chance = 0.01
    AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
    AND comment = ''
    AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy', 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'max_threshold': '32', 'min_threshold': '4'}
    AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}
    AND crc_check_chance = 1.0
    AND dclocal_read_repair_chance = 0.0
    AND default_time_to_live = 172800
    AND gc_grace_seconds = 10800
    AND max_index_interval = 2048
    AND memtable_flush_period_in_ms = 0
    AND min_index_interval = 128
    AND read_repair_chance = 0.0
    AND speculative_retry = 'NONE';

還記得咱們開始保存的那個trace_id: 73c00aa573bf1ed0 麼,如今咱們能夠在這個表中查看它是如何保存的,咱們可使用下面的cql進行查詢,查詢前須要對界面上的trace_id進行補位填充0x0000000000000000 ,這裏必定要注意,最終在cql裏面查詢的trace_id爲:0x000000000000000073c00aa573bf1ed0

cqlsh:jaeger_v1_test> expand on;
Now Expanded output is enabled
cqlsh:jaeger_v1_test> select * from traces where trace_id=0x000000000000000073c00aa573bf1ed0;

@ Row 1
----------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 trace_id       | 0x000000000000000073c00aa573bf1ed0
 span_id        | 2300299680491247480
 span_hash      | 1417161953846781420
 duration       | 204491
 flags          | 1
 logs           | [{ts: 1589363774632310, fields: [{key: 'event', value_type: 'string', value_string: 'searching', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'trace_id', value_type: 'string', value_string: '4c423cfb69721367', value_bool: False, value_long: 0, value_double: 0, value_binary: null}]}]
 operation_name | readTrace
 parent_id      | 0
 process        | {service_name: 'jaeger-query', tags: [{key: 'jaeger.version', value_type: 'string', value_string: 'Go-2.22.1', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'hostname', value_type: 'string', value_string: 'jaeger-query-55c77745b5-ff8tt', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'ip', value_type: 'string', value_string: '192.168.61.148', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'client-uuid', value_type: 'string', value_string: '363a86b295da9842', value_bool: False, value_long: 0, value_double: 0, value_binary: null}]}
 refs           | [{ref_type: 'child-of', trace_id: 0x000000000000000073c00aa573bf1ed0, span_id: 8340678215617945296}]
 start_time     | 1589363774627367
 tags           | [{key: 'db.statement', value_type: 'string', value_string: '\n\t\tSELECT trace_id, span_id, parent_id, operation_name, flags, start_time, duration, tags, logs, refs, process\n\t\tFROM traces\n\t\tWHERE trace_id = ?', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'db.type', value_type: 'string', value_string: 'cassandra', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'component', value_type: 'string', value_string: 'gocql', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'internal.span.format', value_type: 'string', value_string: 'proto', value_bool: False, value_long: 0, value_double: 0, value_binary: null}]

此處省略4個Row....
(5 rows)

由於找到這個trace_id包含了5個span,因此這裏查詢出來了5條記錄,能夠經過這段文本及上面的圖片進行一一觀察,能夠發現存儲結構仍是很是清晰的,UI界面須要展現的信息基本均可以很容易從裏面取到。

咱們再回過頭來看看jaeger client 庫thrift的結構(源碼見:jaeger.thrift)

# 標籤
struct Tag {
  1: required string  key
  2: required TagType vType
  3: optional string  vStr
  4: optional double  vDouble
  5: optional bool    vBool
  6: optional i64     vLong
  7: optional binary  vBinary
}

# 日誌
struct Log {
  1: required i64       timestamp
  2: required list<Tag> fields
}

enum SpanRefType { CHILD_OF, FOLLOWS_FROM }

# Span 之間的關係
struct SpanRef {
  1: required SpanRefType refType
  2: required i64         traceIdLow
  3: required i64         traceIdHigh
  4: required i64         spanId
}

# Span
struct Span {
  1:  required i64           traceIdLow   # the least significant 64 bits of a traceID
  2:  required i64           traceIdHigh  # the most significant 64 bits of a traceID; 0 when only 64bit IDs are used
  3:  required i64           spanId       # unique span id (only unique within a given trace)
  4:  required i64           parentSpanId # since nearly all spans will have parents spans, CHILD_OF refs do not have to be explicit
  5:  required string        operationName
  6:  optional list<SpanRef> references   # causal references to other spans
  7:  required i32           flags        # a bit field used to propagate sampling decisions. 1 signifies a SAMPLED span, 2 signifies a DEBUG span.
  8:  required i64           startTime
  9:  required i64           duration
  10: optional list<Tag>     tags
  11: optional list<Log>     logs
}

基本上能夠跟存儲的數據結構一一對應上。

採樣策略

Jaeger客戶端支持4種採樣策略,分別是:

  1. Constant (sampler.type=const) 採樣率的可設置的值爲 0 和 1,分別表示關閉採樣和所有采樣
  2. Probabilistic (sampler.type=probabilistic) 按照機率採樣,取值可在 0 至 1 之間,例如設置爲 0.5 的話意爲只對 50% 的請求採樣
  3. Rate Limiting (sampler.type=ratelimiting) 設置每秒的採樣次數上限 。 例如,當sampler.param = 2.0時,它將以每秒2條跡線的速率對請求進行採樣。
  4. Remote (sampler.type=remote) 此爲默認策略。 採樣遵循遠程設置,取值的含義和 probabilistic 相同,都意爲採樣的機率,只不過設置爲 remote 後,Client 會從 Jaeger Agent 中動態獲取採樣率設置。 爲了最大程度地減小開銷,Jaeger默認採用 0.1% 的採樣策略採集數據 (1000次裏面採集1次)。

客戶端

全部Jaeger客戶端庫都支持OpenTracing API ,下面這些都是官方支持的客戶端庫

語言 GitHub Repo
Go jaegertracing/jaeger-client-go
Java jaegertracing/jaeger-client-java
Node.js jaegertracing/jaeger-client-node
Python jaegertracing/jaeger-client-python
C++ jaegertracing/jaeger-client-cpp
C# jaegertracing/jaeger-client-csharp

其餘語言的客戶端庫還在開發中,具體進展能夠來這裏查看 issue #366

參考文獻

https://www.jaegertracing.io/docs/

https://github.com/jaegertracing/jaeger

相關文章
相關標籤/搜索