使用 eBPF 零程式碼修改繪製全景應用拓撲

雲杉網路發表於2023-05-10

本文為 DeepFlow 在首屆雲原生社群可觀測性峰會上的演講實錄。回看連結[1],PPT下載[2]。

很高興有機會在第一屆可觀測性峰會上向大家介紹我們的產品 DeepFlow,我相信它會是今天 eBPF 含量最高的一個分享。DeepFlow 的能力很多,而我今天的分享會聚焦於一個點上說透,希望大家由此感知到 eBPF 帶給可觀測性的變革。那麼我今天要分享的內容就是,DeepFlow 如何利用 eBPF 技術,在不改程式碼、不改配置、不重啟程式的前提下,自動繪製雲原生應用的全景拓撲。全景應用拓撲能解決我們很多問題:觀測任意服務的全景依賴關係、觀測整個應用的瓶頸路徑、定位整個應用中的故障位置。

在開始之前,我大概也介紹一下自己的背景:從清華畢業以來,我一直在雲杉網路工作,我們從 2016 年開始開發 DeepFlow,我們基於 eBPF 等創新技術打通雲基礎設施和雲原生應用的全棧環節,零侵擾的實現應用的可觀測性。今天的分享分為四個部分:第一部分介紹傳統的解決方案如何繪製應用拓撲;第二部分介紹如何用 eBPF 完全零侵擾的實現應用拓撲的計算;第三部分介紹如何利用 eBPF 實現程式、資源、服務等標籤的注入,使得開發、運維、運營都能從自己熟悉的視角檢視這個拓撲;最後第四部分介紹一個全鏈路壓測的 Demo 和我們在客戶處的幾個實戰案例。

01|傳統解決方案的問題

首先我們知道,在應用拓撲中,節點可以展示為不同的粒度,例如按服務、按例項、按應用等不同粒度展現。例如對於服務粒度,一個節點代表一個服務,它由多個例項組成。節點之間的連線代表呼叫關係,通常也對應了一系列的指標,表示服務之間呼叫的吞吐、異常、時延等效能。在雲原生時代,雲基礎設施、微服務拆分等因素會帶來很多挑戰,導致繪製一個全景的應用拓撲變得非常艱難。

傳統的解決方案我們很熟悉,透過埋點、插碼的方式,修改程式碼來輸出類似於 Span 的呼叫資訊,透過聚合 Span 得到應用拓撲。即使是採用自動化的 Java 位元組碼注入等技術,雖然看起來不用改程式碼,但還是要修改啟動引數,意味著服務要重新發布,至少要重啟服務程式。

圖片
插碼方案的困難 - 難以升級

在雲原生場景下,使用這樣的方法來獲取應用拓撲變得更加困難。隨著服務拆得越來越微小,每個服務的開發人員的自由度變得越來越大,因此可能會出現各種新奇的語言、框架。相對於 Java agent 而言,其他語言基本都會涉及到程式碼修改、重編譯重發布。當然一般我們都會將這部分邏輯實現在一個 SDK 中,然而 SDK 的升級也是一個痛苦甚至絕望的過程,業務方都不願意升級,升級意味著發版,發版可能就意味著故障。

那有方法能避免修改程式碼嗎?雲原生時代我們還有一個選項 —— 使用服務網格(Service Mesh)。例如 Istio 在 K8s 及非容器環境下可構建一個服務網路。

圖片
網格方案的困難 - 難以全覆蓋

但服務網格的問題在於並不能覆蓋所有協議,例如 Istio 主要覆蓋 HTTP/gRPC,而其他大量各種各樣的 Protobuf/Thrift RPC,以及 MySQL/Redis/Kafka/MQTT 等中介軟體訪問都無法覆蓋到。另外從因果關係來講,我們可以因為選擇了服務網格而順帶實現一部分可觀測性,但肯定不會因為要去實現可觀測性而引入服務網格這樣一個帶有侵入性的技術。

除此之外,在雲原生時代,即使我們去改業務程式碼、上服務網格,仍然無法獲取到一個完整的應用拓撲。比如雲基礎設施中的 Open vSwitch,K8s 中的 IPVS,Ingress 位置的 Nginx 閘道器,這些都是看不到的。

