偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

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

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

01背景

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

圖片

想要解決類似的問題,需要對流量進行實時的分析,并且做到語言、架構(gòu)無關(guān)。而傳統(tǒng)的可觀測領(lǐng)域,缺少解決這種實時流量分析的通用手段。

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

基于以上背景,我們注意到了近些年在各領(lǐng)域興起并得到應(yīng)用的 eBPF 技術(shù),可以在 Linux 確定的內(nèi)核函數(shù) Hook 點運行,來執(zhí)行用戶設(shè)定好的邏輯,常見的如對網(wǎng)絡(luò)數(shù)據(jù)包的監(jiān)控、性能統(tǒng)計和安全審計等功能。我們初步判斷,eBPF 的這些特性能夠解決困擾我們的這些問題。

同時,eBPF 作為近些年來的 Linux 社區(qū)的新寵,受到了國內(nèi)外互聯(lián)網(wǎng)大廠的青睞,在多個領(lǐng)域都得到了應(yīng)用。國內(nèi)各大互聯(lián)網(wǎng)公司的基礎(chǔ)架構(gòu)部門也都在落地 eBPF,如字節(jié)、阿里、騰訊百度等等,都有著eBPF的落地經(jīng)歷?;?eBPF 的開源項目也像雨后春筍一樣涌現(xiàn)出來。

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

02eBPF簡介

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

圖片

傳統(tǒng)意義上的觀測性,是指在外部洞悉應(yīng)用程序運行狀況的能力?;?eBPF 可以無需侵入到應(yīng)用程序內(nèi)部、直接向內(nèi)核添加代碼來收集數(shù)據(jù)的特點,我們可以直接從內(nèi)核中收集、聚合自定義的數(shù)據(jù)指標(biāo)。通過這種方式,我們將可觀測性擴展從應(yīng)用程序擴展到了內(nèi)核,實現(xiàn)對可觀測領(lǐng)域的進一步擴展。

常見的內(nèi)核 Event 如下圖所示:

圖片

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

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

03在流量分析的

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

整體架構(gòu)如下所示:


圖片

我們的 eBPF Agent 以 DaemonSet 方式部署,在啟動過程中,將 eBPF 程序加載到內(nèi)核中,Hook 內(nèi)核的 Tcp 數(shù)據(jù)收發(fā)等系統(tǒng)調(diào)用。主要流程:

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

下面分別從內(nèi)核態(tài)、用戶態(tài)、eBPF Collector 等幾個方面來詳細的闡述。

3.1. 內(nèi)核態(tài)

3.1.1. L4層流量

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

圖片

Client-Server之間,先建立連接:Server 通過 bind、listen 來監(jiān)聽端口,Client 通過 connect 來與 Server 創(chuàng)建連接;Server 在監(jiān)聽到這個請求之后,會調(diào)用 accept 函數(shù)取接收請求,這樣就建立了連接。建立連接之后,Client 可以發(fā)出數(shù)據(jù)包,在L4層,關(guān)鍵函數(shù)是 tcp_sendmsg。

基于上面的流程,我們主要關(guān)注的 Hook 點如下:

圖片

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

在Hook tcp_sendmsg和tcp_cleanup_rbuf中,根據(jù)struct sock對象,拿到上下游的IP、Port、數(shù)據(jù)包大小等關(guān)鍵信息,并Output到用戶態(tài)。

3.1.2. L7層流量

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

我們通過Hook讀寫相關(guān)的系統(tǒng)調(diào)用,來獲取到服務(wù)之間的流量數(shù)據(jù),常見的讀寫相關(guān)的系統(tǒng)調(diào)用,如下:

圖片

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

基本流程如下:

  • 通過 tracepoint/probe 追蹤 socket syscall 相關(guān)的函數(shù),Hook 并根據(jù) Pid 進行過濾,保留 Pid 收發(fā)的流量 buf 數(shù)據(jù);
  • 根據(jù) buf 數(shù)據(jù),提取 socket 元信息,獲取 socket address;
  • 根據(jù) buf 數(shù)據(jù),進行相應(yīng)的協(xié)議推斷,判斷是否是我們支持的協(xié)議,不是則設(shè)置為 unknown;
  • 將原始的流量 buf 數(shù)據(jù) Output 到用戶態(tài),供進一步處理。

