終於!SOFATracer 完成了它的鏈路視覺化之旅

SOFAStack發表於2021-10-21

?

文|趙陳(SOFA 開源之夏鏈路專案組)

武漢理工大學計算機工程碩士在讀

研究方向:唐卡線稿的自動上色

校對|宋國磊(SOFATracer commiter)

本文 6971 字 閱讀 18 分鐘

背 景

有幸參與開源軟體供應鏈點亮計劃——暑期 2021 支援的開源專案,目前 SOFATracer 已經能夠將埋點資料上報到 Zipkin 中,本專案的主要目標是將產生的埋點資料上報給 Jaeger 和 SkyWalking 中進行視覺化展示。

PART. 1 SOFATracer

SOFATracer 是螞蟻集團基於 OpenTracing 規範開發的分散式鏈路跟蹤系統,其核心理念就是通過一個全域性的 TraceId 將分佈在各個服務節點上的同一次請求串聯起來。通過統一的 TraceId 將呼叫鏈路中的各種網路呼叫情況以日誌的方式記錄下來,以達到透視化網路呼叫的目的,這些鏈路資料可用於故障的快速發現,服務治理等。

SOFATracer 提供了非同步落地磁碟的日誌列印能力和將鏈路跟蹤資料上報到開源產品 Zipkin 做分散式鏈路跟蹤展示的能力。這次參加開源之夏活動的任務是要把鏈路跟蹤資料上報到 Jaeger 和 SkyWalking 中進行展示。

SOFATracer 資料上報

上圖是 SOFATracer 中的鏈路上報流程,Span#finish 是 span 生命週期的最後一個執行方法,這是整個資料上報的入口,SOFATracer 的 report span 方法中含有上報鏈路展示端和日誌落盤兩個部分。SOFATracer 中沒有把上報資料採集器和日誌落盤分開只是在日誌落盤之前呼叫 SOFATracer#invokeReporListeners 方法,找到系統中所有實現了 SpanReportListener 介面並加入了 SpanReportListenersHolder 的例項,呼叫其 onSpanReport 方法完成鏈路資料上報至資料採集器。下面的程式碼片段是 invokeReportListeners 方法的具體實現。

 protected void invokeReportListeners(SofaTracerSpan sofaTracerSpan) {
    List<SpanReportListener> listeners = SpanReportListenerHolder
        .getSpanReportListenersHolder();
    if (listeners != null && listeners.size() > 0) {
        for (SpanReportListener listener : listeners) {
            listener.onSpanReport(sofaTracerSpan);
        }
    }
}

SpanReportListenerHolder 中的例項在專案啟動的時候加入,且分為 Spring Boot 應用和 Spring 應用兩種情況:

  • 在 Spring Boot 應用中自動配置類 SOFATracerSpanRemoteReporter 會將當前所有 SpanReportListener 型別的 bean 例項儲存到 SpanReportListenerHolder 的 List 物件中。SpanReportListener 的例項物件會在各自的 AutoConfiguration 自動配置類中注入到 IOC 容器中。
  • 在 Spring 應用中通過實現 Spring 提供的 bean 生命週期介面 InitializingBean,在 afterPropertiesSet 方法中例項化 SpanReportListener 的例項物件並且加入到 SpanReportListenerHolder 中。

要實現把 SOFATracer 中的 trace 資料上傳到 Jaeger 和 SkyWalking 需要實現 SpanReportListener 介面並在應用啟動的時候把對應例項加入到 SpanReportListenersHolder 中。

PART. 2 Jaeger 資料上報

下圖是 Jaeger 中資料上報的部分圖示,圖中 CommandQueue 中存放的是重新整理或新增指令,生產者是取樣器和 flush 定時器,消費者是佇列處理器。取樣器判斷一個 span 需要上報後向 CommandQueue 中新增一個 AppendCommand,flush 定時器根據設定的 flushInterval 不斷向佇列中新增 FlushCommand,佇列處理器不斷從 CommandQueue 中讀取指令判斷是 AppendCommand 還是 FlushCommand,如果重新整理指令把當前 byteBuffer 中的資料傳送到接受端,如果是新增指令把這個 span 新增到 byteBuffer 中暫存。