下面出場的就是我們今天要講的主角,eBPF,它是一個非常火熱的新技術。我們來看看它是不是能解決我們今天聚焦的問題,能否將應用拓撲畫全、畫準,能為開發、運維等不同團隊的人提供一個統一的檢視,消除他們的 Gap。我們知道 eBPF 有很多很好的特性,它不需要業務改程式碼、不需要服務修改啟動引數、不需要重啟服務程式,它是程式語言無關的,而且能覆蓋到雲基礎設施和雲原生應用的整個技術棧。DeepFlow 基於 eBPF 技術也享受到了很多這方面的紅利,我們的客戶做 POC、社群的使用者試用都非常絲滑,不用去考慮運維視窗、實施週期,DeepFlow 的社群版只需一條命令,五分鐘即可開啟全景、全棧的可觀測性。

圖片
DeepFlow 軟體架構

這裡也用一頁 PPT 來簡單介紹一下 DeepFlow 社群版的軟體架構。我們開源至今還不到一年,目前在社群受到了不少關注。上圖中間藍色的部分是 DeepFlow 的兩個主要元件:Agent 負責採集資料,利用 eBPF 的零侵擾特性,覆蓋各種各樣的雲原生技術棧;Server 負責富化資料,將標籤與資料關聯,並儲存至實時數倉中。在北向,DeepFlow 提供 SQL、PromQL、OTLP 等介面,透過 DeepFlow 自己的 GUI 展現所有可觀測性資料,也相容 Grafana、Prometheus、OpenTelemetry Collector 等生態。在南向,DeepFlow 可整合 Prometheus 等指標資料、SkyWalking 等追蹤資料、Pyrosope 等 Profile 資料。

圖片
AutoTracing + AutoTagging

今天我們要聚焦的一點,就是 DeepFlow 如何使用 eBPF 生成全景應用拓撲。DeepFlow 的兩個核心能力 AutoTracing 和 AutoTagging 為應用拓撲的生成奠定了堅實的基礎。在沒有任何程式碼修改、不重啟任何業務程式的情況下,我們實現了全景應用拓撲的繪製。首先,我們可透過 eBPF 從網路包、核心 Socket Data 中獲取原始位元流;接下來我們分析 Raw Data 中的 IP、埠等資訊,使用 eBPF 將原始資料與程式、資源、服務等資訊關聯,以便繪製不同團隊不同視角的應用拓撲,這也是今天分享的第三部分;然後我們會從原始資料中提取應用呼叫協議,聚合呼叫中的請求和響應,計算呼叫的吞吐、時延和異常,以及其他系統和網路效能指標;最後,基於這類 Request Scope 的 Span 資料,我們可以聚合生成全景應用拓撲,也可以透過關聯關係的計算生成分散式追蹤火焰圖。

今天我們主要講的是應用拓撲的繪製,對於使用 eBPF 實現全自動的分散式追蹤這個話題,DeepFlow 社群的部落格 https://deepflow.io/blog 上有很多資料。

02|eBPF 零侵擾計算全景應用拓撲

圖片
Universal Application Topology

首先讓我們看一個效果圖,這是 DeepFlow 展示的一個全景應用拓撲。這個拓撲可能大家比較熟悉,它是 OpenTelemetry 的一個 Demo,它 Fork 自 Google Cloud Platform 的一個專案,它是一個小型的電商應用。從圖中可以看到,DeepFlow 可以獲取所有服務之間的呼叫關係以及相應的效能指標,這都是透過 eBPF 實現的,沒有做任何的程式碼修改和程式重啟,展現這些結果之前我們關閉了所有 OTel Instrumentation。

在這一節,我們將聚焦四個問題:第一個問題是如何採集原始資料;第二個問題是如何解析應用協議,eBPF做了什麼;第三個問題是如何計算全棧效能指標;第四個問題是如何適配低版本核心,許多使用者的核心版本可能是3.10。