其中,兩個關(guān)鍵的過程分別是獲取 socket address、協(xié)議推斷。

socket address獲取:

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

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

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

協(xié)議推斷:

根據(jù)上述列出來的讀寫系統(tǒng)調(diào)用中,拿到的原始的字節(jié)流 buf 數(shù)據(jù),可以來嘗試解析對應(yīng)的應(yīng)用協(xié)議,直接遵照協(xié)議規(guī)范進行解析。當(dāng)前常見的協(xié)議如 Http1、Thrift、Redis、Baidu-std 等,目前我們都已經(jīng)支持了;后續(xù)會支持如 Mysql 等更多協(xié)議的解析推斷。

此外,在協(xié)議的解析推斷過程中,另外一個問題是業(yè)務(wù)消息的拆分和重組:在實際中業(yè)務(wù)進程的一次數(shù)據(jù)收發(fā),在系統(tǒng)調(diào)用層面,可能會拆分成多次系統(tǒng)調(diào)用來進行讀寫,可能會導(dǎo)致后續(xù)的 buf 都無法正確的解析出協(xié)議來。

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

3.1.3. 內(nèi)核適配

基于 eBPF,應(yīng)用開發(fā)的模式主要有兩種:

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

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

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

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

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

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

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

3.2. 用戶態(tài)

用戶態(tài)的主要工作是通過接收采集配置來選擇出目標(biāo) Pid,傳遞給內(nèi)核 eBPF 程序來開啟流量分析;從內(nèi)核中讀取流量數(shù)據(jù)并處理?;镜氖疽鈭D如下:

圖片

3.2.1. 生效機制

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

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

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

3.2.2 eBPF C 程序管理

在拿到 Pids 之后,我們將 eBPF C程序、相關(guān)的 eBPF Map 以及 Pids 加載到內(nèi)核中,這里涉及到 eBPF C程序的管理和數(shù)據(jù)交互。

為了簡化 eBPF C代碼的開發(fā)和調(diào)試流程,我們支持了配置化的對 eBPF 程序的加載、卸載、數(shù)據(jù)讀取等。整體結(jié)構(gòu)如下:

圖片

編譯&加載:

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

Link:

根據(jù)配置文件中配置的 probe、tracepoint 信息,通過 BPF_LINK_CREATE BPF 系統(tǒng)調(diào)用,將 eBPF 程序掛載到對應(yīng)的內(nèi)核事件上,從而實現(xiàn)對這些事件的監(jiān)聽,當(dāng)內(nèi)核執(zhí)行到對應(yīng)的事件,會觸發(fā)并執(zhí)行對應(yīng)的 eBPF 程序邏輯。

數(shù)據(jù)讀?。?/strong>

Map 是 eBPF 內(nèi)核程序和用戶態(tài)程序之間交互的橋梁。在用戶態(tài)中,根據(jù)配置文件中配置的 Map,啟動 Epoll 來讀取 Map 中的數(shù)據(jù)。

3.2.3. 內(nèi)核數(shù)據(jù)接收與處理

圖片

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

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

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

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

3.3. 指標(biāo)采集和處理

對于 L4、L7 層流量數(shù)據(jù)來說,我們在用戶態(tài)拿到的數(shù)據(jù)中,包含了上下游服務(wù)的 IP、Port。實際生產(chǎn)環(huán)境中,上下游服務(wù)實例非常多,并且隨著應(yīng)用發(fā)布會不斷變化,單純提供IP對開發(fā)和運維同學(xué)的幫助不大。因此,我們需要將 IP、Port 關(guān)聯(lián)出所屬的應(yīng)用、服務(wù),并提供更多的相關(guān)信息,如 Region、K8S 集群等信息。

我們部署 eBPF-Collector 來統(tǒng)一采集部署的eBPF Agent的指標(biāo)數(shù)據(jù),處理后進行存儲。

