成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

探索可觀測的新視角—— eBPF 在小紅書的實踐

云計算
小紅書可觀測團隊在過去一段時間內,對 eBPF 等新技術在可觀測的應用進行了探索,在通用流量分析、持續 Profiling 等領域進行落地,解決了之前碰到的一些痛點問題。通過 eBPF 技術的應用,團隊將可觀測能力從應用程序擴展到了內核,實現了對可觀測領域的進一步擴展。

在當前的云原生時代,隨著微服務架構的廣泛應用,云原生可觀測性概念被廣泛討論。可觀測技術建設,將有助于跟蹤、了解和診斷生產環境問題,輔助開發和運維人員快速發現、定位和解決問題,支撐風險追溯、經驗沉淀、故障預警,提升系統可靠性。云原生和微服務技術的不斷深入應用給可觀測提出了新的需求,在 Metrics、Logging、Tracing 等傳統可觀測范疇外,我們需要探索新的技術和方案。

小紅書可觀測團隊在過去一段時間內,對 eBPF 等新技術在可觀測的應用進行了探索,在通用流量分析、持續 Profiling 等領域進行落地,解決了之前碰到的一些痛點問題。通過 eBPF 技術的應用,團隊將可觀測能力從應用程序擴展到了內核,實現了對可觀測領域的進一步擴展。

01背景

在過去一段時間里,我們在生產上遇到了一些實際問題,如中臺服務被多上游服務訪問、或者提供 OpenApi 供外部服務調用,有時候會碰到接收到的流量異常上漲,自身應用在流量異常上漲的情況下CPU、內存可能會跟著飆升,往往會影響應用自身的穩定性的情況。更麻煩的是,此時我們有時候并且不知道調用方在哪。甚至存在開發環境訪問線上環境、跨機房訪問等情況。如下圖所示,可觀測的主機監控存儲集群,被未知的上游服務定期拉取數據,導致 CPU、內存異常上漲,嚴重的影響監控存儲本身的穩定性。

圖片

想要解決類似的問題,需要對流量進行實時的分析,并且做到語言、架構無關。而傳統的可觀測領域,缺少解決這種實時流量分析的通用手段。

此外,業務對于 C++ 性能退化的識別是一個普遍的訴求,之前我們對 C++服務的持續 Profiling 和性能退化檢測中,碰到了一些阻礙,其中主要的困難在于基于傳統的 linux perf 的方式,使用frame pointer 方式的回溯可能會出現結果不準確;使用 dwarf 方式的回溯會出現性能開銷比較大、耗時很長的問題。這些導致常態化 Profiling 無法實現,缺少低開銷且通用的解決方案。

基于以上背景,我們注意到了近些年在各領域興起并得到應用的 eBPF 技術,可以在 Linux 確定的內核函數 Hook 點運行,來執行用戶設定好的邏輯,常見的如對網絡數據包的監控、性能統計和安全審計等功能。我們初步判斷,eBPF 的這些特性能夠解決困擾我們的這些問題。

同時,eBPF 作為近些年來的 Linux 社區的新寵,受到了國內外互聯網大廠的青睞,在多個領域都得到了應用。國內各大互聯網公司的基礎架構部門也都在落地 eBPF,如字節、阿里、騰訊百度等等,都有著eBPF的落地經歷?;?eBPF 的開源項目也像雨后春筍一樣涌現出來。

所以我們嘗試把 eBPF 在可觀測所面臨的問題場景中進行落地,來解決我們遇到的一些痛點問題,最終服務好業務的穩定性。

02eBPF簡介

eBPF(extended Berkeley Packet Filter),是對 BPF (Berkeley Packet Filter) 技術的擴展。通過在內核中運行沙盒程序,eBPF 允許程序在不修改內核源代碼或加載內核模塊的前提下,擴展內核的能力。隨著 eBPF 技術的不斷完善和加強,eBPF 已經不再局限于定義中的網絡數據包的過濾,在可觀測、安全、網絡等方面得到了廣泛的應用。

圖片

傳統意義上的觀測性,是指在外部洞悉應用程序運行狀況的能力。基于 eBPF 可以無需侵入到應用程序內部、直接向內核添加代碼來收集數據的特點,我們可以直接從內核中收集、聚合自定義的數據指標。通過這種方式,我們將可觀測性擴展從應用程序擴展到了內核,實現對可觀測領域的進一步擴展。

常見的內核 Event 如下圖所示:

圖片

在這些 Hook 點上,都可以編寫應用程序來實現可觀測能力的覆蓋,同時探索更多深度觀測能力。

針對實際工作中遇到的痛點,我們基于 eBPF 技術,在流量分析和 Profiling 中進行了探索,下文分別對這兩個方面進行詳細的介紹。

03在流量分析的

在流量分析場景下,目前我們主要聚焦在 L4、L7 層:L4 層得到流量包的大小,L7 層進一步得到 QPS、RPC Method 等信息。

整體架構如下所示:


圖片

我們的 eBPF Agent 以 DaemonSet 方式部署,在啟動過程中,將 eBPF 程序加載到內核中,Hook 內核的 Tcp 數據收發等系統調用。主要流程:

  • Agent 通過接收下發的流量采集配置,在所在的 Node 上查找目標進程,在找到目標進程后,將目標進程號(Pid)傳遞到內核中。
  • 內核態的 eBPF 程序收到Pid信息后,開始采集流量并做輕量級的處理,并將數據發送到 eBPF Map 中。
  • 用戶態的 eBPF Agent 讀取 eBPF Map 中的數據,做聚合和處理,生成 Metrics 指標。
  • eBPF Collector 集中式的采集各個 eBPF Agent 生成的 Metrics 指標;在采集到指標后,根據指標中的上下游 IP 信息來查詢 Meta 服務,獲取到對應的應用信息,并補充到 Metrics 指標中;最終寫入到 Vms 存儲供查詢。

下面分別從內核態、用戶態、eBPF Collector 等幾個方面來詳細的闡述。

3.1. 內核態

3.1.1. L4層流量

一個典型的 Client-Server 之間的收發包流程,如下:

圖片

Client-Server之間,先建立連接:Server 通過 bind、listen 來監聽端口,Client 通過 connect 來與 Server 創建連接;Server 在監聽到這個請求之后,會調用 accept 函數取接收請求,這樣就建立了連接。建立連接之后,Client 可以發出數據包,在L4層,關鍵函數是 tcp_sendmsg。

基于上面的流程,我們主要關注的 Hook 點如下:

圖片

其中,對于 Server 來說,我們沒有 Hook tcp_recvmsg,而是 Hook tcp_cleanup_rbuf。這主要是因為一方面 tcp_recvmsg 可能存在統計上的遺漏和重復;另一方面,tcp_cleanup_rbuf 的執行次數低于 tcp_recvmsg,可以降低消耗。

在Hook tcp_sendmsg和tcp_cleanup_rbuf中,根據struct sock對象,拿到上下游的IP、Port、數據包大小等關鍵信息,并Output到用戶態。

3.1.2. L7層流量

L4層面的流量提供了網絡流量大小。有時候,我們還想要知道更多的信息,如QPS、延遲、消息協議、Rpc Method、Redis 命令等等。在這種情況下,我們需要進一步來實現 L7 層的流量分析功能。

我們通過Hook讀寫相關的系統調用,來獲取到服務之間的流量數據,常見的讀寫相關的系統調用,如下:

圖片

通過Hook這些系統調用,我們最終需要拿到的是原始的報文buf數據、對端地址信息(socket address),并基于 buf 數據和 socket address 處理得到 QPS、協議、RPC method 等信息。

基本流程如下:

  • 通過 tracepoint/probe 追蹤 socket syscall 相關的函數,Hook 并根據 Pid 進行過濾,保留 Pid 收發的流量 buf 數據;
  • 根據 buf 數據,提取 socket 元信息,獲取 socket address;
  • 根據 buf 數據,進行相應的協議推斷,判斷是否是我們支持的協議,不是則設置為 unknown;
  • 將原始的流量 buf 數據 Output 到用戶態,供進一步處理。

其中,兩個關鍵的過程分別是獲取 socket address、協議推斷。

socket address獲?。?/strong>

一個方案是Hook 建立連接的系統調用,如 sys_enter_connect、inet_sock_set_state 等,并解析參數中的 skaddr 的信息來拿到 IP、Port。這種方案的優點是簡單易實現,但是這種方案的問題是,對于在我們 Agent 部署之前就存在的長連接來說,我們無法捕獲到相應的事件和相應的信息。

我們采用的方法是是通過 bpf_get_current_task 來拿到 task_struct 類型的 task,來獲取 socket 對象,進而拿到 sockaddr:

  • task_struct 中的 files 字段,類型為 files_struct;
  • 根據 files 拿到 fdtable 字段,是當前進程的文件描述符表;
  • 再從 fdtable 中,根據 socket 的 fd,拿到 socket 的 file 結構;
  • socket 的 file 中,有個 sock 類型的 sk 對象,就是 socket 的內核對象指針;根據 sk 對象,就可以得到 IP、Port、UDP 還是 TCP、IPV4 還是 IPV6 等各種屬性。

協議推斷:

根據上述列出來的讀寫系統調用中,拿到的原始的字節流 buf 數據,可以來嘗試解析對應的應用協議,直接遵照協議規范進行解析。當前常見的協議如 Http1、Thrift、Redis、Baidu-std 等,目前我們都已經支持了;后續會支持如 Mysql 等更多協議的解析推斷。

此外,在協議的解析推斷過程中,另外一個問題是業務消息的拆分和重組:在實際中業務進程的一次數據收發,在系統調用層面,可能會拆分成多次系統調用來進行讀寫,可能會導致后續的 buf 都無法正確的解析出協議來。

為了解決這種問題,當一個 socket 上的 buf 數據在協議推斷成功后,將 socket 和協議信息保存在 socket info 中,并將 socket info 進行緩存;該 socket 上后續的 buf 數據在協議解析推斷失敗后,會默認使用該 socket info 中的協議信息;如果后續 buf 數據協議解析成功且多次不同時,對協議進行覆蓋。這樣,可以盡可能降低解析錯誤的概率。最后,Hook close 系統調用,在 socket close 的時候,把 socket info 清理掉。此外,利用數據包之間的關聯來判斷協議,即 request、response 之間的協議應該是一樣的,來進一步降低解析錯誤的概率。

3.1.3. 內核適配

基于 eBPF,應用開發的模式主要有兩種:

  • BPF 編譯器集合 (BCC Tools) 工具包提供了許多有用的資源和示例來構建有效的內核跟蹤和操作程序。
  • BPF CO-RE (Compile Once – Run Everywhere)是與 BCC 框架不同的開發部署模式,使用 BTF來解決編譯依賴問題。

BCC的優點是提供了很多有用的示例,同時還有多種前端語言(主要是用戶態用來處理加載到內核態BPF程序的輸出和交互)來輔助進行編程,如Python、Golang。存在的問題是:

  • 使用 Clang 修改編寫的 BPF 程序,當出現問題時,排查問題更加困難。
  • 類似一種動態語言的方式,BPF 程序是在運行時編譯的,編譯的時候需要工具鏈和內核文件。編譯依賴是脆弱的、容易失敗,所以總體不可控,兼容性不夠好。
  • 應用在啟動時,編譯BPF程序會占用大量的CPU和內存資源,在大量的低規格的機器上,可能會影響業務進程。

這些問題,特別是兼容性問題和性能問題,對于我們想要在線上大規模部署的話,是很大的阻礙。

BPF CO-RE 依賴內核特性支持 BTF,將內核的數據結構類型構建在內核中。用戶態的程序可以導出 BTF 成一個單獨的大的.h 頭文件(如vmlinux.h),這個頭文件包含了所有的內核內部類型,BPF 程序只要依賴這個頭文件就行,不需要安裝內核頭文件的包了。這樣就可以減少依賴,進行提前編譯。

因此,考慮到我們需要大規模部署并且長時間運行,我們需要盡可能降低資源占用、提高性能,我們選擇了 CO-RE 方式。

使用 BTF 機制,需要內核開啟了 CONFIG_DEBUG_INFO_BTF選項(CONFIG_DEBUG_INFO_BTF=y)。在我們上線覆蓋過程中,遇到了部分機器的內核是5.4,同時沒有開啟 CONFIG_DEBUG_INFO_BTF 選項。對于這些沒有開啟的內核,我們生成并導入對應版本的 BTF 文件;我們的 eBPF Agent在啟動時先檢測內核版本和 CONFIG_DEBUG_INFO_BTF 選項;如果選項沒有開啟,則根據內核版本加載對應的 BTF 文件。當前我們對線上主要的5.4、5.10的多個內核版本做了適配。

3.2. 用戶態

用戶態的主要工作是通過接收采集配置來選擇出目標 Pid,傳遞給內核 eBPF 程序來開啟流量分析;從內核中讀取流量數據并處理。基本的示意圖如下:

圖片

3.2.1. 生效機制

在實際應用中,如果采集 Node 上所有的流量數據,消耗會很大;同時大量的未知流量信息會帶來很大的干擾。因此,我們需要在 Node 上選擇出我們實際關注的 Pid,同時將 Pid 信息傳遞到內核中,在內核流量采集分析的時候,根據Pid進行過濾。

當前流量分析功能是按需開啟的。在 Node 上部署 eBPF Agent 后,通過配置中心下發配置來決定對哪些服務開啟、開啟的 K8S 集群,以及生效的比例等。Agent 在接收到配置后,根據 Pod 過濾規則,在所屬的 Node 上查找匹配到的 Pod;在 Pod 的各個 Container 中,根據 Container name 查找匹配的 Container;最后根據 K8S 集群信息和 Pid name,在 Container 中匹配到 Pid。

匹配到 Pid 之后,將 Pid 傳遞給內核 eBPF 程序來開啟采集。在需要關閉采集的時候,將 Pid 從內核中刪除即可。

3.2.2 eBPF C 程序管理

在拿到 Pids 之后,我們將 eBPF C程序、相關的 eBPF Map 以及 Pids 加載到內核中,這里涉及到 eBPF C程序的管理和數據交互。

為了簡化 eBPF C代碼的開發和調試流程,我們支持了配置化的對 eBPF 程序的加載、卸載、數據讀取等。整體結構如下:

圖片

編譯&加載:

eBPF 的 C 代碼,使用 Clang 和 LLVM 工具鏈來編譯 eBPF 代碼,生成可加載的字節碼文件。將字節碼文件作為 ELF 文件資源進行讀取,并解析其中的 Maps、Program 等。在解析之后,通過 BPF 系統調用:對 Maps 進行 BPF_MAP_CREATE 創建 Maps;對 Program 進行BPF_PROG_LOAD。這樣將字節碼加載到內核中并進行安全驗證。

Link:

根據配置文件中配置的 probe、tracepoint 信息,通過 BPF_LINK_CREATE BPF 系統調用,將 eBPF 程序掛載到對應的內核事件上,從而實現對這些事件的監聽,當內核執行到對應的事件,會觸發并執行對應的 eBPF 程序邏輯。

數據讀?。?/strong>

Map 是 eBPF 內核程序和用戶態程序之間交互的橋梁。在用戶態中,根據配置文件中配置的 Map,啟動 Epoll 來讀取 Map 中的數據。

3.2.3. 內核數據接收與處理

圖片

eBPF C程序被加載進內核后,代理程序(eBPF Agent)便開始通過 Epoll 機制讀取 eBPF Map 中的數據。這些數據包含了業務模塊間直接交換的原始流量。

采樣流量開關:對于一些輕量級的 Proxy 服務,往往單個實例的流量很大;同時,單個 Node 上可能部署多個實例,這樣一來 Node 上部署的eBPF Agent 采集流量并做處理的壓力就很大。為了解決 eBPF Agent 流量處理壓力大的問題,eBPF Agent 實現了流量采樣機制,Agent 通過配置中心獲取采樣比例配置,通過 eBPF Map 將配置信息傳給內核。eBPF Agent 也通過配置中心配置下發實現了更細粒度的流量開關,能精確控制 L4/L7 的進/出不同方向的流量采集,按需開啟,來實現節約資源消耗的目的。

流量數據解析:當流量數據傳到用戶側時 Agent 根據 L7 協議規范進一步解析并提供更多信息:在如網關場景下,通過精準解析 HTTP 消息,可以實時獲取到請求的實際 IP;在 RPC 場景下,通過遞歸解析Thrift消息,可以識別 RPC 方法,任意 RPC 參數等信息(比如排序服務的模型信息);在 Redis 場景下,可以解析Redis命令。

指標數據生成:在解析補全 L7 流量信息后,Agent 將消息事件進行哈希后放入 Queue 中,保證后續構成相同指標的事件總是被緩存在同一個隊列中。在消費 Queue 中緩存的消息事件時,消息事件流量 IP、方向、協議等信息被聚合為流量指標;同時將流量指標根據采樣率進行流量還原,最終生成 Prometheus 格式的 Metrics 指標。此外,為了控制資源消耗、內存使用和監控指標的過度膨脹,Agent 會在實例IP變動后,需要及時進行數據過期清理。

3.3. 指標采集和處理

對于 L4、L7 層流量數據來說,我們在用戶態拿到的數據中,包含了上下游服務的 IP、Port。實際生產環境中,上下游服務實例非常多,并且隨著應用發布會不斷變化,單純提供IP對開發和運維同學的幫助不大。因此,我們需要將 IP、Port 關聯出所屬的應用、服務,并提供更多的相關信息,如 Region、K8S 集群等信息。

我們部署 eBPF-Collector 來統一采集部署的eBPF Agent的指標數據,處理后進行存儲。

3.3.1 元數據關聯

我們通過 CMDB 查詢出 IP:Port 對應的應用名/區域等服務元信息。由于指標數據量巨大,不可能為每一個數據點請求一次 CMDB 來獲取元信息,因此我們設計了元信息緩存來加速查詢。

Cache 整體架構

我們最初將元信息緩存設計為指標采集服務(eBPF Collector)的本地內存緩存。但是由于相同的 IP:Port 查詢請求會等概率地出現在所有采集分片中,采集分片的本地緩存會保存幾乎全部被用到的數據。在水平擴容分片時,本地內存緩存數目也會成倍增加,這意味著當緩存更新時,緩存對 CMDB 的請求數目也會隨服務分片數成倍增加,這會對 CMDB 服務造成巨大查詢壓力。為了解決這一問題,我們重新設計了如下圖所示的新的 Cache Server 結構:

圖片

我們將緩存服務獨立部署為單獨的 Cache Server,與指標采集服務隔離。這解除了指標采集服務和元信息緩存的耦合,防止指標采集服務水平擴容帶來的元信息重復請求問題。

Cache 內部結構

圖片

元信息緩存是基于 Working Set 的思路設計的,我們將查詢到的元信息存儲一段時間,同時使用 Singleflight 機制,合并同一時刻出現的相同的元信息查詢請求,降低對 CMDB 的請求并發度。

為了降低查詢延遲,緩存除了根據預先確定的 TTL 刪除一段時間內未訪問的元信息,還會對仍在緩存中的元信息每隔若干時間進行后臺刷新來更新數據。

由于緩存的元信息都保存在內存中,Cache Server 服務重啟/發布后會導致緩存的數據丟失。這意味著每次啟動都需要幾乎大量拉取 CMDB 元信息,我們為緩存服務添加了緩存持久化功能,緩存服務會將緩存持久化在硬盤中,重啟后直接嘗試讀取舊緩存,防止冷啟動問題。

3.3.2. 查詢性能優化

eBPF 的網絡流指標量非常大,一次采樣周期內采集到的指標量超過1.1億;并且高度集中在L4、L7的三四個指標中,這給指標查詢帶來了巨大壓力,日??刹樵兊臅r間范圍不超過一天,并且經常查詢超時。

但是相對而言,eBPF 指標的查詢方式比較固定,所以我們可以根據預先定義的 PromQL 查詢對指標進行流式預聚合,將預聚合之后的指標寫入存儲。這相當于將指標鏈路中采集之后鏈路(比如存儲/查詢)的計算壓力前置,大幅降低寫入存儲的指標量,進而減少查詢的數據量,加快查詢速度和可查詢的時間范圍。整體過程如下圖:

圖片

就具體實現來說,我們通過配置中心下發預聚合配置,當配置有變更時,服務會原子地更新預聚合算子(Operator)并重置預聚合狀態。

當指標數據到達預聚合服務時,數據會被復制一份,復制后的數據會經過預聚合 State Operator 來計算得到預聚合中間狀態,并保存在內存中;根據配置不同,每隔若干時間(比如 30s)服務會將中間狀態通過 Merge Operator 合并為聚合后的數據,并寫入游數據源。

為了保證數據的完整性,預聚合服務起停時的最近聚合數據會被丟棄。對于單副本預聚合服務,服務起停時指標可能出現斷點,我們使用雙副本加上數據去重來避免這個問題。

經過對比驗證,我們測試發現通過指標預聚合,指標查詢速度提升能 10 倍以上;查詢時間范圍從一天延長至至少一周以上。

3.4. 產品化&實際落地的場景

當前的流量分析功能和“目標應用”的語言、框架無關,接入時不需要業務方做任何修改,對業務無感知、無侵入。我們在部署 Agent 并發布配置后,就會產生實時的、持續的流量數據,數據保存一個月。生效、取消生效的過程快速,秒級生效。

在性能上,在當前所有覆蓋的場景下,eBPF Agent 日常平均CPU使用量在0.1 Core、內存在200MB;CPU Limit設置為0.5 Core,內存1GB,對業務基本無影響。

當前在小紅書的Redis、KV存儲、推薦、廣告等場景規模落地,接入服務過一千。下面介紹流量分析的使用方式和一些實際 Case。

3.4.1. 產品

3.4.1.1. 流量大盤

L4層協議,當前支持展示"目標應用"的流量大小。

作為服務端(Server),接收到上游請求的流量(MB/s)、返回給上游的流量(MB/s);作為客戶端(Client),請求下游的流量(MB/s)、接收到下游返回的流量(MB/s)。

圖片

上圖中展示的一個排序服務的L4層詳情:作為服務端(Server),接收到上游的請求流量(MB/s)、返回給上游的流量(MB/s);作為客戶端(Client)請求下游的流量(MB/s)、接收到下游返回的流量(MB/s)。

L7層的流量分析,例子如下:

圖片

當前支持展示"目標應用":作為服務端(Server),接收到上游的請求QPS、返回給上游的QPS;作為客戶端(Client)請求下游的QPS、接收到下游返回的QPS。此外,還展示對應的應用協議(當前支持Thrift、Redis、Http)、服務部署的Region(上海、南京、杭州)等信息。

此外,我們還提供了OpenApi接口,來查詢服務的上下游流量指標情況。Redis、KV存儲等存儲服務的高可用架構規范治理過程中,通過這種方式來獲取上游服務的來源和訪問情況。

3.4.1.2. 服務拓撲

基于 eBPF 的服務流量指標,我們可以構造出服務之間拓撲關系,所有 A 服務發往 B 服務的流量都會聚合為服務 A 到服務 B 的一條邊,由此構成拓撲圖。我們定期拉取一天的的流量指標,聚合出服務拓撲邊,并將邊信息存儲在 Clickhouse 中。當用戶查詢拓撲關系時,服務從 Clickhouse 中取出拓撲邊信息構造拓撲圖。下圖展示了由 eBPF 流量指標獲取的兩層拓撲圖:

圖片

3.4.2. 落地場景

在實際覆蓋過程中,流量分析可以輔助定位流量上漲的來源確定、偶發的流量、服務下線過程中的流量排空等問題。

Case 1.  服務下線前,偶發流量的來源定位

問題背景:電商的研發同學向我們咨詢,他們有個服務在準備下線的時候,遇到個問題:還有偶發的、非常零星的上游流量會訪問他們的服務,訪問的頻率在每小時十幾個請求。擔心貿然的下線會影響穩定性,他們希望幫忙定位這些零星流量的來源。流量情況如下所示:

圖片

這種零星的流量,夾雜在日常的其他消息中,常規的抓包是很難定位的。我們的eBPF 流量來源分析,因為可以做到任意時刻、實時的流量采集,可以來解決這種問題。

我們在部署并開啟了 eBPF 的 100% 全采樣的流量分析。進一步了解到業務同學關心的零星請求是特定的 Thrift Method,所以想要定位的話,需要在采集 Thrift 流量后,進一步對 Thrift 消息進行解析和分析。經過解析實際的消息并進行 Thrift Method 聚合后,終于可以看到了小時級的偶發的流量來源,可以看到對應的上游服務 IP,如下所示:

圖片

根據 IP 很快就成功的定位到了上游服務,是一個很古老的前端 Node 服務。

Case2.  流量上漲的來源確定

問題背景:Redis 的一個集群,某晚上海區異常,流量大幅上漲導致集群被打掛,影響內流初排成功率。初步找到的流量來源看起來不是真正的大頭,需要排查上游流量上漲的來源。

我們通過部署 eBPF Agent 并采集分析流量,在幾分鐘內,識別出真實的上游流量來源和流量大小,輔助業務同學進行止損。

圖片


04在持續Profiling的應用

對于 C++服務的 Profiling 和性能退化檢測來說,我們之前碰到了一些阻礙,其中主要的困難在于基于 linux perf 實現的常態化 Profiling 性能開銷比較大、耗時很長?;?linux perf 來 Profiling 的兩個主要步驟:

  • perf record 按固定頻率采集進程內各個線程棧信息, 生成性能事件
  • perf script 解析性能事件,轉換為可讀數據,將棧幀地址轉換為對應的函數名稱與所屬文件和行號

在第一步 perf record 采集性能事件中,一般使用 -g 參數來獲取完整的調用棧,默認使用 frame pointer。然而公司內 C++ 服務往往會開啟編譯優化選項,frame pointer 不可用,導致 profiling 的結果很大程度上失真。為了保證覆蓋率,一般使用 -g dwarf 參數,指定使用 dwarf 方式來回溯獲取調用棧。使用 dwarf 會遇到一個問題就是中間數據量大:為了后續回溯的需要,會將每個 CPU 的完整棧從內核拷貝出來;核數越多、采集時間越長,得到的棧數據就越大,以廣告的一個服務為例,采樣 10s 會生成將近 175MB 的數據。

第二步 perf script 解析性能事件,首先需要將第一步中的所有性能事件的棧進行回溯,拿到完整的調用鏈棧幀地址;再將地址通過 addr2line 工具轉換為函數名稱和文件信息。這時遇到了第二個問題,由于數據量大,整個轉換過程耗費大量 CPU,耗時也很久。

以廣告的一個服務為例,在服務的 Node 上部署 perf Agent,在 Agen t的 CPU Limits 為0.5 Core 的情況下,對服務進行 Profiling;采樣 10s 的數據并處理,整體耗時將近 1 小時,并且全程 CPU 打滿,如下圖所示:

圖片

這種資源消耗和耗時情況,對于需要大面積部署、常態化的持續 Profiling并基于 Profiling 數據進行分析性能退化來說,是基本不可行的。

針對這個問題,我們基于 eBPF 來重新實現 C++ 的 Profiling,大幅降低 C++ 服務的 Profiling 資源消耗、整體耗時,來實現真正的持續 Profiling。核心的思路是:在 Node 上部署 Profiling Agent,在內核性能事件生成后,直接在內核繼續完成棧回溯和聚合,大幅降低拷貝到用戶態的棧數據量;通過 Collector 服務來集中式的采集各個 Profiling Agent 產生的棧數據并做處理。這種模式下,Agent 的計算壓力很小,可以實現持續 Profiling;Collector 的處理邏輯對各個 Agent 可以復用,整體消耗低??傮w架構如下:

圖片

簡要的過程如下:

  • eBPF Agent 用戶態程序根據下發的采集配置,獲取目前服務的 Pid。根據 Pid,獲取對應的內存分布信息以及使用的可執行文件內容,預處理后通過 eBPF Map 傳遞給內核態供?;厮輹r查找;
  • eBPF Agent 內核態程序由 CPU Cycles 性能采樣事件觸發,從當前執行位置回溯得到完整調用棧,聚合并保存在 eBPF Map 中;
  • eBPF Agent 用戶態按固定頻率從 eBPF Map 獲取每條調用棧的命中次數,轉化為 pprof 格式數據;
  • eBPF Collector 按固定頻率從 eBPF Agent 獲取 pprof 格式數據;完成符號解析、生成火焰圖,并寫入存儲;
  • 寫入的數據支持實時火焰圖、性能對比分析、性能退化監測等功能。

下面分別從內核態、eBPF agent用戶態、eBPF Collector 分別詳細的介紹。

4.1 內核態

相比于 linux perf 將棧拷貝到用戶態后再做回溯,我們選擇借助 eBPF 提供的能力在內核態直接完成回溯。這樣帶來了很多好處:

  • 大幅減少內核態到用戶態的數據拷貝。
  • 在內核態完成回溯后,重復命中采樣的調用??梢灾苯臃纸M累積,數據量不隨采集時間線型增長。

下面具體介紹我們在內核態做了哪些工作,以及如何在內核態完成基于 dwarf 的?;厮?/strong>的。

圖片

eBPF 原生支持 linux perf 的性能事件, 我們只需要編寫對應的 eBPF 程序加載到對應的 perf event 就可以按固定的采樣頻率觸發 eBPF 程序回調。

eBPF 程序會讀取當前的三個寄存器(ip: 指向下一條指令, sp: 棧頂地址, bp: 棧幀基址),這三個寄存器的值是?;厮葸^程的起點。回溯的過程就是將這三個寄存器的值反復地恢復到當前函數被調用前的值,直到沒有函數調用為止。

回溯完成后更新獲得的調用棧的命中次數到 eBPF Map 中,待用戶側采集使用。