資料採集:DeepFlow 同時用到了 eBPF 和它的前身,有 30 年曆史的 Classic BPF(cBPF)。在雲原生環境中,應用程式執行在容器 Pod 內,容器可能存在於多個節點上,並透過閘道器連線到資料庫等其他服務。我們可以使用 eBPF 技術來覆蓋所有的端節點,並使用 cBPF 覆蓋所有的中間轉發節點。從應用程式(端節點)層面,我們使用 kprobe 和 tracepoint 覆蓋所有的 TCP/UDP socket 讀寫操作;並使用 uprobe 來掛載到應用程式內的核心函式上,比如 OpenSSL/Golang 的 TLS/SSL 函式,來獲取加密或壓縮之前的資料。除此之外,在兩個服務之間還有許多中間轉發路徑,例如 IPVS、iptables、OvS 等,此時需要使用 cBPF 來獲取網路包資料,因為這部分流量不會有使用者態應用程式去讀寫,會直接在核心裡查錶轉發。

圖片
eBPF Probes

這裡列舉了 DeepFlow 中主要使用到的 eBPF Probe,包括最左邊的 kprobe,以及中間最高效能的 tracepoint,以及最右邊解決加密和壓縮資料的 uprobe。其中 tracepoint 滿足了 DeepFlow 中的絕大多數需求。

協議解析:在獲取原始資料方面,我們使用 eBPF 和 cBPF 來捕獲位元組流,但此時我們無法看到任何可理解的資訊。接下來,我們要做的就是從這些位元組流中提取出我們關心的應用協議。大多數協議都是明文的,例如 HTTP、RPC、SQL 等,我們可以直接遵照協議規範來解析它們的內容。對於一些特殊的協議,例如 HTTP2 之類的壓縮協議,我們需要進行更復雜的處理。我們可以使用 tracepoint 或 kprobe 來提取未壓縮的欄位,但此時對於已經壓縮的頭部欄位,還原它們是一項困難的工作,因此我們會選擇使用 uprobe 作為補充來採集壓縮協議的資料。另外對於加密流量也無法從核心函式中獲取明文,我們透過 uprobe 直接獲取加密之前的資料。最後,對於私有協議,它們沒有可遵循的通用規範,或者雖然遵循了 Protobuf/Thrift 等標準但無法提供協議定義檔案,此時我們提供了基於 WASM 的外掛化的可程式設計介面,在未來也有計劃提供基於 LUA 的外掛機制。

在協議解析層面還需要考慮的一個事情是流重組。例如一個 HTTP2/gRPC 請求的頭部欄位可能會分為多次系統呼叫讀寫,或者分拆為多個網路包收發,此時我們需要還原完整請求或響應頭,一遍重組一遍嘗試解析。

圖片
eBPF + WASM

特別提一下我們對私有協議的識別,透過結合 eBPF 和 WebAssembly 的能力,提供一個靈活的外掛機制。外掛在 DeepFlow 中解決兩個問題:提供對私有協議的解析能力、提供對任意協議的業務欄位解析能力。商用軟體供應商或業務開發人員可以輕鬆新增自定義外掛來解析協議,將可觀測性從 IT 層面提升到業務層面。這樣的方式使得我們可以自定義提取出一些業務指標或業務標籤,例如訂單量、使用者 ID、交易 ID、車機 ID 等資訊。這些業務資訊對於不同的業務場景都是獨特的,我們將這個靈活性給到了業務方,使得業務方可以快速實現零侵擾的可觀測性。

圖片
RED Metrics

效能指標:我們可以透過數請求和響應的個數來獲取吞吐量,同時還可以根據響應狀態碼、耗時等資訊計算 Error 和 Delay 指標。吞吐和異常的計算相對簡單,只需要基於單個請求或單個響應進行處理即可。最困難的是對時延的計算,我們需要使用 eBPF 技術來關聯請求和響應這兩個動作。這個過程又分為兩步:從 Socket/Packet Data 中基於 <sip, dip, sport, dport, protocol> 五元組聚合出 TCP/UDP Flow;然後在 Flow 上下文中匹配應用協議的每一個 Request 和 Response。而對於第二步,一個 Flow 中一般會有多個請求和響應,匹配邏輯我們以 HTTP 協議為例解釋:

對於序列協議如 HTTP 1.0,直接匹配臨近的請求和響應即可對於併發協議如 HTTP 2.0,需要提取協議頭中的 StreamID 進行請求和響應的配對還有一種特殊情況即 HTTP 1.1 中的管道機制,他是一種偽併發協議,我們可以利用它的 FIFO 特點完成請求和響應的配對