3.3.1 元數(shù)據(jù)關(guān)聯(lián)

我們通過 CMDB 查詢出 IP:Port 對應(yīng)的應(yīng)用名/區(qū)域等服務(wù)元信息。由于指標(biāo)數(shù)據(jù)量巨大,不可能為每一個數(shù)據(jù)點請求一次 CMDB 來獲取元信息,因此我們設(shè)計了元信息緩存來加速查詢。

Cache 整體架構(gòu)

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

圖片

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

Cache 內(nèi)部結(jié)構(gòu)

圖片

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

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

由于緩存的元信息都保存在內(nèi)存中,Cache Server 服務(wù)重啟/發(fā)布后會導(dǎo)致緩存的數(shù)據(jù)丟失。這意味著每次啟動都需要幾乎大量拉取 CMDB 元信息,我們?yōu)榫彺娣?wù)添加了緩存持久化功能,緩存服務(wù)會將緩存持久化在硬盤中,重啟后直接嘗試讀取舊緩存,防止冷啟動問題。

3.3.2. 查詢性能優(yōu)化

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

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

圖片

就具體實現(xiàn)來說,我們通過配置中心下發(fā)預(yù)聚合配置,當(dāng)配置有變更時,服務(wù)會原子地更新預(yù)聚合算子(Operator)并重置預(yù)聚合狀態(tài)。

當(dāng)指標(biāo)數(shù)據(jù)到達預(yù)聚合服務(wù)時,數(shù)據(jù)會被復(fù)制一份,復(fù)制后的數(shù)據(jù)會經(jīng)過預(yù)聚合 State Operator 來計算得到預(yù)聚合中間狀態(tài),并保存在內(nèi)存中;根據(jù)配置不同,每隔若干時間(比如 30s)服務(wù)會將中間狀態(tài)通過 Merge Operator 合并為聚合后的數(shù)據(jù),并寫入游數(shù)據(jù)源。

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

經(jīng)過對比驗證,我們測試發(fā)現(xiàn)通過指標(biāo)預(yù)聚合,指標(biāo)查詢速度提升能 10 倍以上;查詢時間范圍從一天延長至至少一周以上。

3.4. 產(chǎn)品化&實際落地的場景

當(dāng)前的流量分析功能和“目標(biāo)應(yīng)用”的語言、框架無關(guān),接入時不需要業(yè)務(wù)方做任何修改,對業(yè)務(wù)無感知、無侵入。我們在部署 Agent 并發(fā)布配置后,就會產(chǎn)生實時的、持續(xù)的流量數(shù)據(jù),數(shù)據(jù)保存一個月。生效、取消生效的過程快速,秒級生效。

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

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

3.4.1. 產(chǎn)品

3.4.1.1. 流量大盤

L4層協(xié)議,當(dāng)前支持展示"目標(biāo)應(yīng)用"的流量大小。

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

圖片

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

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

圖片

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

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

3.4.1.2. 服務(wù)拓撲

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

圖片

3.4.2. 落地場景

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

Case 1.  服務(wù)下線前,偶發(fā)流量的來源定位

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

圖片

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

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

圖片

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

Case2.  流量上漲的來源確定

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

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

圖片


04在持續(xù)Profiling的應(yīng)用

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

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

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

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

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

圖片

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

針對這個問題,我們基于 eBPF 來重新實現(xiàn) C++ 的 Profiling,大幅降低 C++ 服務(wù)的 Profiling 資源消耗、整體耗時,來實現(xiàn)真正的持續(xù) Profiling。核心的思路是:在 Node 上部署 Profiling Agent,在內(nèi)核性能事件生成后,直接在內(nèi)核繼續(xù)完成?;厮莺途酆希蠓档涂截惖接脩魬B(tài)的棧數(shù)據(jù)量;通過 Collector 服務(wù)來集中式的采集各個 Profiling Agent 產(chǎn)生的棧數(shù)據(jù)并做處理。這種模式下,Agent 的計算壓力很小,可以實現(xiàn)持續(xù) Profiling;Collector 的處理邏輯對各個 Agent 可以復(fù)用,整體消耗低??傮w架構(gòu)如下:

圖片

簡要的過程如下:

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

下面分別從內(nèi)核態(tài)、eBPF agent用戶態(tài)、eBPF Collector 分別詳細的介紹。

4.1 內(nèi)核態(tài)

相比于 linux perf 將??截惖接脩魬B(tài)后再做回溯,我們選擇借助 eBPF 提供的能力在內(nèi)核態(tài)直接完成回溯。這樣帶來了很多好處:

  • 大幅減少內(nèi)核態(tài)到用戶態(tài)的數(shù)據(jù)拷貝。
  • 在內(nèi)核態(tài)完成回溯后,重復(fù)命中采樣的調(diào)用??梢灾苯臃纸M累積,數(shù)據(jù)量不隨采集時間線型增長。

下面具體介紹我們在內(nèi)核態(tài)做了哪些工作,以及如何在內(nèi)核態(tài)完成基于 dwarf 的棧回溯的。

圖片

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

eBPF 程序會讀取當(dāng)前的三個寄存器(ip: 指向下一條指令, sp: 棧頂?shù)刂? bp: 棧幀基址),這三個寄存器的值是?;厮葸^程的起點?;厮莸倪^程就是將這三個寄存器的值反復(fù)地恢復(fù)到當(dāng)前函數(shù)被調(diào)用前的值,直到?jīng)]有函數(shù)調(diào)用為止。

回溯完成后更新獲得的調(diào)用棧的命中次數(shù)到 eBPF Map 中,待用戶側(cè)采集使用。

eBPF 的verfier機制會限制程序復(fù)雜度和指令條數(shù),但調(diào)用棧深度可能會很長,無法一次完成回溯。我們限制了 eBPF 程序中的循環(huán)次數(shù),當(dāng)循環(huán)完成仍未完成回溯時,我們用尾調(diào)用的方式重新調(diào)用自身繼續(xù)回溯直到完成。

一般來說,內(nèi)核函數(shù)與 jit 生成的函數(shù)會使用 framepointer 方式調(diào)用壓棧,回溯也使用 framepointer。而大部分用戶態(tài)的函數(shù)經(jīng)過編譯優(yōu)化后 framepointer 不可用,需要使用 eh_frame 段中的 cfi 指令信息來輔助回溯。

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

4.1.1. framepointer 方式回溯

framepointer 的意思就是使用一個獨立的寄存器保存?;?,一般用來訪問函數(shù)參數(shù),也用來回溯函數(shù)調(diào)用, framepointer 一般就是指代 bp 寄存器。下圖是包含了 framepointer 的函數(shù)調(diào)用壓棧方式。

首先壓棧返回地址,也就是調(diào)用函數(shù)返回后繼續(xù)執(zhí)行的下一條指令

然后壓棧 bp 寄存器內(nèi)容,把 bp 寄存器更新為新的棧幀基址

圖片

回溯其實就是調(diào)用函數(shù)的反向過程,由于 bp 寄存器的內(nèi)容是?;?,而棧基址所指的地方保存了 caller 函數(shù)的 bp。只要反復(fù)將 bp 寄存器的值作為指針讀取值更新到 bp,就可以完成回溯。

我們關(guān)心的函數(shù)的 ip 指令地址,可以基于 bp 偏移得到。

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

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

4.1.2. eh_frame方式回溯

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

不使用 framepointer 的函數(shù)在調(diào)用時,不再壓棧 bp 寄存器了

圖片

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

cfi 指令示例

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

每當(dāng)寄存器壓棧時,編譯器生成一條 cfi 指令更新如何從?;坊謴?fù)寄存器內(nèi)容的信息

圖片

回溯的思路類似 framepointer 方式,先拿到?;罚ㄟ^?;菲偏@取其他關(guān)心的寄存器內(nèi)容。

cfi 指令一般記錄?;返綏m?shù)木嚯x,每次回溯時,我們讀取 sp 寄存器的內(nèi)容與對應(yīng)的 cfi 指令信息找到棧基址。有了?;吩偻ㄟ^偏移找到 下一輪回溯使用的 bp ip sp 寄存器。

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

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

圖片

4.1.2.2. 回溯表結(jié)構(gòu)設(shè)計

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

圖片

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

索引表:提供可執(zhí)行文件到數(shù)據(jù)表之間的索引,定位可執(zhí)行文件關(guān)聯(lián)了數(shù)據(jù)表中分段。每次數(shù)據(jù)表寫滿一個shard 或當(dāng)前可執(zhí)行文件的回溯表寫完,在索引表中記錄一條數(shù)據(jù)表分段信息。

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

4.2 用戶態(tài)

圖片

4.2.1. 生成回溯表

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

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

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

4.2.2. 獲取性能采樣數(shù)據(jù)

定時采集內(nèi)核態(tài)暴露出來的調(diào)用棧和采樣次數(shù),按 Pid 聚合,為每個 Pid 生成 pprof 格式的性能采樣數(shù)據(jù), 通過 http 接口暴露給采集側(cè)。

4.2.2.1. 內(nèi)核函數(shù)符號解析

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

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

4.2.2.2. 關(guān)聯(lián)元數(shù)據(jù)標(biāo)簽

在發(fā)現(xiàn) Profiling 進程的過程中,我們已經(jīng)保留了進程的元數(shù)據(jù),包括 Pod 名稱、鏡像版本、可用區(qū)等等,這些信息作為標(biāo)簽附加在性能采樣數(shù)據(jù)中,方便后續(xù)實現(xiàn)過濾下鉆查詢。

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

4.2.3 精簡可執(zhí)行文件

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

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

4.3 采集側(cè)

采集側(cè)主要對各個Agent的性能采樣數(shù)據(jù)進行集中采集和處理:

圖片

基本流程:

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

符號信息有2種來源,一種是可執(zhí)行文件的 .symtab 段, 提供了函數(shù)地址到函數(shù)名的簡單映射,但是不包含內(nèi)聯(lián)函數(shù)信息。symtab 形式的的符號查找非常簡單,地址和函數(shù)名一一對應(yīng)。

另一種是查找 dwarf 信息, .debug_xxx 段包含了每個函數(shù)覆蓋的指令范圍,函數(shù)名稱,調(diào)用了哪些內(nèi)聯(lián)函數(shù),屬于哪個文件,行號等豐富信息。

下面重點介紹dwarf的結(jié)構(gòu)和符號查找過程。

4.3.1. dwarf 結(jié)構(gòu)&符號查找

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

圖片


結(jié)構(gòu):

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

最外層的 DIE 表示代碼文件(DW_TAG_compile_unit,cu),如上圖中的server.c。文件下層的DIE是各種數(shù)據(jù)結(jié)構(gòu)、函數(shù)的聲明,其中我們主要關(guān)心兩種類型:函數(shù)(DW_TAG_subprogram) 與內(nèi)聯(lián)函數(shù)(DW_TAG_inlined_subroutine),內(nèi)聯(lián)函數(shù)處于調(diào)用它的函數(shù)下層,在上圖中都有所展示。

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

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

查找過程:

查找函數(shù)的邏輯類似addr2line:

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

將所有函數(shù)信息按調(diào)用層級返回即完成查找,輸出函數(shù)調(diào)用鏈信息。整個過程中,文件DIE的定位和子結(jié)點遍歷如上圖中pc和藍色結(jié)點所示;內(nèi)聯(lián)函數(shù)的定位如圖中綠色地址的對應(yīng)關(guān)系所示。

4.3.2. 查找優(yōu)化

4.3.2.1. dwarf 索引

dwarf 符號查找的整個查找過程需要加載整個 dwarf 結(jié)構(gòu),對于復(fù)雜項目來說,文件數(shù)量多且空間大。支持查找需要耗費很多內(nèi)存,跳轉(zhuǎn)過程也較為復(fù)雜。對于執(zhí)行一次性命令的工具適合這種方法,對于持續(xù)常態(tài)化運行的服務(wù)來說就比較浪費。

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

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

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

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

圖片

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

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

4.3.2.2. 符號緩存

長時間運行的服務(wù)采樣得到的函數(shù)地址有固定范圍,適合緩存,我們在符號索引前加了一道緩存后,穩(wěn)定運行情況下緩存命中率達到了 99.9%,緩存后的索引變?yōu)榘葱璨檎遥蠓档土瞬杉?wù)的 CPU 開銷。我們使用了可持久化的緩存保證服務(wù)重啟升級時的緩存命中率。

4.3.3. 存儲

在地址關(guān)聯(lián)后,我們得到了完整的Profiling Sample數(shù)據(jù)。我們對Profiling數(shù)據(jù),以Pod粒度進行處理:如根據(jù)函數(shù)調(diào)用鏈統(tǒng)計 Sample 數(shù)、過濾占比過低的函數(shù)調(diào)用鏈等。處理后,我們將數(shù)據(jù)進行存儲,用于后續(xù)的分析。我們的存儲方案選擇的是 Clickhouse,在存儲 Profiling 的數(shù)據(jù)之外,同時會把相關(guān)的環(huán)境變量信息一起存儲,如應(yīng)用名、應(yīng)用版本、機房等。

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

4.4 產(chǎn)品化 & 落地場景

4.4.1 實時火焰圖

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

圖片

圖片

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

4.4.2 性能對比分析

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

圖片

在性能diff火焰圖中,展示潛在的性能退化點的調(diào)用鏈、對應(yīng)的資源漲幅情況:

圖片

4.4.3.性能退化監(jiān)測

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

圖片

當(dāng)前已經(jīng)接入推薦排序服務(wù)、以及廣告業(yè)務(wù)線的C++服務(wù)。上線近一個月以來,發(fā)現(xiàn)多起疑似性能退化的Case,已經(jīng)反饋給業(yè)務(wù)方并跟進排查中。

05總結(jié)與展望

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

在當(dāng)前基礎(chǔ)上,未來我們計劃在以下方面繼續(xù)演化:

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

06作者簡介

  • 韓柏
    小紅書可觀測技術(shù)工程師,畢業(yè)于上海交通大學(xué),從事推薦架構(gòu)、基礎(chǔ)架構(gòu)工作,在可觀測、云原生、推薦工程、中間件、性能優(yōu)化等方面有較為豐富的經(jīng)驗。


  • 布克
    小紅書可觀測技術(shù)工程師,畢業(yè)于南京大學(xué),從事基礎(chǔ)架構(gòu)可觀測相關(guān)工作,熟悉監(jiān)控基礎(chǔ)組件、時序數(shù)據(jù)庫相關(guān)的研發(fā)。


  • 科米
    小紅書可觀測技術(shù)工程師,畢業(yè)于浙江大學(xué),從事基礎(chǔ)架構(gòu)可觀測相關(guān)工作,熟悉可觀測日志、指標(biāo)相關(guān)工作。
責(zé)任編輯:龐桂玉 來源: 小紅書技術(shù)REDtech
相關(guān)推薦

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小程序生態(tài)評估

2021-11-19 09:40:50

數(shù)據(jù)技術(shù)實踐

2021-06-23 10:00:46

eBPFKubernetesLinux

2022-09-08 10:08:31

阿里云可觀測云原生

2023-11-17 08:00:54

Tetragon執(zhí)行工具

2021-12-01 00:05:03

Js應(yīng)用Ebpf

2024-06-19 07:45:20

2023-05-18 22:44:09

2021-05-24 15:48:38

高德打車系統(tǒng)可觀測性

2023-10-09 14:15:52

可觀測性數(shù)據(jù)

2015-09-10 13:28:51

暢享網(wǎng)

2022-08-30 08:22:14

可觀測性監(jiān)控軟件

2023-09-20 16:11:32

云原生分布式系統(tǒng)
點贊
收藏

51CTO技術(shù)棧公眾號