在實現上報到 Jaeger 過程中主要工作是 Jaeger Span 和 SOFATracer Span 模型的轉換,轉換過後利用上面的邏輯傳送 span 到後端。

上圖是 Jaeger 中 Sender 的 UML 圖,從圖中可以看到有兩種型別的 Sender 分別是 HTTPSender 和 UDPSender 。分別對應用 HTTP 傳送資料和 UDP 傳送資料,在實現 SOFATracer 上報 Jaeger 中使用 UDPSender 傳送 span 資料到 Jaeger Agent 中,使用 HTTPSender 直接傳送資料到 Jaeger-Collector 中。

Jaeger Span 與 SOFATracer Span 模型的轉換

模型轉換對照

TraceId 和 SpanId 的處理

TraceId 的轉換:

  • 問題在 SOFATracer 中的 TracerId 的產生規則是:伺服器 IP + ID 產生的時間 + 自增序列 + 當前程式號
例如 :0ad1348f1403169275002100356696 前 8 位 0ad1348f 即產生 TraceId 的機器的 IP,這是一個十六進位制的數字,每兩位代表 IP 中的一段,我們把這個數字,按每兩位轉成 10 進位制即可得到常見的 IP 地址表示方式 10.209.52.143,您也可以根據這個規律來查詢到請求經過的第一個伺服器。後面的 13 位 1403169275002 是產生 TraceId 的時間。之後的 4 位 1003 是一個自增的序列,從 1000 漲到 9000,到達 9000 後回到 1000 再開始往上漲。最後的 5 位 56696 是當前的程式 ID,為了防止單機多程式出現 TraceId 衝突的情況,所以在 TraceId 末尾新增了當前的程式 ID。——TraceId 和 SpanId 生成規則

在 SOFATracer 中 TraceId 是 String 型別,但是在 Jaeger 中 TraceId 是使用的兩個 Long 型的整數來構成最終的 TraceId。

解決方案

在 Jaeger 中表示 TraceId 的是 TraceIdHigh 與 TraceIdLow 在內部再使用函式將兩者轉換成 String 型別的 TraceIdAsString 在拼接的過程中分別將兩個 ID 轉換為對應的 HexString,當 HexString 不夠 16 位時頭部加 0。

    StringBuilder builder = new StringBuilder(desiredLength);
    int offset = desiredLength - id.length();

    for (int i = 0; i < offset; i++)
        builder.append('0');
    builder.append(id);
    return builder.toString();
}

SpanId 的轉化

  • 問題在 Jaeger 中 SpanId 是 Long 型整數,在 SOFATracer 中是 String 型別。
  • 解決辦法這個問題的解決辦法同之前已有的轉化為 Zipkin 中的 SpanId 的解決辦法一樣,也是使用 FNV Hash 將 String 對映成衝突較小的 Long 型。

兩種上傳方式

配合 Jaeger Agent

The Jaeger agent is a network daemon that listens for spans sent over UDP, which it batches and sends to the Collector. It is designed to be deployed to all hosts as an infrastructure component. The agent abstracts the routing and discovery of the Collectors away from the client.

Jaeger Agent 被設計成一種基本元件部署到主機上,能夠將路由和發現 Collector 的任務從 client 中抽離出來。Agent 只能接受通過 UDP 傳送的 Thrift 格式的資料,所以要使用 Jaeger Agent 需要使用 UDPSender。

使用 HTTP 協議上報 Collector

當使用 UDP 上報到 Jaeger Agent 的時候為了保證資料不在傳輸過程中丟失應該把 Jaeger Agent 部署在服務所在的機器,但是有的情況不能滿足前述要求,這時可以使用 HTTP 協議直接傳送資料到 Collector,這時使用 HTTPSender。