圖片
Network Metrics

但是,在雲原生環境下,僅僅只統計應用層 RED 往往不夠。例如我們會發現客戶端側看到的時延是 3 秒,而服務端側看到的時延是 3 毫秒;或者客戶端和服務端側都看不到有請求,但下游服務卻報錯了。針對這些場景,我們基於 Flow 資料計算得到了網路層面的吞吐、異常、時延。業務開發發現時延高時,可以快速檢視網路層時延來判斷到底是業務自身的問題還是基礎設施問題;另外在發現請求報錯時可以快速檢視是否建連或者傳輸異常了。

針對時延我們分的更細緻,透過從 Packet Data 中重建 TCP 狀態機來更加精細的展現各個層面引入的時延。在生產環境中我們會發現高時延的原因一般分為幾個方面:

建連慢:由於防火牆、負載均衡、閘道器的影響,導致 TCP 建連過程慢,這一過程又進一步拆分為了到底是客戶側建連慢還是服務側建連慢框架慢:由於業務程式碼在建連後的慢處理,客戶端在建連後等待了一段時間才傳送請求,這段等待時間我們會刻畫為客戶端等待時延,它能夠方面定位到底是框架/庫層面的問題,還是業務程式碼的問題系統慢:由於作業系統處理不及時,例如系統負載高等,對請求的 TCP ACK 回覆慢,從而導致 TCP 無法高效增大視窗大小傳輸慢:網路重傳、零窗等也是導致高時延的一些可能原因

在雲原生環境下,還有一個特點是網路路徑非常長,例如兩個服務之間的通訊可能依次經過容器 Pod 網路卡、容器 Node 網路卡、KVM 主機網路卡等。路徑的複雜性也是引發傳輸慢的主要原因。由於 DeepFlow 同時使用 cBPF,因此可以從所有中間轉發路徑中觀測到應用層和網路層的時延,然後透過對比逐跳時延來定位到底是哪一跳開始出現了問題。

圖片
System Metrics

除了網路層面以外,造成慢呼叫的還有一個重要原因是 IO 慢。例如 Client 訪問 DB 時可能出現時延抖動,而這些抖動通常是由 DB 的檔案讀寫慢導致。再例如大資料場景下一批 Worker 中可能總有那麼一兩個 Worker 完成的慢,排查後會發現通常是由於檔案讀寫慢導致。這裡 DeepFlow 的做法是觀測所有與應用呼叫相關的 IO 時延,並記錄所有的慢 IO 事件。實現層面,檔案 IO 與 Socket IO 事件可透過 tracepoint/kprobe hook 同樣一組函式獲取到,透過 FD 的型別可以進行區分。依靠這樣的能力,我們能快速的定位到 deepflow-server 寫入 clickhouse-server 偶發性慢是由於檔案 IO 時延抖動造成,且能定位到具體的檔案路徑。

低版核心:最後我們總結一下本節。我們希望在低版本核心中 DeepFlow 也能展現更多的能力。以 Kernel 4.14 為界,DeepFlow 在 4.14+ 的環境下可以基於 eBPF 實現全功能,包括加密資料的觀測、應用程式的效能指標、系統層面檔案 IO 效能指標,以及全自動的分散式追蹤能力。而在 4.14 以下的核心環境中,我們基於 cBPF 也實現了大部分的能力,包括對於明文協議、私有協議的觀測,對於壓縮協議部分頭部欄位的觀測,以及對於應用效能、網路效能指標的採集,對於效能指標的全棧路徑追蹤。

03|eBPF 自動關聯服務和資源標籤

講到這裡實際上我們整個目標只完成了一半。我們從原始資料中提取出來了指標、呼叫關係、呼叫鏈,但還需要以開發者熟悉的方式展現出來。Packet 和 Socket Data 中只有 IP 和埠號資訊,這是開發和運維都無法理解的。我們希望所有的資料都能從例項、服務、業務等維度按需展示。
在這個問題上我們也遇到了一些挑戰:我會先介紹從哪裡採集標籤,以及採集什麼樣的標籤;然後討論輪詢採集的方式會帶來哪些問題;接下來我會介紹一下基於 eBPF 的事件觸發的方案,來避免輪詢的缺陷;最後同樣的也會介紹一下我們在低版本核心環境下的一些能力。

