分布式追蹤的概念
谷歌在2010年4月發表了一篇論文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》(http://1t.click/6EB),介紹了分布式追蹤的概念。

對於分布式追蹤,主要有以下的幾個概念:
- 追蹤 Trace:就是由分布的微服務協作所支撐的一個事務。一個追蹤,包含為該事務提供服務的各個服務請求。
- 跨度 Span:Span是事務中的一個工作流,一個Span包含了時間戳,日志和標簽信息。Span之間包含父子關系,或者主從(Followup)關系。
- 跨度上下文 Span Context:跨度上下文是支撐分布式追蹤的關鍵,它可以在調用的服務之間傳遞,上下文的內容包括諸如:從一個服務傳遞到另一個服務的時間,追蹤的ID,Span的ID還有其它需要從上游服務傳遞到下游服務的信息。
為什么要做分布式追蹤
隨着搜索系統的擴大和微服務的流行,一次請求涉及的服務往往是很多次調用,這些模塊,通常是由不同團隊,甚至是不同語言編寫的,所以,可能部署在數十台服務器、橫款多個數據中心。因此,在排查問題時,就不能人工對調用鏈路通過日志分析問題,我們需要輕松的獲取到調用鏈上的每次請求的情況,以便能快速定位和分析問題。
所以全鏈路監控的目標是
- 鏈路追蹤、定位故障
- 各階段耗時可視化
- 服務依賴調用梳理
- 數據分析,鏈路優化

什么是 OpenTracing?
OpenTracing 是一個分布式追蹤規范。OpenTracing 通過提供平台無關、廠商無關的 API,為分布式追蹤提供統一的概念和數據標准,使得開發人員能夠方便的添加(或更換)追蹤系統的實現。OpenTracing 定義了如下幾個術語:
- Span:代表了系統中的一個邏輯工作單元,它具有操作名、操作開始時間以及持續時長。Span 可能會有嵌套或排序,從而對因果關系建模。
- Tags:每個 Span 可以有多個鍵值對(key: value)形式的 Tags,Tags 是沒有時間戳的,支持簡單地對 Span 進行注解和補充。
- Logs:每個 Span 可以進行多次 Log 操作,每一次 Log 操作,都需要一個帶時間戳的時間名稱,以及可選的任意大小的存儲結構。
- Trace:代表了系統中的一個數據/執行路徑(一個或多個 Span),可以將其理解為 Span 的有向無環圖。
OpenTracing 還有其他一些概念,這里不過多解釋。我們看個傳統的調用關系例子,如下所示:

在一個分布式系統中,追蹤一個事務或者調用流一般如上圖所示。雖然這種圖對於看清各組件的組合關系是很有用的,但是,它不能很好顯示組件的調用時間,以及是串行調用還是並行調用。如果展現更復雜的調用關系,會更加復雜,甚至無法畫出這樣的圖。另外,這種圖也無法顯示調用間的時間間隔以及是否通過定時調用來啟動調用。一種更有效的展現一個典型的 trace 過程,如下圖所示:

這種展現方式增加了執行時間的上下文,相關服務間的層次關系,進程或者任務的串行或並行調用關系。這樣的視圖有助於發現系統調用的關鍵路徑。通過關注關鍵路徑的執行過程,項目團隊可能專注於優化路徑中的關鍵位置,最大幅度地提升系統的性能。例如:可以通過追蹤一個資源定位的調用情況,明確底層的調用情況,發現哪些操作有阻塞的情況。
OpenTracing 標准概念
基於谷歌提出的概念OpenTracing(http://1t.click/6tC)定義了一個開放的分布式追蹤的標准。
Span是分布式追蹤的基本組成單元,表示一個分布式系統中的單獨的工作單元。每一個Span可以包含其它Span的引用。多個Span在一起構成了Trace。

OpenTracing的規范定義每一個Span都包含了以下內容:
- 操作名(Operation Name),標志該操作是什么
- 標簽 (Tag),標簽是一個名值對,用戶可以加入任何對追蹤有意義的信息
- 日志(Logs),日志也定義為名值對。用於捕獲調試信息,或者相關Span的相關信息
- 跨度上下文呢 (SpanContext),SpanContext負責子微服務系統邊界傳遞數據。它主要包含兩部分:
- 和實現無關的狀態信息,例如Trace ID,Span ID
- 行李項 (Baggage Item)。如果把微服務調用比做從一個城市到另一個城市的飛行, 那么SpanContext就可以看成是飛機運載的內容。Trace ID和Span ID就像是航班號,而行李項就像是運送的行李。每次服務調用,用戶都可以決定發送不同的行李。
這里是一個Span的例子:
t=0 operation name: db_query t=x +-----------------------------------------------------+ | · · · · · · · · · · Span · · · · · · · · · · | +-----------------------------------------------------+ Tags: - db.instance:"jdbc:mysql://127.0.0.1:3306/customers - db.statement: "SELECT * FROM mytable WHERE foo='bar';" Logs: - message:"Can't connect to mysql server on '127.0.0.1'(10061)" SpanContext: - trace_id:"abc123" - span_id:"xyz789" - Baggage Items: - special_id:"vsid1738"
要實現分布式追蹤,如何傳遞SpanContext是關鍵。OpenTracing定義了兩個方法Inject和Extract用於SpanContext的注入和提取。

Inject 偽代碼
span_context = ...
outbound_request = ...
# We'll use the (builtin) HTTP_HEADERS carrier format. We
# start by using an empty map as the carrier prior to the
# call to `tracer.inject`.
carrier = {}
tracer.inject(span_context, opentracing.Format.HTTP_HEADERS, carrier)
# `carrier` now contains (opaque) key:value pairs which we pass
# along over whatever wire protocol we already use.
for key, value in carrier:
outbound_request.headers[key] = escape(value)
這里的注入的過程就是把context的所有信息寫入到一個叫Carrier的字典中,然后把字典中的所有名值對寫入 HTTP Header。
Extract 偽代碼
inbound_request = ...
# We'll again use the (builtin) HTTP_HEADERS carrier format. Per the
# HTTP_HEADERS documentation, we can use a map that has extraneous data
# in it and let the OpenTracing implementation look for the subset
# of key:value pairs it needs.
#
# As such, we directly use the key:value `inbound_request.headers`
# map as the carrier.
carrier = inbound_request.headers
span_context = tracer.extract(opentracing.Format.HTTP_HEADERS, carrier)
# Continue the trace given span_context. E.g.,
span = tracer.start_span("...", child_of=span_context)
# (If `carrier` held trace data, `span` will now be ready to use.)
抽取過程是注入的逆過程,從carrier,也就是HTTP Headers,構建SpanContext。
整個過程類似客戶端和服務器傳遞數據的序列化和反序列化的過程。這里的Carrier字典支持Key為string類型,value為string或者Binary格式(Bytes)。
什么是 Jaeger?
Jaeger 是 OpenTracing 的一個實現,是 Uber 開源的一個分布式追蹤系統,其靈感來源於Dapper 和 OpenZipkin。從 2016 年開始,該系統已經在 Uber 內部得到了廣泛的應用,它可以用於微服務架構應用的監控,特性包括分布式上下文傳播(Distributed context propagation)、分布式事務監控、根原因分析、服務依賴分析以及性能優化。該項目已經被雲原生計算基金會(Cloud Native Computing Foundation,CNCF)接納為第 12 個項目。
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。
- Data Store - 后端存儲被設計成一個可插拔的組件,支持將數據寫入 cassandra、elastic search。
- Query - 接收查詢請求,然后從后端存儲系統中檢索 trace 並通過 UI 進行展示。Query 是無狀態的,您可以啟動多個實例,把它們部署在 nginx 這樣的負載均衡器后面。
Jaeger 服務相關端口
Agent
5775 UDP協議,接收兼容zipkin的協議數據
6831 UDP協議,接收兼容jaeger的兼容協議
6832 UDP協議,接收jaeger的二進制協議
5778 HTTP協議,數據量大不建議使用
它們之間的傳輸協議都是基於thrift封裝的。我們默認使用5775作為傳輸端口。
Collector
14267 tcp agent發送jaeger.thrift格式數據
14250 tcp agent發送proto格式數據(背后gRPC)
14268 http 直接接受客戶端數據
14269 http 健康檢查
Query
16686 http jaeger的前端,放給用戶的接口
16687 http 健康檢查
啟動 Jaeger + Jaeger UI
我們使用 Docker 啟動 Jaeger + Jaeger UI(Jaeger 可視化 web 控制台),運行如下命令:
$ docker run -d -p5775:5775/udp \-p 6831:6831/udp \-p 6832:6832/udp \-p 5778:5778 \-p 16686:16686 \-p 14268:14268 \jaegertracing/all-in-one:latest
瀏覽器打開 localhost:16686,如下所示:

更多參考:
Python接入jaeger:jaeger-client-python
import logging from jaeger_client import Config import time def say_hello(hello_to): with tracer.start_span('say-hello') as span: span.set_tag('hello-to', hello_to) hello_str = format_string(span, hello_to) print_hello(span, hello_str) def format_string(root_span, hello_to): with tracer.start_span('format', child_of=root_span) as span: hello_str = 'Hello, %s!' % hello_to span.log_kv({'event': 'string-format', 'value': hello_str}) return hello_str def print_hello(root_span, hello_str): with tracer.start_span('println', child_of=root_span) as span: print(hello_str) span.log_kv({'event': 'println'}) def init_tracer(service): logging.getLogger('').handlers = [] logging.basicConfig(format='%(message)s', level=logging.DEBUG) # config = Config( # config={ # 'sampler': { # 'type': 'const', # 'param': 1, # }, # 'logging': True, # }, # service_name=service, # ) config = Config( config={ # usually read from some yaml config 'sampler': { 'type': 'const', 'param': 1, }, 'local_agent': { 'reporting_host': 'xxx.xxx.xxx.xxx', 'reporting_port': '5775', }, 'logging': True, }, service_name='your-app-name', validate=True, ) # this call also sets opentracing.tracer return config.initialize_tracer() tracer = init_tracer('hello-world') say_hello("server2") time.sleep(2) tracer.close()
運行后進Jaeger查看結果:

