概念
當咱們把系統微服務化後,想查詢某個接口一次請求的耗時信息,須要登陸多臺機器查詢相關日誌才行。 以下圖所示架構,當對應服務集羣化部署後,想要查詢到某一次請求信息更是難上加難。那咱們有什麼辦法能夠解決這個問題麼?html
答案固然是有的,分佈式追蹤系統正是爲了解決這個問題而生。分佈式跟蹤爲描述和分析跨進程事務提供了一種解決方案。如Google Dapper論文 (業界的分佈式追蹤系統基本都是以這篇論文爲基礎進行實現)所述,分佈式跟蹤的一些使用場景包括:java
- 異常檢測,問題診斷
- 分佈式系統內各組件的調用狀況
- 性能/延遲優化
- 服務依賴性分析
由於業界分佈式追蹤系統衆多,各家Api定義上有必定的差別,爲了統一標準,因而OpenTracing出現了。node
什麼是OpenTracing?python
OpenTracing經過提供平臺無關、廠商無關的API,使得開發人員可以方便的添加或更換追蹤系統的實現。OpenTracing正在爲全球的分佈式追蹤,提供統一的概念和數據標準。git
除了OpenTracing外,還有OpenCensus 這個項目,OpenCensus 由google發起,它除了包含tracing外,還包含度量(metrics)。github
兩套分佈式追蹤框架,都有不少追隨者,都想統一對方,但最終結果是對峙不下,最後兩個組織一塊兒組隊新建了OpenTelemetry項目。項目的第一宗旨就是:兼容OpenTracing和OpenSensus。對於使用OpenTracing或OpenSensus的應用不須要從新改動就能夠接入OpenTelemetry。sql
模型
從應用角度看分佈式追蹤系統所處的位置docker
示例
先來看兩張效果圖感覺一下(以jaeger ui爲例) shell
語義
這裏的語義以 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
一條Trace是指一個請求包含的調用鏈(包含下游全部請求的調用鏈), 一條Trace能夠被認爲是一個由多個Span組成的有向無環圖, Span與Span的關係被命名爲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定義了兩種關係:ChildOf
和 FollowsFrom
。這兩種引用類型表明了子節點和父節點間的直接因果關係。
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)形式的Tags,Tags是沒有時間戳的,支持簡單的對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元素是一個鍵值對集合,將這些值設置給給定的Span
,Span
的SpanContext
,以及全部和此Span
有直接或者間接關係的本地Span
。 也就是說,baggage元素隨Trace一塊兒應用程序調用過程 一同傳播
Baggage擁有強大功能,也會有很大的消耗。因爲Baggage的全局傳輸,若是包含的數量量太大,或者元素太多,它將下降系統的吞吐量或增長RPC的延遲。
Inject and Extract
SpanContext
能夠經過Injected操做向Carrier增長,或者經過Extracted從Carrier中獲取,跨進程通信數據。經過這種方式,SpanContexts能夠跨越進程邊界,並提供足夠的信息來創建跨進程的span間關係(所以能夠實現跨進程連續追蹤)。
- Inject 類比傳遞序列化後的參數
- Extract 反序列化Inject的參數值
傳遞方式例如:
- 依賴HTTP頭傳遞(B3-header)
- Dubbo 定製Filter經過 RpcContext 設置 Attachment 來傳遞
Carrier能夠是一個接口或者一個數據載體,他對於跨進程通信是十分有幫助的。Carrier負責將追蹤狀態從一個進程"carries"傳遞到另外一個進程 。OpenTracing規定全部平臺的實現者支持兩種Carrier格式:基於"text map"(基於字符串的map)的格式和基於"binary"(二進制)的格式。
- text map 格式的 Carrier是一個平臺慣用的map格式,基於unicode編碼的
字符串
對字符串
鍵值對 - binary 格式的 Carrier 是一個不透明的二進制數組(更緊湊和有效)
Jaeger
Jaeger是 Uber 推出的一款開源分佈式追蹤系統(已從CNCF畢業),兼容 OpenTracing API。 它用於監視和診斷基於微服務的分佈式系統,功能包括:
- 分佈式上下文傳播
- 分佈式鏈路跟蹤
- 服務依賴分析
技術棧
- 基於Go實現
- 數據支持多種類型的後端存儲
- Cassandra 3.4+
- Elasticsearch 5.x, 6.x, 7.x
- Kafka
- memory storage
架構
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數據直接寫入存儲)
2、收集器數據緩衝後異步寫入存儲架構(tracing數據經過kafka緩衝後再異步消費寫入存儲)
我的推薦採用第二種架構方式部署
部署
爲了快速搭建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
臨時保存下它,後面分析會用到,打開後界面以下。
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種採樣策略,分別是:
- Constant (
sampler.type=const
) 採樣率的可設置的值爲 0 和 1,分別表示關閉採樣和所有采樣 - Probabilistic (
sampler.type=probabilistic
) 按照機率採樣,取值可在 0 至 1 之間,例如設置爲 0.5 的話意爲只對 50% 的請求採樣 - Rate Limiting (
sampler.type=ratelimiting
) 設置每秒的採樣次數上限 。 例如,當sampler.param = 2.0時,它將以每秒2條跡線的速率對請求進行採樣。 - 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