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

📄

文|趙陳(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:針對大型分佈式應用系統的拓撲自動檢測方法

本週推薦閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章