標籤資料:首先僅透過 IP 地址是不能完整關聯客戶端和服務端的服務資訊的,這是因為實際環境中普遍存在 NAT,包括 SNAT、DNAT、FULLNAT 等。實際上透過單側的 IP+Port 也難以準確關聯,這是因為 Client Port Reuse 也是一個普遍存在的現象。因此,在 DeepFlow 中使用五元組來將通訊端點關聯至服務。具體來講,我們會透過 K8s apiserver 獲取 IP 對應的容器資源標籤;透過 CMDB 及指令碼化的外掛獲取 PID 對應的業務標籤資訊;然後再依靠下面要講的一些機制將 IP+Port 的五元組與 PID 關聯起來,最終實現資源、服務、業務標籤的自動注入。

輪詢方案:我們首先能想到的是透過輪詢 /proc/pid/net/ 資料夾來獲取 PID 與 Socket 五元組的關聯。每個 agent 獲取本機的關聯資訊,並透過 server 交換得到全域性的關聯資訊,從而使得每個 agent 能獨立的為客戶端和服務端標註雙端的 PID 資訊。

輪詢方案也會碰到一些挑戰,例如 LVS 場景下,/proc 下的 Socket 資訊指向的是 LVS,但從業務上來講我們希望獲取 LVS 背後的 RS 的程式資訊。DeepFlow 透過同步 LVS 轉發規則解決這個問題。具體來講,基於 LVS 規則和包頭中嵌入的 TOA(TCP Option Address)欄位,可以快速的在 RS 上定位客戶端的 PID,並透過 LVS 轉發規則快速的在客戶端側定位服務端的 PID。

觸發方案:當時輪詢總會存在時間間隔,因此永遠無法解決短連線的監控問題。在這方面 DeepFlow 也做了一些工作,這就是下面我們要介紹的觸發式方案。

受 TOA 的啟發,我們基於 eBPF 實現了一個 TOT(TCP Option Tracing)的機制,即在 TCP 包頭 Option 欄位中嵌入額外的資訊,表示傳送方的 IP 和 PID。這樣的話我們就能將源發的程式資訊告知對端了。為了實現這樣的機制,我們使用 eBPF sockops 和 tracepoint,一共 Hook 了五個函式。我們會在每個 TCP SYN、SYN-ACK 包中注入 TOT 資訊,使得連線新建時即能標記程式資訊。同時也會機率性的抽取 TCP PSH 包注入 TOT,使得對於長連線,即使 agent 的啟動時間滯後於業務程式,也能及時獲取到程式資訊。

低版核心:同樣這裡也分享一下我們在低版本核心上做的一些工作。首先輪詢的方案依靠掃描 /proc 資料夾實現,因此是能適配所有 2.6+ 的核心環境的。在 3.10 及以上的核心中,我們也提供了一個精巧的 ko 模組來實現 TOT 的注入,這個核心模組僅有 227 行程式碼,我們也進行了開源,歡迎大家使用。

04|Demo - 持續觀測全鏈路壓測效能瓶頸

圖片
OTel Demo

最後透過一個 Demo 介紹一下這些工作的效果。我們仍然是以 OTel 的電商 Demo 為例,仍然是關閉了其中的所有 OTel Instrumentation。選擇這個 Demo 的理由在於,他是一個典型的微服務架構的應用,且盡力模擬了比較真實的電商場景,微服務的實現語言涵蓋了十二種之多,也包含了 PostgreSQL、Redis、Kafka、Envoy 等中介軟體。

首先我們可以看到,不修改程式碼、不修改啟動引數、不重啟程式,我們已經能自動繪製微服務粒度的全景應用。當我們注入一個 1.5K QPS 的壓力時,能清晰的在拓撲中看到瓶頸鍊路。沿著 frontend 服務一路往下,也能快速的定位瓶頸服務 ProductCatalog。接下來我們分為三次,分別將 ProductCatalog 擴容至 2x、4x、8x 副本數,擴容的過程中也可清晰的看到瓶頸鍊路在逐漸消失,知道最終所有服務的時延恢復正常。