PART. 3 SkyWalking 資料上報

SkyWalking 是分散式系統的應用程式效能監視工具,專為微服務、雲原生架構和基於容器架構而設計,提供分散式追蹤、服務網格遙測分析、度量聚合和視覺化的一體化解決方案。SkyWalking 採用位元組碼注入的方式實現程式碼的無侵入,且效能表現優秀。SkyWalking 的 receiver-trace 模組可以通過 gRPC 和 HTTPRestful 服務接受 SkyWalking 格式的 trace 資料,在實現上報 SkyWalking 中選擇的上報方式是通過 HTTPRestful 服務上報。

模型轉換對照

SegmentId、SpanId、PatentSpanID 的轉換

SOFATracer 中的 SpanId 是一個字串,但是在 SkyWalking 中 SpanId 和 ParentSpanId 是一個 int 整數並且每一個 segment 中的 SpanId 都是從 0 開始編號,SpanId 最大值由配置的一個 segment 中最多有多少 span 指定。在轉換過程中需要指定 SpanId,因為現在每一個 segment 中只有一個 span,所以轉換生成的 segment 中的 span 的 ID 可以固定成 0。

SegmentId 是用來唯一標識一個 segment 的,如果 segmentId 相同前一個 segment 會被後面的 segment 覆蓋導致 span 丟失。最後使用的 segmentId 的構造方式是 segmentId = traceId + SpanId 雜湊值 + 0/1,其中 0 和 1 分別代表 server 和 client。最後需要加上 client 和 server 的原因是在 Dubbo 和 SOFARPC 中存在 server -> server 的情況,其中 RPC 呼叫的 client、server span 的 SpanId 和 parentId 都一樣,需要以此來區分它們,否則 client 端的 span 會被覆蓋。

Dubbo 與 SOFARPC 的處理

基本的模型是 client-server-client-server-. 這種模式,但是在 Dubbo 和 SOFARPC 中存在 server -> server 的情況,其中 client span、server span 兩個 span 除了 kind 型別不同之外,其他的資訊是一樣。

  • parentSegmentId

要找出 parentSegmentId,在非 SOFARPC 和 Dubbo 情況下,遵循 server -> client, client -> server 也就是 client 的父 spa 只能是 server 型別的,server 型別的父 span 只能為空或 client 型別。轉換方式是在 SOFARPC 和 Dubbo 中,根據使用 SkyWalking Java Agent 上報時兩者的鏈路展示情況,轉化按照:

server span:parentSegmentId = traceId + parentId 雜湊值 + client(1)

client span:parentSegmentId = traceId + parentId 雜湊值 + server(0)

server span:parentSegmentId = traceId + spanId 雜湊值 + client(1)

client span :parentSegmentId = traceId + parentId 雜湊值 + server(0)

  • 欄位和 networkAddressUsedAtPeer 欄位:

Peer 欄位

在 Dubbo 中 Peer 欄位可以通過 remote.host、remote.port 兩個 tag 組成 SOFARPC 中在 remote.ip 中包含了 IP 和 port,只使用 IP,因為在 server 端上報的 span 中無法獲得 client 使用的是自己的哪個端。

networkAddressUsedAtPeerDubbo

可以通過 local.host、local.port 組成 SOFARPC 中不能直接從 span 中獲取到本機的 IP,使用的是獲取本機的第一個有效 IPv4 地址,但是沒有埠號,所以在上面的 peer 欄位中也只用了 IP。

### 展示拓撲圖