eBPF 的verfier機制會限制程序復雜度和指令條數,但調用棧深度可能會很長,無法一次完成回溯。我們限制了 eBPF 程序中的循環次數,當循環完成仍未完成回溯時,我們用尾調用的方式重新調用自身繼續回溯直到完成。

一般來說,內核函數與 jit 生成的函數會使用 framepointer 方式調用壓棧,回溯也使用 framepointer。而大部分用戶態的函數經過編譯優化后 framepointer 不可用,需要使用 eh_frame 段中的 cfi 指令信息來輔助回溯。

下面具體解析函數的兩種回溯方式。

4.1.1. framepointer 方式回溯

framepointer 的意思就是使用一個獨立的寄存器保存棧基址,一般用來訪問函數參數,也用來回溯函數調用, framepointer 一般就是指代 bp 寄存器。下圖是包含了 framepointer 的函數調用壓棧方式。

首先壓棧返回地址,也就是調用函數返回后繼續執行的下一條指令

然后壓棧 bp 寄存器內容,把 bp 寄存器更新為新的棧幀基址

圖片

回溯其實就是調用函數的反向過程,由于 bp 寄存器的內容是?;罚鴹;匪傅牡胤奖4媪?caller 函數的 bp。只要反復將 bp 寄存器的值作為指針讀取值更新到 bp,就可以完成回溯。

我們關心的函數的 ip 指令地址,可以基于 bp 偏移得到。

如果只是使用 framepointer 方式回溯,我們只需要 bp 和 ip 的內容。但是實際程序運行場景中往往會出現帶 framepointer 和不帶 framepointer 函數互相調用的情況,而不帶 framepointer 的函數需要使用 eh_frame 信息回溯,依賴 sp 寄存器。

所以我們也保留 sp 寄存器的值,也基于 bp 偏移得到。

4.1.2. eh_frame方式回溯

framepointer 方式占用了一個專用的寄存器,函數執行過程中很少使用,而且每次需要壓棧。整體來說帶來了額外的內存開銷?,F代編譯器在開啟編譯優化的情況下不再使用 framepointer,這個時候我們的 bp 寄存器不再保存棧幀基址,而是作為通用寄存器使用,提高了內存效率。

不使用 framepointer 的函數在調用時,不再壓棧 bp 寄存器了

圖片

但是函數調試/異常處理都需要用到回溯信息,沒有 framepointer 的函數,它的回溯信息會在編譯期間通過插入 cfi 指令的方式記錄,cfi 指令最終會生成可執行 elf 文件中的 .eh_frame 段。

cfi 指令示例

每當發生棧變量分配和回收時,編譯器生成一條 cfi 指令更新如何從棧頂找到?;返男畔?/p>

每當寄存器壓棧時,編譯器生成一條 cfi 指令更新如何從?;坊謴图拇嫫鲀热莸男畔?/p>

圖片

回溯的思路類似 framepointer 方式,先拿到?;?,通過棧基址偏移獲取其他關心的寄存器內容。

cfi 指令一般記錄棧基址到棧頂的距離,每次回溯時,我們讀取 sp 寄存器的內容與對應的 cfi 指令信息找到?;贰S辛藯;吩偻ㄟ^偏移找到 下一輪回溯使用的 bp ip sp 寄存器。

4.1.2.1. 使用回溯表簡化 cfi 指令使用

由于需要在內核側 eBPF 程序中完成回溯,直接解析 cfi 指令過于復雜,我們將 cfi 指令生成 key 為指令地址的一張表,告訴回溯程序當執行到任意指令時如何找到?;?,如何恢復寄存器內容,表內容如下圖:

圖片

4.1.2.2. 回溯表結構設計

可執行文件大小不一,指令數差異大,生成的回溯表大小不一,但 eBPF Map 的Key、Value 都是固定大小,為了高效存儲回溯表,我們使用兩個 Map 分別作為數據表、索引表,如下圖所示:

圖片

數據表:用來保存具體回溯信息,由若干個shard組成,每個shard有數據量上限。對某個Pid開啟Profiling時,對Pid的所有可執行文件進行遍歷和解析,生成回溯表后,將回溯表數據append 寫入數據表。寫入數據表過程是按 shard 依次寫滿。

索引表:提供可執行文件到數據表之間的索引,定位可執行文件關聯了數據表中分段。每次數據表寫滿一個shard 或當前可執行文件的回溯表寫完,在索引表中記錄一條數據表分段信息。

回溯表查找時,首先根據 pc (指令地址),在索引表找到可執行文件對應的所有分段,根據包含關系確定具體分段;最后根據分段對應的數據表信息,在數據表中二分查找。

4.2 用戶態

圖片

4.2.1. 生成回溯表

讀取 Profiling 進程的 Mapping (內存地址分布,/proc/$pid/maps 文件),將用到的可執行文件生成回溯表,寫入 eBPF Map 傳遞到內核態。

進程的 Mapping 不是固定的,部分情況下會發生改變,比如動態鏈接庫加載,jit 代碼生成,這些都會在運行時改變進程 Mapping。

為了保證采樣數據的完整性和正確性,每次采集 Profiling 數據時我們先檢查 Mapping 可執行的部分有沒有發生變化,如果變化就廢棄這一次采集,更新 eBPF Map 的內容到 Mapping 的最新狀態。

4.2.2. 獲取性能采樣數據

定時采集內核態暴露出來的調用棧和采樣次數,按 Pid 聚合,為每個 Pid 生成 pprof 格式的性能采樣數據, 通過 http 接口暴露給采集側。

4.2.2.1. 內核函數符號解析

我們在 Agent 側完成內核函數的符號解析(/proc/kallsyms 文件),因為不同節點的內核符號不同,內核函數必須在本地解析。另外內核函數的查找較為簡單輕量。

用戶態函數我們不在 Agent 側解析,因為內聯函數的符號解析依賴 dwarf, 是個比較重的查找過程,要解析 debug_info 段,占用大量內存和CPU, 對于 Agent 來說負載過重。而且對不同節點部署的相同服務來說,符號解析是個重復動作,放在采集側完成能更有效利用緩存,避免重復計算。

4.2.2.2. 關聯元數據標簽