除了這個 Demo 意外,這裡也分享幾個我們在客戶處的實戰案例:

  • 某造車新勢力客戶,使用 DeepFlow 從數萬 Pod 中在 5 分鐘內定位 RDS 訪問量最大的 Pod、所屬服務、負責該服務的團隊。
  • 某股份制銀行客戶,信用卡核心業務上線受阻,壓測效能上不去,使用 DeepFlow 在 5 分鐘內發現兩個服務之間 API 閘道器是效能瓶頸,進而發現快取設定不合理。
  • 某網際網路客戶,使用 DeepFlow 在 5 分鐘內定位服務間 K8s CNI 效能瓶頸,識別由於環路造成的某個服務上下雲訪問時延週期性飆升的問題,雲廠商兩週無解。
  • 某證券客戶,使用 DeepFlow 在 5 分鐘內定位 ARP 故障導致 Pod 無法上線的問題,「瞬間」結束業務、系統、網路多部門「會商」。
  • 某基礎設施軟體客戶,使用 DeepFlow 在 5 分鐘內定位 Rust 客戶端使用 Tokio 不合理導致的 gRPC 偶發性超大時延,終結了 QA 及多個開發團隊之間踢來踢去的 Bug。
  • 某四大行客戶,使用 DeepFlow 在 5 分鐘內定位某個 NFVGW 例項對特定服務流量不轉發導致的客戶端頻繁重試。

從這些案例中我們能發現,依靠 eBPF 技術的零侵擾特性,我們能完整的覆蓋應用的全景呼叫拓撲,且能展現任意呼叫路徑中的全棧效能指標。得益於這些可觀測效能力,我們能快速的定位 RDS、API 閘道器、K8s CNI、ARP 故障、Rust Tokio 庫、NFVGW 造成的故障。

圖片
Distributed Profile

這是最後一張PPT,我想和大家分享一下 DeepFlow 對可觀測性的更多思考。在傳統 APM 中,我們通常使用 Span 之間的關聯關係以火焰圖的方式展現一次分散式呼叫(Trace),也會講所有的 Span 聚合為指標來展現所有服務之間的應用拓撲。我們發現拓撲展現了所有呼叫的資料,而追蹤展現了一個呼叫的資料,它們二者恰恰是取了兩個極端。DeepFlow 目前正在探索,中間是否有一個折中的點,他們展示一組聚合的火焰圖或拓撲圖,有點類似於單個程式的 Profile,但是用於分散式應用的場景,我們稱它為 Distributed Profile。我們相信這樣的折中會帶來效率的提升,它不香所有呼叫聚合而成的拓撲,會有太多噪聲;也不像單一請求展示出來的 Trace,問題排查需要一個一個 Trace 的看。

而 eBPF,正是絕佳的實現 Distributed Profile 的技術手段,請期待後續我們進一步的分享。

05|什麼是 DeepFlow

DeepFlow[3] 是一款開源的高度自動化的可觀測性平臺,是為雲原生應用開發者建設可觀測效能力而量身打造的全棧、全鏈路、高效能資料引擎。DeepFlow 使用 eBPF、WASM、OpenTelemetry 等新技術,創新的實現了 AutoTracing、AutoMetrics、AutoTagging、SmartEncoding 等核心機制,幫助開發者提升埋點插碼的自動化水平,降低可觀測性平臺的運維複雜度。利用 DeepFlow 的可程式設計能力和開放介面,開發者可以快速將其融入到自己的可觀測性技術棧中。

GitHub 地址:https://github.com/deepflowio/deepflow

訪問 DeepFlow Demo[4],體驗高度自動化的可觀測性新時代。

參考資料

[1] 回看連結: https://www.bilibili.com/video/BV1B14y1f7UB/

[2] PPT下載: http://yunshan-guangzhou.oss-cn-beijing.aliyuncs.com/yunshan-...

[3] DeepFlow: https://github.com/deepflowio/deepflow

[4] DeepFlow Demo: https://deepflow.yunshan.net/docs/zh/install/overview/

相關文章