在構建鏈路的過程中幾個比較關鍵的欄位是 peer、networkAddressUsedAtPeer 、parentService、parentServiceInstance、parentEndpoint。其中 Peer 和 networkAddressUsedAtPeer 分別表示對端地址以及 client 端呼叫當前例項使用的地址,這兩個欄位的作用是將鏈路中的例項連線起來,如果這兩個欄位缺失會導致鏈路斷開,在轉換過程中這兩個欄位通過在 span 的 tag 中尋找或獲取本機第一個合法的 IPv4 地址獲得。後三個欄位的作用是指出對應的父例項節點,如果不設定這三個欄位會產生一個空的例項資訊,如下圖所示。目前 SOFATracer 中在能在上下文中傳播的只有 TraceIdSpanId、parentId、sysBaggage、bizBaggage 從其中無法得到以上的三個欄位,為了能展示拓撲圖在 SOFATracer 的上下文中增加了七個欄位 service、serviceInstance、endpoint、parentService、parentServiceInstance、parentEndpoint、peer 這樣就能夠在轉換的過程中獲得父服務的相關資訊。

非同步上傳

使用 HTTP 上報 Json 格式的 segment 資料到後端,上報時以 message 為單位,多個 segment 組合成一個 message。

流程如下圖,span 結束後將轉換好的 segment 加入到 segment 緩衝陣列中,另一個執行緒不斷到陣列中重新整理資料到 message,當 message 的大小達到最大值或等待傳送的時間達到設定值就傳送一次資料,設定的 message 最大預設為 2MB。

PART. 4 壓 測

測試配置

  • Windows 10
  • Memory 16G
  • Disk 500GB SSD
  • Intel(R) Core(TM) i7-7700HQ CPU @2.80GHz 2.80GHz

測試方式

部署一個包含六個服務的呼叫鏈路。設定三組對照:

  • 不採集 span
  • 50% 採集
  • 全量採集

Jaeger 測試結果

測試中相關的幾個引數設定如下:

Jaeger Agent 方式

全量採集

50% 採集

不採集

上報 Jaeger Collector

全量採集

50%採集

不採集

SkyWalking 測試結果

全集採集

50% 採集

不採集

測試小結

在全取樣時三種上報方式中上報 SkyWalking 的本機吞吐率是最低的只有 512.75/sec,相比於上報 Jaeger Agent 吞吐率下降了約 14%,相比於上傳 Jaeger Agent 吞吐率減少了 11.89%。就每種方式對比全取樣與不取樣時吞吐率的變化:上報 Jaeger Agent 時因為全取樣吞吐率下降了 14.6%,上報 Jaeger Collector 時因為全取樣吞吐率下降了 17%,上報 SkyWalking 時因為全取樣吞吐率下降了約 23%。

本次介紹的 SOFATracer 的鏈路視覺化,將會在下個版本 release。

-

「收穫」

很幸運能夠參加這次的開源之夏活動,在閱讀 SOFATracer 原始碼的過程中學習了很多優秀的設計思想與實現方式,實現的過程中會去模仿一些原始碼的實現方式在這個過程中自己學習到了很多。在專案實施過程中也發現了自己的一些問題,比如在解決問題時有一點思路就開始做,沒有深挖這個思路是否可行,這個壞習慣浪費了許多時間。這是我第一次參與到開源社群的相關活動中,在這個過程中瞭解了開源社群的運作方式,在以後的學習過程中會更加努力提高自己的程式碼能力,爭取能為開源社群做出一點貢獻。

特別感謝感謝宋國磊老師對我的耐心指導,在專案過程中宋老師幫助我解開了很多疑惑,學到很多東西,感謝 SOFAStack 社群在整個過程中對我的諸多幫助,感謝活動主辦方提供的平臺。

-

「參考資料」

  1. 螞蟻集團分散式鏈路跟蹤元件 SOFATracer 資料上報機制和原始碼分析 | 剖析
  2. 使用 SkyWalking 實現全鏈路監控
  3. Zipkin-SkyWalking Exporter
  4. STAM:針對大型分散式應用系統的拓撲自動檢測方法

本週推薦閱讀

相關文章