在發現 Profiling 進程的過程中,我們已經保留了進程的元數據,包括 Pod 名稱、鏡像版本、可用區等等,這些信息作為標簽附加在性能采樣數據中,方便后續實現過濾下鉆查詢。

為何使用 pprof 格式:有豐富工具類庫可使用;序列化壓縮效率高,所有字符串通過 id 引用;opentelemetry 規范中 profiling 數據模型是基于 pprof 格式設計的, 使用 pprof 方便后續對接業界規范。

4.2.3 精簡可執行文件

精簡可執行文件,抽取包含符號信息的分段,傳遞給采集側供符號解析時使用。

符號信息有2種來源,一種是可執行文件的 .symtab 段,另一種是 dwarf。我們抽取這些分段合成一個精簡過的 debuginfo 文件,通過 http 接口暴露給采集側。debuginfo 文件可執行文件 buildid 緩存,避免相同的可執行文件被重復抽取。

4.3 采集側

采集側主要對各個Agent的性能采樣數據進行集中采集和處理:

圖片

基本流程:

  • 服務發現并定時抓取所有eBPF Agent 的性能采樣數據
  • 抓取性能采樣數據后,對缺失名稱的函數地址進行查找補足函數名
  • 如果是第一次遇到的可執行文件,異步下載與構造符號索引,在索引 ready 之前始終忽略當前采樣,直接返回;
  • 如果索引可用,先在 dwarf 中查找當前地址關聯的函數和內聯函數,以及所屬文件和行號;
  • 如果當前地址不在 dwarf 的范圍里,回退到 symtab 中查找函數名稱
  • 符號關聯完成后我們就拿到了完整的生成火焰圖所需的所有數據,這份數據我們生成并上傳火焰圖供性能平臺訪問,并且寫入 ck 存儲支持性能對比與性能退化監測能力。

符號信息有2種來源,一種是可執行文件的 .symtab 段, 提供了函數地址到函數名的簡單映射,但是不包含內聯函數信息。symtab 形式的的符號查找非常簡單,地址和函數名一一對應。

另一種是查找 dwarf 信息, .debug_xxx 段包含了每個函數覆蓋的指令范圍,函數名稱,調用了哪些內聯函數,屬于哪個文件,行號等豐富信息。

下面重點介紹dwarf的結構和符號查找過程。

4.3.1. dwarf 結構&符號查找

如下圖所示,結合一個例子,我們具體介紹下dwarf的結構和符合查找過程:

圖片


結構:

dwarf 是ELF文件的debug_info section,用來表示源碼結構信息,整體是樹狀結構,由DIE(debug info entry) 構成,每個 DIE 有Tag 字段來區分類型,且各自帶有不同屬性信息。

最外層的 DIE 表示代碼文件(DW_TAG_compile_unit,cu),如上圖中的server.c。文件下層的DIE是各種數據結構、函數的聲明,其中我們主要關心兩種類型:函數(DW_TAG_subprogram) 與內聯函數(DW_TAG_inlined_subroutine),內聯函數處于調用它的函數下層,在上圖中都有所展示。

此外,文件、函數、內聯函數,都有屬性來代表指令范圍(如上圖中的[0x40000,0x500000])。指令范圍指的是,源代碼編譯生成的機器指令,在 .text 代碼段中的偏移范圍。函數編譯生成的機器指令分布不一定是連續的一段,可能由多段范圍構成,查找時每段都參與匹配。內聯函數的生成的指令是調用它的函數的一部分,它的指令范圍被它的 caller 函數覆蓋。

我們會使用這個屬性與待查找的指令地址做匹配。

查找過程:

查找函數的邏輯類似addr2line:

  • 定位文件DIE:對給定的 pc (指令地址),定位文件DIE (指令范圍包含此地址的);
  • 文件DIE的子節點遍歷:指令范圍包含地址的原則,對給定的pc,在各個子節點中進行遍歷和查找;得到所有匹配的結點,包括內聯函數的結點;
  • 內聯函數:函數 DIE 提供了屬性可獲取函數名稱、所屬文件與行號,內聯函數不包含這些信息,需要通過 DW_AT_abstrct_origin 屬性 (如上圖中綠色序號)找到原始函數聲明。

將所有函數信息按調用層級返回即完成查找,輸出函數調用鏈信息。整個過程中,文件DIE的定位和子結點遍歷如上圖中pc和藍色結點所示;內聯函數的定位如圖中綠色地址的對應關系所示。

4.3.2. 查找優化

4.3.2.1. dwarf 索引

dwarf 符號查找的整個查找過程需要加載整個 dwarf 結構,對于復雜項目來說,文件數量多且空間大。支持查找需要耗費很多內存,跳轉過程也較為復雜。對于執行一次性命令的工具適合這種方法,對于持續常態化運行的服務來說就比較浪費。

為了保證查找的性能、節省內存,我們將 dwarf 的內容先做一次讀取,取出我們關心的信息來構造成索引,后續的查找就可以基于索引來,這樣 dwarf 結構就可以從內存中釋放。索引構建的過程分兩步:

  • 構建全量的函數信息集合
  • 對 dwarf 進行深度優先遍歷,取出每個遇到的函數和內聯函數的信息,包括函數名、文件名、指令范圍等屬性,如下圖藍色部分所示;
  • 遍歷完成后,對內聯函數查找函數定義,補足信息
  • 構建地址范圍索引
  • 獲取所有函數的指令范圍屬性,每段指令范圍關聯到函數數組的下標;

  • 對范圍進行排序:首先比較每個范圍的開始地址,這一步是為了支持函數地址的二分查找;如果開始地址相同(如一個函數的第一行就是執行一個內聯函數),比較它們的樹層級(子函數的層級更深),這一步是為了讓函數和內聯函數可以直接按調用順序返回,得到正確的調用關系。如圖中黃色部分(ranges)所示

排序后的范圍數組和函數數組即可作為索引查找。

圖片

對于一個指令(上圖中pc)來說,在上文4.3.1中常規的查找過程,需要遍歷整個樹,同時全量的dwarf都被加載到內存中并一直持有。我們優化后的過程,對于一個指令,首先在ranges中,使用二分查找找到匹配的指令范圍;再根據關聯關系,得到對應的函數信息;最后根據排序先后,得到函數調用鏈。優化之后的過程中,僅依賴少量的屬性,大幅降低內存使用量;并且基于索引信息,加快查找速度。

由于有些函數會在多個文件聲明,這一步完成后可能會匹配到多個名稱一樣的函數。我們將找到的函數按所屬的 cu 分組,選擇 cu offset最小的那組函數,這個行為對齊了 llvm-addr2line 中的選擇入口 cu 的邏輯。

4.3.2.2. 符號緩存

長時間運行的服務采樣得到的函數地址有固定范圍,適合緩存,我們在符號索引前加了一道緩存后,穩定運行情況下緩存命中率達到了 99.9%,緩存后的索引變為按需查找,大幅降低了采集服務的 CPU 開銷。我們使用了可持久化的緩存保證服務重啟升級時的緩存命中率。

4.3.3. 存儲

在地址關聯后,我們得到了完整的Profiling Sample數據。我們對Profiling數據,以Pod粒度進行處理:如根據函數調用鏈統計 Sample 數、過濾占比過低的函數調用鏈等。處理后,我們將數據進行存儲,用于后續的分析。我們的存儲方案選擇的是 Clickhouse,在存儲 Profiling 的數據之外,同時會把相關的環境變量信息一起存儲,如應用名、應用版本、機房等。

此外,根據Profiling Sample,當前會一起生成單 Pod 的火焰圖,將火焰圖壓縮并保存在對象存儲中。

4.4 產品化 & 落地場景

4.4.1 實時火焰圖

提供 C++ 服務各個 Pod 、各歷史版本的近實時火焰圖,例子如下:

圖片

圖片

后續基于 Clickhouse 存儲可實現實時的火焰圖查詢,實現任意范圍的火焰圖生成和展示。

4.4.2 性能對比分析

對于接入的服務,提供當前、歷史版本之間的性能diff分析,無須人工對比火焰圖,例子如下:

圖片

在性能diff火焰圖中,展示潛在的性能退化點的調用鏈、對應的資源漲幅情況:

圖片

4.4.3.性能退化監測

當前支持對接入的服務,進行天級別的自動化性能退化巡檢并推送。

圖片

當前已經接入推薦排序服務、以及廣告業務線的C++服務。上線近一個月以來,發現多起疑似性能退化的Case,已經反饋給業務方并跟進排查中。

05總結與展望

在過去的半年時間內,我們從零開始,嘗試將eBPF技術與可觀測的實際需求結合,來解決之前的一些疑難問題,比如流量來源分析、C++服務的Profiling等。這些能力在推薦、廣告、Redis、Redkv等業務線的核心服務中得到了應用,接入服務過千,覆蓋近五萬個Node,實現日常常態化的運行。

在當前基礎上,未來我們計劃在以下方面繼續演化:

  • 流量分析:支持繪制服務拓撲,補充 C++ 等多語言拓撲與鏈路數據;
  • Profiling的應用上:支持Off-CPU、內存泄露排查等更多的事件類型;支持實時的火焰圖查詢,實現任意范圍的火焰圖生成和展示。

06作者簡介

  • 韓柏
    小紅書可觀測技術工程師,畢業于上海交通大學,從事推薦架構、基礎架構工作,在可觀測、云原生、推薦工程、中間件、性能優化等方面有較為豐富的經驗。


  • 布克
    小紅書可觀測技術工程師,畢業于南京大學,從事基礎架構可觀測相關工作,熟悉監控基礎組件、時序數據庫相關的研發。


  • 科米
    小紅書可觀測技術工程師,畢業于浙江大學,從事基礎架構可觀測相關工作,熟悉可觀測日志、指標相關工作。
責任編輯:龐桂玉 來源: 小紅書技術REDtech
相關推薦

2023-10-13 13:40:29

2024-10-23 20:09:47

2024-10-10 08:19:50

2022-05-10 08:27:15

小紅書FlinkK8s

2021-09-14 09:52:56

ToB小程序生態評估

2021-11-19 09:40:50

數據技術實踐

2021-06-23 10:00:46

eBPFKubernetesLinux

2022-09-08 10:08:31

阿里云可觀測云原生

2023-11-17 08:00:54

Tetragon執行工具

2021-12-01 00:05:03

Js應用Ebpf

2024-06-19 07:45:20

2021-05-24 15:48:38

高德打車系統可觀測性

2023-05-18 22:44:09

2023-10-09 14:15:52

可觀測性數據

2015-09-10 13:28:51

暢享網

2022-08-30 08:22:14

可觀測性監控軟件

2023-09-20 16:11:32

云原生分布式系統
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产高清在线精品 | 日韩一区二区三区视频 | 欧美激情在线观看一区二区三区 | 在线观看免费国产 | 免费日本视频 | 日韩欧美国产精品一区 | 欧美性一区二区三区 | 亚洲免费在线视频 | 欧美日本高清 | 狠狠色狠狠色综合系列 | 韩国精品一区二区三区 | 福利社午夜影院 | 日韩国产中文字幕 | 亚洲视频中文字幕 | 国产一区二区三区在线 | 四虎影院新地址 | 亚洲一区二区三区在线 | 国产精品揄拍一区二区久久国内亚洲精 | 日本精品一区二区 | 国产精品久久久久久婷婷天堂 | 午夜精品一区 | 亚洲成人国产精品 | 欧美一区二区在线免费观看 | 国产一级片精品 | 欧美日韩在线一区二区 | 中文字幕亚洲欧美 | 亚洲日韩中文字幕一区 | 久久久久久99 | 亚洲色在线视频 | 国产一区视频在线 | 免费观看黄网站 | 91精品久久久久久综合五月天 | 日韩精品一区在线 | 国产一区二区三区色淫影院 | 国产日韩欧美激情 | av大片| 中文字幕一级毛片视频 | 精品国产18久久久久久二百 | 欧美专区在线观看 | 色噜噜亚洲男人的天堂 | 亚洲欧美激情精品一区二区 |