探索可觀測(cè)的新視角—— eBPF 在小紅書(shū)的實(shí)踐
在當(dāng)前的云原生時(shí)代,隨著微服務(wù)架構(gòu)的廣泛應(yīng)用,云原生可觀測(cè)性概念被廣泛討論??捎^測(cè)技術(shù)建設(shè),將有助于跟蹤、了解和診斷生產(chǎn)環(huán)境問(wèn)題,輔助開(kāi)發(fā)和運(yùn)維人員快速發(fā)現(xiàn)、定位和解決問(wèn)題,支撐風(fēng)險(xiǎn)追溯、經(jīng)驗(yàn)沉淀、故障預(yù)警,提升系統(tǒng)可靠性。云原生和微服務(wù)技術(shù)的不斷深入應(yīng)用給可觀測(cè)提出了新的需求,在 Metrics、Logging、Tracing 等傳統(tǒng)可觀測(cè)范疇外,我們需要探索新的技術(shù)和方案。
小紅書(shū)可觀測(cè)團(tuán)隊(duì)在過(guò)去一段時(shí)間內(nèi),對(duì) eBPF 等新技術(shù)在可觀測(cè)的應(yīng)用進(jìn)行了探索,在通用流量分析、持續(xù) Profiling 等領(lǐng)域進(jìn)行落地,解決了之前碰到的一些痛點(diǎn)問(wèn)題。通過(guò) eBPF 技術(shù)的應(yīng)用,團(tuán)隊(duì)將可觀測(cè)能力從應(yīng)用程序擴(kuò)展到了內(nèi)核,實(shí)現(xiàn)了對(duì)可觀測(cè)領(lǐng)域的進(jìn)一步擴(kuò)展。
01背景
在過(guò)去一段時(shí)間里,我們?cè)谏a(chǎn)上遇到了一些實(shí)際問(wèn)題,如中臺(tái)服務(wù)被多上游服務(wù)訪問(wèn)、或者提供 OpenApi 供外部服務(wù)調(diào)用,有時(shí)候會(huì)碰到接收到的流量異常上漲,自身應(yīng)用在流量異常上漲的情況下CPU、內(nèi)存可能會(huì)跟著飆升,往往會(huì)影響應(yīng)用自身的穩(wěn)定性的情況。更麻煩的是,此時(shí)我們有時(shí)候并且不知道調(diào)用方在哪。甚至存在開(kāi)發(fā)環(huán)境訪問(wèn)線上環(huán)境、跨機(jī)房訪問(wèn)等情況。如下圖所示,可觀測(cè)的主機(jī)監(jiān)控存儲(chǔ)集群,被未知的上游服務(wù)定期拉取數(shù)據(jù),導(dǎo)致 CPU、內(nèi)存異常上漲,嚴(yán)重的影響監(jiān)控存儲(chǔ)本身的穩(wěn)定性。
想要解決類(lèi)似的問(wèn)題,需要對(duì)流量進(jìn)行實(shí)時(shí)的分析,并且做到語(yǔ)言、架構(gòu)無(wú)關(guān)。而傳統(tǒng)的可觀測(cè)領(lǐng)域,缺少解決這種實(shí)時(shí)流量分析的通用手段。
此外,業(yè)務(wù)對(duì)于 C++ 性能退化的識(shí)別是一個(gè)普遍的訴求,之前我們對(duì) C++服務(wù)的持續(xù) Profiling 和性能退化檢測(cè)中,碰到了一些阻礙,其中主要的困難在于基于傳統(tǒng)的 linux perf 的方式,使用frame pointer 方式的回溯可能會(huì)出現(xiàn)結(jié)果不準(zhǔn)確;使用 dwarf 方式的回溯會(huì)出現(xiàn)性能開(kāi)銷(xiāo)比較大、耗時(shí)很長(zhǎng)的問(wèn)題。這些導(dǎo)致常態(tài)化 Profiling 無(wú)法實(shí)現(xiàn),缺少低開(kāi)銷(xiāo)且通用的解決方案。
基于以上背景,我們注意到了近些年在各領(lǐng)域興起并得到應(yīng)用的 eBPF 技術(shù),可以在 Linux 確定的內(nèi)核函數(shù) Hook 點(diǎn)運(yùn)行,來(lái)執(zhí)行用戶(hù)設(shè)定好的邏輯,常見(jiàn)的如對(duì)網(wǎng)絡(luò)數(shù)據(jù)包的監(jiān)控、性能統(tǒng)計(jì)和安全審計(jì)等功能。我們初步判斷,eBPF 的這些特性能夠解決困擾我們的這些問(wèn)題。
同時(shí),eBPF 作為近些年來(lái)的 Linux 社區(qū)的新寵,受到了國(guó)內(nèi)外互聯(lián)網(wǎng)大廠的青睞,在多個(gè)領(lǐng)域都得到了應(yīng)用。國(guó)內(nèi)各大互聯(lián)網(wǎng)公司的基礎(chǔ)架構(gòu)部門(mén)也都在落地 eBPF,如字節(jié)、阿里、騰訊百度等等,都有著eBPF的落地經(jīng)歷?;?eBPF 的開(kāi)源項(xiàng)目也像雨后春筍一樣涌現(xiàn)出來(lái)。
所以我們嘗試把 eBPF 在可觀測(cè)所面臨的問(wèn)題場(chǎng)景中進(jìn)行落地,來(lái)解決我們遇到的一些痛點(diǎn)問(wèn)題,最終服務(wù)好業(yè)務(wù)的穩(wěn)定性。
02eBPF簡(jiǎn)介
eBPF(extended Berkeley Packet Filter),是對(duì) BPF (Berkeley Packet Filter) 技術(shù)的擴(kuò)展。通過(guò)在內(nèi)核中運(yùn)行沙盒程序,eBPF 允許程序在不修改內(nèi)核源代碼或加載內(nèi)核模塊的前提下,擴(kuò)展內(nèi)核的能力。隨著 eBPF 技術(shù)的不斷完善和加強(qiáng),eBPF 已經(jīng)不再局限于定義中的網(wǎng)絡(luò)數(shù)據(jù)包的過(guò)濾,在可觀測(cè)、安全、網(wǎng)絡(luò)等方面得到了廣泛的應(yīng)用。
傳統(tǒng)意義上的觀測(cè)性,是指在外部洞悉應(yīng)用程序運(yùn)行狀況的能力?;?eBPF 可以無(wú)需侵入到應(yīng)用程序內(nèi)部、直接向內(nèi)核添加代碼來(lái)收集數(shù)據(jù)的特點(diǎn),我們可以直接從內(nèi)核中收集、聚合自定義的數(shù)據(jù)指標(biāo)。通過(guò)這種方式,我們將可觀測(cè)性擴(kuò)展從應(yīng)用程序擴(kuò)展到了內(nèi)核,實(shí)現(xiàn)對(duì)可觀測(cè)領(lǐng)域的進(jìn)一步擴(kuò)展。
常見(jiàn)的內(nèi)核 Event 如下圖所示:
在這些 Hook 點(diǎn)上,都可以編寫(xiě)應(yīng)用程序來(lái)實(shí)現(xiàn)可觀測(cè)能力的覆蓋,同時(shí)探索更多深度觀測(cè)能力。
針對(duì)實(shí)際工作中遇到的痛點(diǎn),我們基于 eBPF 技術(shù),在流量分析和 Profiling 中進(jìn)行了探索,下文分別對(duì)這兩個(gè)方面進(jìn)行詳細(xì)的介紹。
03在流量分析的
在流量分析場(chǎng)景下,目前我們主要聚焦在 L4、L7 層:L4 層得到流量包的大小,L7 層進(jìn)一步得到 QPS、RPC Method 等信息。
整體架構(gòu)如下所示:
我們的 eBPF Agent 以 DaemonSet 方式部署,在啟動(dòng)過(guò)程中,將 eBPF 程序加載到內(nèi)核中,Hook 內(nèi)核的 Tcp 數(shù)據(jù)收發(fā)等系統(tǒng)調(diào)用。主要流程:
- Agent 通過(guò)接收下發(fā)的流量采集配置,在所在的 Node 上查找目標(biāo)進(jìn)程,在找到目標(biāo)進(jìn)程后,將目標(biāo)進(jìn)程號(hào)(Pid)傳遞到內(nèi)核中。
- 內(nèi)核態(tài)的 eBPF 程序收到Pid信息后,開(kāi)始采集流量并做輕量級(jí)的處理,并將數(shù)據(jù)發(fā)送到 eBPF Map 中。
- 用戶(hù)態(tài)的 eBPF Agent 讀取 eBPF Map 中的數(shù)據(jù),做聚合和處理,生成 Metrics 指標(biāo)。
- eBPF Collector 集中式的采集各個(gè) eBPF Agent 生成的 Metrics 指標(biāo);在采集到指標(biāo)后,根據(jù)指標(biāo)中的上下游 IP 信息來(lái)查詢(xún) Meta 服務(wù),獲取到對(duì)應(yīng)的應(yīng)用信息,并補(bǔ)充到 Metrics 指標(biāo)中;最終寫(xiě)入到 Vms 存儲(chǔ)供查詢(xún)。
下面分別從內(nèi)核態(tài)、用戶(hù)態(tài)、eBPF Collector 等幾個(gè)方面來(lái)詳細(xì)的闡述。
3.1. 內(nèi)核態(tài)
3.1.1. L4層流量
一個(gè)典型的 Client-Server 之間的收發(fā)包流程,如下:
Client-Server之間,先建立連接:Server 通過(guò) bind、listen 來(lái)監(jiān)聽(tīng)端口,Client 通過(guò) connect 來(lái)與 Server 創(chuàng)建連接;Server 在監(jiān)聽(tīng)到這個(gè)請(qǐng)求之后,會(huì)調(diào)用 accept 函數(shù)取接收請(qǐng)求,這樣就建立了連接。建立連接之后,Client 可以發(fā)出數(shù)據(jù)包,在L4層,關(guān)鍵函數(shù)是 tcp_sendmsg。
基于上面的流程,我們主要關(guān)注的 Hook 點(diǎn)如下:
其中,對(duì)于 Server 來(lái)說(shuō),我們沒(méi)有 Hook tcp_recvmsg,而是 Hook tcp_cleanup_rbuf。這主要是因?yàn)橐环矫?tcp_recvmsg 可能存在統(tǒng)計(jì)上的遺漏和重復(fù);另一方面,tcp_cleanup_rbuf 的執(zhí)行次數(shù)低于 tcp_recvmsg,可以降低消耗。
在Hook tcp_sendmsg和tcp_cleanup_rbuf中,根據(jù)struct sock對(duì)象,拿到上下游的IP、Port、數(shù)據(jù)包大小等關(guān)鍵信息,并Output到用戶(hù)態(tài)。
3.1.2. L7層流量
L4層面的流量提供了網(wǎng)絡(luò)流量大小。有時(shí)候,我們還想要知道更多的信息,如QPS、延遲、消息協(xié)議、Rpc Method、Redis 命令等等。在這種情況下,我們需要進(jìn)一步來(lái)實(shí)現(xiàn) L7 層的流量分析功能。
我們通過(guò)Hook讀寫(xiě)相關(guān)的系統(tǒng)調(diào)用,來(lái)獲取到服務(wù)之間的流量數(shù)據(jù),常見(jiàn)的讀寫(xiě)相關(guān)的系統(tǒng)調(diào)用,如下:
通過(guò)Hook這些系統(tǒng)調(diào)用,我們最終需要拿到的是原始的報(bào)文buf數(shù)據(jù)、對(duì)端地址信息(socket address),并基于 buf 數(shù)據(jù)和 socket address 處理得到 QPS、協(xié)議、RPC method 等信息。
基本流程如下:
- 通過(guò) tracepoint/probe 追蹤 socket syscall 相關(guān)的函數(shù),Hook 并根據(jù) Pid 進(jìn)行過(guò)濾,保留 Pid 收發(fā)的流量 buf 數(shù)據(jù);
- 根據(jù) buf 數(shù)據(jù),提取 socket 元信息,獲取 socket address;
- 根據(jù) buf 數(shù)據(jù),進(jìn)行相應(yīng)的協(xié)議推斷,判斷是否是我們支持的協(xié)議,不是則設(shè)置為 unknown;
- 將原始的流量 buf 數(shù)據(jù) Output 到用戶(hù)態(tài),供進(jìn)一步處理。
其中,兩個(gè)關(guān)鍵的過(guò)程分別是獲取 socket address、協(xié)議推斷。
socket address獲?。?/strong>
一個(gè)方案是Hook 建立連接的系統(tǒng)調(diào)用,如 sys_enter_connect、inet_sock_set_state 等,并解析參數(shù)中的 skaddr 的信息來(lái)拿到 IP、Port。這種方案的優(yōu)點(diǎn)是簡(jiǎn)單易實(shí)現(xiàn),但是這種方案的問(wèn)題是,對(duì)于在我們 Agent 部署之前就存在的長(zhǎng)連接來(lái)說(shuō),我們無(wú)法捕獲到相應(yīng)的事件和相應(yīng)的信息。
我們采用的方法是是通過(guò) bpf_get_current_task 來(lái)拿到 task_struct 類(lèi)型的 task,來(lái)獲取 socket 對(duì)象,進(jìn)而拿到 sockaddr:
- task_struct 中的 files 字段,類(lèi)型為 files_struct;
- 根據(jù) files 拿到 fdtable 字段,是當(dāng)前進(jìn)程的文件描述符表;
- 再?gòu)?fdtable 中,根據(jù) socket 的 fd,拿到 socket 的 file 結(jié)構(gòu);
- socket 的 file 中,有個(gè) sock 類(lèi)型的 sk 對(duì)象,就是 socket 的內(nèi)核對(duì)象指針;根據(jù) sk 對(duì)象,就可以得到 IP、Port、UDP 還是 TCP、IPV4 還是 IPV6 等各種屬性。
協(xié)議推斷:
根據(jù)上述列出來(lái)的讀寫(xiě)系統(tǒng)調(diào)用中,拿到的原始的字節(jié)流 buf 數(shù)據(jù),可以來(lái)嘗試解析對(duì)應(yīng)的應(yīng)用協(xié)議,直接遵照協(xié)議規(guī)范進(jìn)行解析。當(dāng)前常見(jiàn)的協(xié)議如 Http1、Thrift、Redis、Baidu-std 等,目前我們都已經(jīng)支持了;后續(xù)會(huì)支持如 Mysql 等更多協(xié)議的解析推斷。
此外,在協(xié)議的解析推斷過(guò)程中,另外一個(gè)問(wèn)題是業(yè)務(wù)消息的拆分和重組:在實(shí)際中業(yè)務(wù)進(jìn)程的一次數(shù)據(jù)收發(fā),在系統(tǒng)調(diào)用層面,可能會(huì)拆分成多次系統(tǒng)調(diào)用來(lái)進(jìn)行讀寫(xiě),可能會(huì)導(dǎo)致后續(xù)的 buf 都無(wú)法正確的解析出協(xié)議來(lái)。
為了解決這種問(wèn)題,當(dāng)一個(gè) socket 上的 buf 數(shù)據(jù)在協(xié)議推斷成功后,將 socket 和協(xié)議信息保存在 socket info 中,并將 socket info 進(jìn)行緩存;該 socket 上后續(xù)的 buf 數(shù)據(jù)在協(xié)議解析推斷失敗后,會(huì)默認(rèn)使用該 socket info 中的協(xié)議信息;如果后續(xù) buf 數(shù)據(jù)協(xié)議解析成功且多次不同時(shí),對(duì)協(xié)議進(jìn)行覆蓋。這樣,可以盡可能降低解析錯(cuò)誤的概率。最后,Hook close 系統(tǒng)調(diào)用,在 socket close 的時(shí)候,把 socket info 清理掉。此外,利用數(shù)據(jù)包之間的關(guān)聯(lián)來(lái)判斷協(xié)議,即 request、response 之間的協(xié)議應(yīng)該是一樣的,來(lái)進(jìn)一步降低解析錯(cuò)誤的概率。
3.1.3. 內(nèi)核適配
基于 eBPF,應(yīng)用開(kāi)發(fā)的模式主要有兩種:
- BPF 編譯器集合 (BCC Tools) 工具包提供了許多有用的資源和示例來(lái)構(gòu)建有效的內(nèi)核跟蹤和操作程序。
- BPF CO-RE (Compile Once – Run Everywhere)是與 BCC 框架不同的開(kāi)發(fā)部署模式,使用 BTF來(lái)解決編譯依賴(lài)問(wèn)題。
BCC的優(yōu)點(diǎn)是提供了很多有用的示例,同時(shí)還有多種前端語(yǔ)言(主要是用戶(hù)態(tài)用來(lái)處理加載到內(nèi)核態(tài)BPF程序的輸出和交互)來(lái)輔助進(jìn)行編程,如Python、Golang。存在的問(wèn)題是:
- 使用 Clang 修改編寫(xiě)的 BPF 程序,當(dāng)出現(xiàn)問(wèn)題時(shí),排查問(wèn)題更加困難。
- 類(lèi)似一種動(dòng)態(tài)語(yǔ)言的方式,BPF 程序是在運(yùn)行時(shí)編譯的,編譯的時(shí)候需要工具鏈和內(nèi)核文件。編譯依賴(lài)是脆弱的、容易失敗,所以總體不可控,兼容性不夠好。
- 應(yīng)用在啟動(dòng)時(shí),編譯BPF程序會(huì)占用大量的CPU和內(nèi)存資源,在大量的低規(guī)格的機(jī)器上,可能會(huì)影響業(yè)務(wù)進(jìn)程。
這些問(wèn)題,特別是兼容性問(wèn)題和性能問(wèn)題,對(duì)于我們想要在線上大規(guī)模部署的話,是很大的阻礙。
BPF CO-RE 依賴(lài)內(nèi)核特性支持 BTF,將內(nèi)核的數(shù)據(jù)結(jié)構(gòu)類(lèi)型構(gòu)建在內(nèi)核中。用戶(hù)態(tài)的程序可以導(dǎo)出 BTF 成一個(gè)單獨(dú)的大的.h 頭文件(如vmlinux.h),這個(gè)頭文件包含了所有的內(nèi)核內(nèi)部類(lèi)型,BPF 程序只要依賴(lài)這個(gè)頭文件就行,不需要安裝內(nèi)核頭文件的包了。這樣就可以減少依賴(lài),進(jìn)行提前編譯。
因此,考慮到我們需要大規(guī)模部署并且長(zhǎng)時(shí)間運(yùn)行,我們需要盡可能降低資源占用、提高性能,我們選擇了 CO-RE 方式。
使用 BTF 機(jī)制,需要內(nèi)核開(kāi)啟了 CONFIG_DEBUG_INFO_BTF選項(xiàng)(CONFIG_DEBUG_INFO_BTF=y)。在我們上線覆蓋過(guò)程中,遇到了部分機(jī)器的內(nèi)核是5.4,同時(shí)沒(méi)有開(kāi)啟 CONFIG_DEBUG_INFO_BTF 選項(xiàng)。對(duì)于這些沒(méi)有開(kāi)啟的內(nèi)核,我們生成并導(dǎo)入對(duì)應(yīng)版本的 BTF 文件;我們的 eBPF Agent在啟動(dòng)時(shí)先檢測(cè)內(nèi)核版本和 CONFIG_DEBUG_INFO_BTF 選項(xiàng);如果選項(xiàng)沒(méi)有開(kāi)啟,則根據(jù)內(nèi)核版本加載對(duì)應(yīng)的 BTF 文件。當(dāng)前我們對(duì)線上主要的5.4、5.10的多個(gè)內(nèi)核版本做了適配。
3.2. 用戶(hù)態(tài)
用戶(hù)態(tài)的主要工作是通過(guò)接收采集配置來(lái)選擇出目標(biāo) Pid,傳遞給內(nèi)核 eBPF 程序來(lái)開(kāi)啟流量分析;從內(nèi)核中讀取流量數(shù)據(jù)并處理?;镜氖疽鈭D如下:
3.2.1. 生效機(jī)制
在實(shí)際應(yīng)用中,如果采集 Node 上所有的流量數(shù)據(jù),消耗會(huì)很大;同時(shí)大量的未知流量信息會(huì)帶來(lái)很大的干擾。因此,我們需要在 Node 上選擇出我們實(shí)際關(guān)注的 Pid,同時(shí)將 Pid 信息傳遞到內(nèi)核中,在內(nèi)核流量采集分析的時(shí)候,根據(jù)Pid進(jìn)行過(guò)濾。
當(dāng)前流量分析功能是按需開(kāi)啟的。在 Node 上部署 eBPF Agent 后,通過(guò)配置中心下發(fā)配置來(lái)決定對(duì)哪些服務(wù)開(kāi)啟、開(kāi)啟的 K8S 集群,以及生效的比例等。Agent 在接收到配置后,根據(jù) Pod 過(guò)濾規(guī)則,在所屬的 Node 上查找匹配到的 Pod;在 Pod 的各個(gè) Container 中,根據(jù) Container name 查找匹配的 Container;最后根據(jù) K8S 集群信息和 Pid name,在 Container 中匹配到 Pid。
匹配到 Pid 之后,將 Pid 傳遞給內(nèi)核 eBPF 程序來(lái)開(kāi)啟采集。在需要關(guān)閉采集的時(shí)候,將 Pid 從內(nèi)核中刪除即可。
3.2.2 eBPF C 程序管理
在拿到 Pids 之后,我們將 eBPF C程序、相關(guān)的 eBPF Map 以及 Pids 加載到內(nèi)核中,這里涉及到 eBPF C程序的管理和數(shù)據(jù)交互。
為了簡(jiǎn)化 eBPF C代碼的開(kāi)發(fā)和調(diào)試流程,我們支持了配置化的對(duì) eBPF 程序的加載、卸載、數(shù)據(jù)讀取等。整體結(jié)構(gòu)如下:
編譯&加載:
eBPF 的 C 代碼,使用 Clang 和 LLVM 工具鏈來(lái)編譯 eBPF 代碼,生成可加載的字節(jié)碼文件。將字節(jié)碼文件作為 ELF 文件資源進(jìn)行讀取,并解析其中的 Maps、Program 等。在解析之后,通過(guò) BPF 系統(tǒng)調(diào)用:對(duì) Maps 進(jìn)行 BPF_MAP_CREATE 創(chuàng)建 Maps;對(duì) Program 進(jìn)行BPF_PROG_LOAD。這樣將字節(jié)碼加載到內(nèi)核中并進(jìn)行安全驗(yàn)證。
Link:
根據(jù)配置文件中配置的 probe、tracepoint 信息,通過(guò) BPF_LINK_CREATE BPF 系統(tǒng)調(diào)用,將 eBPF 程序掛載到對(duì)應(yīng)的內(nèi)核事件上,從而實(shí)現(xiàn)對(duì)這些事件的監(jiān)聽(tīng),當(dāng)內(nèi)核執(zhí)行到對(duì)應(yīng)的事件,會(huì)觸發(fā)并執(zhí)行對(duì)應(yīng)的 eBPF 程序邏輯。
數(shù)據(jù)讀?。?/strong>
Map 是 eBPF 內(nèi)核程序和用戶(hù)態(tài)程序之間交互的橋梁。在用戶(hù)態(tài)中,根據(jù)配置文件中配置的 Map,啟動(dòng) Epoll 來(lái)讀取 Map 中的數(shù)據(jù)。
3.2.3. 內(nèi)核數(shù)據(jù)接收與處理
eBPF C程序被加載進(jìn)內(nèi)核后,代理程序(eBPF Agent)便開(kāi)始通過(guò) Epoll 機(jī)制讀取 eBPF Map 中的數(shù)據(jù)。這些數(shù)據(jù)包含了業(yè)務(wù)模塊間直接交換的原始流量。
采樣流量開(kāi)關(guān):對(duì)于一些輕量級(jí)的 Proxy 服務(wù),往往單個(gè)實(shí)例的流量很大;同時(shí),單個(gè) Node 上可能部署多個(gè)實(shí)例,這樣一來(lái) Node 上部署的eBPF Agent 采集流量并做處理的壓力就很大。為了解決 eBPF Agent 流量處理壓力大的問(wèn)題,eBPF Agent 實(shí)現(xiàn)了流量采樣機(jī)制,Agent 通過(guò)配置中心獲取采樣比例配置,通過(guò) eBPF Map 將配置信息傳給內(nèi)核。eBPF Agent 也通過(guò)配置中心配置下發(fā)實(shí)現(xiàn)了更細(xì)粒度的流量開(kāi)關(guān),能精確控制 L4/L7 的進(jìn)/出不同方向的流量采集,按需開(kāi)啟,來(lái)實(shí)現(xiàn)節(jié)約資源消耗的目的。
流量數(shù)據(jù)解析:當(dāng)流量數(shù)據(jù)傳到用戶(hù)側(cè)時(shí) Agent 根據(jù) L7 協(xié)議規(guī)范進(jìn)一步解析并提供更多信息:在如網(wǎng)關(guān)場(chǎng)景下,通過(guò)精準(zhǔn)解析 HTTP 消息,可以實(shí)時(shí)獲取到請(qǐng)求的實(shí)際 IP;在 RPC 場(chǎng)景下,通過(guò)遞歸解析Thrift消息,可以識(shí)別 RPC 方法,任意 RPC 參數(shù)等信息(比如排序服務(wù)的模型信息);在 Redis 場(chǎng)景下,可以解析Redis命令。
指標(biāo)數(shù)據(jù)生成:在解析補(bǔ)全 L7 流量信息后,Agent 將消息事件進(jìn)行哈希后放入 Queue 中,保證后續(xù)構(gòu)成相同指標(biāo)的事件總是被緩存在同一個(gè)隊(duì)列中。在消費(fèi) Queue 中緩存的消息事件時(shí),消息事件流量 IP、方向、協(xié)議等信息被聚合為流量指標(biāo);同時(shí)將流量指標(biāo)根據(jù)采樣率進(jìn)行流量還原,最終生成 Prometheus 格式的 Metrics 指標(biāo)。此外,為了控制資源消耗、內(nèi)存使用和監(jiān)控指標(biāo)的過(guò)度膨脹,Agent 會(huì)在實(shí)例IP變動(dòng)后,需要及時(shí)進(jìn)行數(shù)據(jù)過(guò)期清理。
3.3. 指標(biāo)采集和處理
對(duì)于 L4、L7 層流量數(shù)據(jù)來(lái)說(shuō),我們?cè)谟脩?hù)態(tài)拿到的數(shù)據(jù)中,包含了上下游服務(wù)的 IP、Port。實(shí)際生產(chǎn)環(huán)境中,上下游服務(wù)實(shí)例非常多,并且隨著應(yīng)用發(fā)布會(huì)不斷變化,單純提供IP對(duì)開(kāi)發(fā)和運(yùn)維同學(xué)的幫助不大。因此,我們需要將 IP、Port 關(guān)聯(lián)出所屬的應(yīng)用、服務(wù),并提供更多的相關(guān)信息,如 Region、K8S 集群等信息。
我們部署 eBPF-Collector 來(lái)統(tǒng)一采集部署的eBPF Agent的指標(biāo)數(shù)據(jù),處理后進(jìn)行存儲(chǔ)。
3.3.1 元數(shù)據(jù)關(guān)聯(lián)
我們通過(guò) CMDB 查詢(xún)出 IP:Port 對(duì)應(yīng)的應(yīng)用名/區(qū)域等服務(wù)元信息。由于指標(biāo)數(shù)據(jù)量巨大,不可能為每一個(gè)數(shù)據(jù)點(diǎn)請(qǐng)求一次 CMDB 來(lái)獲取元信息,因此我們?cè)O(shè)計(jì)了元信息緩存來(lái)加速查詢(xún)。
Cache 整體架構(gòu)
我們最初將元信息緩存設(shè)計(jì)為指標(biāo)采集服務(wù)(eBPF Collector)的本地內(nèi)存緩存。但是由于相同的 IP:Port 查詢(xún)請(qǐng)求會(huì)等概率地出現(xiàn)在所有采集分片中,采集分片的本地緩存會(huì)保存幾乎全部被用到的數(shù)據(jù)。在水平擴(kuò)容分片時(shí),本地內(nèi)存緩存數(shù)目也會(huì)成倍增加,這意味著當(dāng)緩存更新時(shí),緩存對(duì) CMDB 的請(qǐng)求數(shù)目也會(huì)隨服務(wù)分片數(shù)成倍增加,這會(huì)對(duì) CMDB 服務(wù)造成巨大查詢(xún)壓力。為了解決這一問(wèn)題,我們重新設(shè)計(jì)了如下圖所示的新的 Cache Server 結(jié)構(gòu):
我們將緩存服務(wù)獨(dú)立部署為單獨(dú)的 Cache Server,與指標(biāo)采集服務(wù)隔離。這解除了指標(biāo)采集服務(wù)和元信息緩存的耦合,防止指標(biāo)采集服務(wù)水平擴(kuò)容帶來(lái)的元信息重復(fù)請(qǐng)求問(wèn)題。
Cache 內(nèi)部結(jié)構(gòu)
元信息緩存是基于 Working Set 的思路設(shè)計(jì)的,我們將查詢(xún)到的元信息存儲(chǔ)一段時(shí)間,同時(shí)使用 Singleflight 機(jī)制,合并同一時(shí)刻出現(xiàn)的相同的元信息查詢(xún)請(qǐng)求,降低對(duì) CMDB 的請(qǐng)求并發(fā)度。
為了降低查詢(xún)延遲,緩存除了根據(jù)預(yù)先確定的 TTL 刪除一段時(shí)間內(nèi)未訪問(wèn)的元信息,還會(huì)對(duì)仍在緩存中的元信息每隔若干時(shí)間進(jìn)行后臺(tái)刷新來(lái)更新數(shù)據(jù)。
由于緩存的元信息都保存在內(nèi)存中,Cache Server 服務(wù)重啟/發(fā)布后會(huì)導(dǎo)致緩存的數(shù)據(jù)丟失。這意味著每次啟動(dòng)都需要幾乎大量拉取 CMDB 元信息,我們?yōu)榫彺娣?wù)添加了緩存持久化功能,緩存服務(wù)會(huì)將緩存持久化在硬盤(pán)中,重啟后直接嘗試讀取舊緩存,防止冷啟動(dòng)問(wèn)題。
3.3.2. 查詢(xún)性能優(yōu)化
eBPF 的網(wǎng)絡(luò)流指標(biāo)量非常大,一次采樣周期內(nèi)采集到的指標(biāo)量超過(guò)1.1億;并且高度集中在L4、L7的三四個(gè)指標(biāo)中,這給指標(biāo)查詢(xún)帶來(lái)了巨大壓力,日??刹樵?xún)的時(shí)間范圍不超過(guò)一天,并且經(jīng)常查詢(xún)超時(shí)。
但是相對(duì)而言,eBPF 指標(biāo)的查詢(xún)方式比較固定,所以我們可以根據(jù)預(yù)先定義的 PromQL 查詢(xún)對(duì)指標(biāo)進(jìn)行流式預(yù)聚合,將預(yù)聚合之后的指標(biāo)寫(xiě)入存儲(chǔ)。這相當(dāng)于將指標(biāo)鏈路中采集之后鏈路(比如存儲(chǔ)/查詢(xún))的計(jì)算壓力前置,大幅降低寫(xiě)入存儲(chǔ)的指標(biāo)量,進(jìn)而減少查詢(xún)的數(shù)據(jù)量,加快查詢(xún)速度和可查詢(xún)的時(shí)間范圍。整體過(guò)程如下圖:
就具體實(shí)現(xiàn)來(lái)說(shuō),我們通過(guò)配置中心下發(fā)預(yù)聚合配置,當(dāng)配置有變更時(shí),服務(wù)會(huì)原子地更新預(yù)聚合算子(Operator)并重置預(yù)聚合狀態(tài)。
當(dāng)指標(biāo)數(shù)據(jù)到達(dá)預(yù)聚合服務(wù)時(shí),數(shù)據(jù)會(huì)被復(fù)制一份,復(fù)制后的數(shù)據(jù)會(huì)經(jīng)過(guò)預(yù)聚合 State Operator 來(lái)計(jì)算得到預(yù)聚合中間狀態(tài),并保存在內(nèi)存中;根據(jù)配置不同,每隔若干時(shí)間(比如 30s)服務(wù)會(huì)將中間狀態(tài)通過(guò) Merge Operator 合并為聚合后的數(shù)據(jù),并寫(xiě)入游數(shù)據(jù)源。
為了保證數(shù)據(jù)的完整性,預(yù)聚合服務(wù)起停時(shí)的最近聚合數(shù)據(jù)會(huì)被丟棄。對(duì)于單副本預(yù)聚合服務(wù),服務(wù)起停時(shí)指標(biāo)可能出現(xiàn)斷點(diǎn),我們使用雙副本加上數(shù)據(jù)去重來(lái)避免這個(gè)問(wèn)題。
經(jīng)過(guò)對(duì)比驗(yàn)證,我們測(cè)試發(fā)現(xiàn)通過(guò)指標(biāo)預(yù)聚合,指標(biāo)查詢(xún)速度提升能 10 倍以上;查詢(xún)時(shí)間范圍從一天延長(zhǎng)至至少一周以上。
3.4. 產(chǎn)品化&實(shí)際落地的場(chǎng)景
當(dāng)前的流量分析功能和“目標(biāo)應(yīng)用”的語(yǔ)言、框架無(wú)關(guān),接入時(shí)不需要業(yè)務(wù)方做任何修改,對(duì)業(yè)務(wù)無(wú)感知、無(wú)侵入。我們?cè)诓渴?Agent 并發(fā)布配置后,就會(huì)產(chǎn)生實(shí)時(shí)的、持續(xù)的流量數(shù)據(jù),數(shù)據(jù)保存一個(gè)月。生效、取消生效的過(guò)程快速,秒級(jí)生效。
在性能上,在當(dāng)前所有覆蓋的場(chǎng)景下,eBPF Agent 日常平均CPU使用量在0.1 Core、內(nèi)存在200MB;CPU Limit設(shè)置為0.5 Core,內(nèi)存1GB,對(duì)業(yè)務(wù)基本無(wú)影響。
當(dāng)前在小紅書(shū)的Redis、KV存儲(chǔ)、推薦、廣告等場(chǎng)景規(guī)模落地,接入服務(wù)過(guò)一千。下面介紹流量分析的使用方式和一些實(shí)際 Case。
3.4.1. 產(chǎn)品
3.4.1.1. 流量大盤(pán)
L4層協(xié)議,當(dāng)前支持展示"目標(biāo)應(yīng)用"的流量大小。
作為服務(wù)端(Server),接收到上游請(qǐng)求的流量(MB/s)、返回給上游的流量(MB/s);作為客戶(hù)端(Client),請(qǐng)求下游的流量(MB/s)、接收到下游返回的流量(MB/s)。
上圖中展示的一個(gè)排序服務(wù)的L4層詳情:作為服務(wù)端(Server),接收到上游的請(qǐng)求流量(MB/s)、返回給上游的流量(MB/s);作為客戶(hù)端(Client)請(qǐng)求下游的流量(MB/s)、接收到下游返回的流量(MB/s)。
L7層的流量分析,例子如下:
當(dāng)前支持展示"目標(biāo)應(yīng)用":作為服務(wù)端(Server),接收到上游的請(qǐng)求QPS、返回給上游的QPS;作為客戶(hù)端(Client)請(qǐng)求下游的QPS、接收到下游返回的QPS。此外,還展示對(duì)應(yīng)的應(yīng)用協(xié)議(當(dāng)前支持Thrift、Redis、Http)、服務(wù)部署的Region(上海、南京、杭州)等信息。
此外,我們還提供了OpenApi接口,來(lái)查詢(xún)服務(wù)的上下游流量指標(biāo)情況。Redis、KV存儲(chǔ)等存儲(chǔ)服務(wù)的高可用架構(gòu)規(guī)范治理過(guò)程中,通過(guò)這種方式來(lái)獲取上游服務(wù)的來(lái)源和訪問(wèn)情況。
3.4.1.2. 服務(wù)拓?fù)?/span>
基于 eBPF 的服務(wù)流量指標(biāo),我們可以構(gòu)造出服務(wù)之間拓?fù)潢P(guān)系,所有 A 服務(wù)發(fā)往 B 服務(wù)的流量都會(huì)聚合為服務(wù) A 到服務(wù) B 的一條邊,由此構(gòu)成拓?fù)鋱D。我們定期拉取一天的的流量指標(biāo),聚合出服務(wù)拓?fù)溥?,并將邊信息存?chǔ)在 Clickhouse 中。當(dāng)用戶(hù)查詢(xún)拓?fù)潢P(guān)系時(shí),服務(wù)從 Clickhouse 中取出拓?fù)溥呅畔?gòu)造拓?fù)鋱D。下圖展示了由 eBPF 流量指標(biāo)獲取的兩層拓?fù)鋱D:
3.4.2. 落地場(chǎng)景
在實(shí)際覆蓋過(guò)程中,流量分析可以輔助定位流量上漲的來(lái)源確定、偶發(fā)的流量、服務(wù)下線過(guò)程中的流量排空等問(wèn)題。
Case 1. 服務(wù)下線前,偶發(fā)流量的來(lái)源定位
問(wèn)題背景:電商的研發(fā)同學(xué)向我們咨詢(xún),他們有個(gè)服務(wù)在準(zhǔn)備下線的時(shí)候,遇到個(gè)問(wèn)題:還有偶發(fā)的、非常零星的上游流量會(huì)訪問(wèn)他們的服務(wù),訪問(wèn)的頻率在每小時(shí)十幾個(gè)請(qǐng)求。擔(dān)心貿(mào)然的下線會(huì)影響穩(wěn)定性,他們希望幫忙定位這些零星流量的來(lái)源。流量情況如下所示:
這種零星的流量,夾雜在日常的其他消息中,常規(guī)的抓包是很難定位的。我們的eBPF 流量來(lái)源分析,因?yàn)榭梢宰龅饺我鈺r(shí)刻、實(shí)時(shí)的流量采集,可以來(lái)解決這種問(wèn)題。
我們?cè)诓渴鸩㈤_(kāi)啟了 eBPF 的 100% 全采樣的流量分析。進(jìn)一步了解到業(yè)務(wù)同學(xué)關(guān)心的零星請(qǐng)求是特定的 Thrift Method,所以想要定位的話,需要在采集 Thrift 流量后,進(jìn)一步對(duì) Thrift 消息進(jìn)行解析和分析。經(jīng)過(guò)解析實(shí)際的消息并進(jìn)行 Thrift Method 聚合后,終于可以看到了小時(shí)級(jí)的偶發(fā)的流量來(lái)源,可以看到對(duì)應(yīng)的上游服務(wù) IP,如下所示:
根據(jù) IP 很快就成功的定位到了上游服務(wù),是一個(gè)很古老的前端 Node 服務(wù)。
Case2. 流量上漲的來(lái)源確定
問(wèn)題背景:Redis 的一個(gè)集群,某晚上海區(qū)異常,流量大幅上漲導(dǎo)致集群被打掛,影響內(nèi)流初排成功率。初步找到的流量來(lái)源看起來(lái)不是真正的大頭,需要排查上游流量上漲的來(lái)源。
我們通過(guò)部署 eBPF Agent 并采集分析流量,在幾分鐘內(nèi),識(shí)別出真實(shí)的上游流量來(lái)源和流量大小,輔助業(yè)務(wù)同學(xué)進(jìn)行止損。
04在持續(xù)Profiling的應(yīng)用
對(duì)于 C++服務(wù)的 Profiling 和性能退化檢測(cè)來(lái)說(shuō),我們之前碰到了一些阻礙,其中主要的困難在于基于 linux perf 實(shí)現(xiàn)的常態(tài)化 Profiling 性能開(kāi)銷(xiāo)比較大、耗時(shí)很長(zhǎng)?;?linux perf 來(lái) Profiling 的兩個(gè)主要步驟:
- perf record 按固定頻率采集進(jìn)程內(nèi)各個(gè)線程棧信息, 生成性能事件
- perf script 解析性能事件,轉(zhuǎn)換為可讀數(shù)據(jù),將棧幀地址轉(zhuǎn)換為對(duì)應(yīng)的函數(shù)名稱(chēng)與所屬文件和行號(hào)
在第一步 perf record 采集性能事件中,一般使用 -g 參數(shù)來(lái)獲取完整的調(diào)用棧,默認(rèn)使用 frame pointer。然而公司內(nèi) C++ 服務(wù)往往會(huì)開(kāi)啟編譯優(yōu)化選項(xiàng),frame pointer 不可用,導(dǎo)致 profiling 的結(jié)果很大程度上失真。為了保證覆蓋率,一般使用 -g dwarf 參數(shù),指定使用 dwarf 方式來(lái)回溯獲取調(diào)用棧。使用 dwarf 會(huì)遇到一個(gè)問(wèn)題就是中間數(shù)據(jù)量大:為了后續(xù)回溯的需要,會(huì)將每個(gè) CPU 的完整棧從內(nèi)核拷貝出來(lái);核數(shù)越多、采集時(shí)間越長(zhǎng),得到的棧數(shù)據(jù)就越大,以廣告的一個(gè)服務(wù)為例,采樣 10s 會(huì)生成將近 175MB 的數(shù)據(jù)。
第二步 perf script 解析性能事件,首先需要將第一步中的所有性能事件的棧進(jìn)行回溯,拿到完整的調(diào)用鏈棧幀地址;再將地址通過(guò) addr2line 工具轉(zhuǎn)換為函數(shù)名稱(chēng)和文件信息。這時(shí)遇到了第二個(gè)問(wèn)題,由于數(shù)據(jù)量大,整個(gè)轉(zhuǎn)換過(guò)程耗費(fèi)大量 CPU,耗時(shí)也很久。
以廣告的一個(gè)服務(wù)為例,在服務(wù)的 Node 上部署 perf Agent,在 Agen t的 CPU Limits 為0.5 Core 的情況下,對(duì)服務(wù)進(jìn)行 Profiling;采樣 10s 的數(shù)據(jù)并處理,整體耗時(shí)將近 1 小時(shí),并且全程 CPU 打滿(mǎn),如下圖所示:
這種資源消耗和耗時(shí)情況,對(duì)于需要大面積部署、常態(tài)化的持續(xù) Profiling并基于 Profiling 數(shù)據(jù)進(jìn)行分析性能退化來(lái)說(shuō),是基本不可行的。
針對(duì)這個(gè)問(wèn)題,我們基于 eBPF 來(lái)重新實(shí)現(xiàn) C++ 的 Profiling,大幅降低 C++ 服務(wù)的 Profiling 資源消耗、整體耗時(shí),來(lái)實(shí)現(xiàn)真正的持續(xù) Profiling。核心的思路是:在 Node 上部署 Profiling Agent,在內(nèi)核性能事件生成后,直接在內(nèi)核繼續(xù)完成?;厮莺途酆?,大幅降低拷貝到用戶(hù)態(tài)的棧數(shù)據(jù)量;通過(guò) Collector 服務(wù)來(lái)集中式的采集各個(gè) Profiling Agent 產(chǎn)生的棧數(shù)據(jù)并做處理。這種模式下,Agent 的計(jì)算壓力很小,可以實(shí)現(xiàn)持續(xù) Profiling;Collector 的處理邏輯對(duì)各個(gè) Agent 可以復(fù)用,整體消耗低。總體架構(gòu)如下:
簡(jiǎn)要的過(guò)程如下:
- eBPF Agent 用戶(hù)態(tài)程序根據(jù)下發(fā)的采集配置,獲取目前服務(wù)的 Pid。根據(jù) Pid,獲取對(duì)應(yīng)的內(nèi)存分布信息以及使用的可執(zhí)行文件內(nèi)容,預(yù)處理后通過(guò) eBPF Map 傳遞給內(nèi)核態(tài)供?;厮輹r(shí)查找;
- eBPF Agent 內(nèi)核態(tài)程序由 CPU Cycles 性能采樣事件觸發(fā),從當(dāng)前執(zhí)行位置回溯得到完整調(diào)用棧,聚合并保存在 eBPF Map 中;
- eBPF Agent 用戶(hù)態(tài)按固定頻率從 eBPF Map 獲取每條調(diào)用棧的命中次數(shù),轉(zhuǎn)化為 pprof 格式數(shù)據(jù);
- eBPF Collector 按固定頻率從 eBPF Agent 獲取 pprof 格式數(shù)據(jù);完成符號(hào)解析、生成火焰圖,并寫(xiě)入存儲(chǔ);
- 寫(xiě)入的數(shù)據(jù)支持實(shí)時(shí)火焰圖、性能對(duì)比分析、性能退化監(jiān)測(cè)等功能。
下面分別從內(nèi)核態(tài)、eBPF agent用戶(hù)態(tài)、eBPF Collector 分別詳細(xì)的介紹。
4.1 內(nèi)核態(tài)
相比于 linux perf 將??截惖接脩?hù)態(tài)后再做回溯,我們選擇借助 eBPF 提供的能力在內(nèi)核態(tài)直接完成回溯。這樣帶來(lái)了很多好處:
- 大幅減少內(nèi)核態(tài)到用戶(hù)態(tài)的數(shù)據(jù)拷貝。
- 在內(nèi)核態(tài)完成回溯后,重復(fù)命中采樣的調(diào)用棧可以直接分組累積,數(shù)據(jù)量不隨采集時(shí)間線型增長(zhǎng)。
下面具體介紹我們?cè)趦?nèi)核態(tài)做了哪些工作,以及如何在內(nèi)核態(tài)完成基于 dwarf 的?;厮?/strong>的。
eBPF 原生支持 linux perf 的性能事件, 我們只需要編寫(xiě)對(duì)應(yīng)的 eBPF 程序加載到對(duì)應(yīng)的 perf event 就可以按固定的采樣頻率觸發(fā) eBPF 程序回調(diào)。
eBPF 程序會(huì)讀取當(dāng)前的三個(gè)寄存器(ip: 指向下一條指令, sp: 棧頂?shù)刂? bp: 棧幀基址),這三個(gè)寄存器的值是?;厮葸^(guò)程的起點(diǎn)?;厮莸倪^(guò)程就是將這三個(gè)寄存器的值反復(fù)地恢復(fù)到當(dāng)前函數(shù)被調(diào)用前的值,直到?jīng)]有函數(shù)調(diào)用為止。
回溯完成后更新獲得的調(diào)用棧的命中次數(shù)到 eBPF Map 中,待用戶(hù)側(cè)采集使用。
eBPF 的verfier機(jī)制會(huì)限制程序復(fù)雜度和指令條數(shù),但調(diào)用棧深度可能會(huì)很長(zhǎng),無(wú)法一次完成回溯。我們限制了 eBPF 程序中的循環(huán)次數(shù),當(dāng)循環(huán)完成仍未完成回溯時(shí),我們用尾調(diào)用的方式重新調(diào)用自身繼續(xù)回溯直到完成。
一般來(lái)說(shuō),內(nèi)核函數(shù)與 jit 生成的函數(shù)會(huì)使用 framepointer 方式調(diào)用壓棧,回溯也使用 framepointer。而大部分用戶(hù)態(tài)的函數(shù)經(jīng)過(guò)編譯優(yōu)化后 framepointer 不可用,需要使用 eh_frame 段中的 cfi 指令信息來(lái)輔助回溯。
下面具體解析函數(shù)的兩種回溯方式。
4.1.1. framepointer 方式回溯
framepointer 的意思就是使用一個(gè)獨(dú)立的寄存器保存?;罚话阌脕?lái)訪問(wèn)函數(shù)參數(shù),也用來(lái)回溯函數(shù)調(diào)用, framepointer 一般就是指代 bp 寄存器。下圖是包含了 framepointer 的函數(shù)調(diào)用壓棧方式。
首先壓棧返回地址,也就是調(diào)用函數(shù)返回后繼續(xù)執(zhí)行的下一條指令
然后壓棧 bp 寄存器內(nèi)容,把 bp 寄存器更新為新的棧幀基址
回溯其實(shí)就是調(diào)用函數(shù)的反向過(guò)程,由于 bp 寄存器的內(nèi)容是?;?,而棧基址所指的地方保存了 caller 函數(shù)的 bp。只要反復(fù)將 bp 寄存器的值作為指針讀取值更新到 bp,就可以完成回溯。
我們關(guān)心的函數(shù)的 ip 指令地址,可以基于 bp 偏移得到。
如果只是使用 framepointer 方式回溯,我們只需要 bp 和 ip 的內(nèi)容。但是實(shí)際程序運(yùn)行場(chǎng)景中往往會(huì)出現(xiàn)帶 framepointer 和不帶 framepointer 函數(shù)互相調(diào)用的情況,而不帶 framepointer 的函數(shù)需要使用 eh_frame 信息回溯,依賴(lài) sp 寄存器。
所以我們也保留 sp 寄存器的值,也基于 bp 偏移得到。
4.1.2. eh_frame方式回溯
framepointer 方式占用了一個(gè)專(zhuān)用的寄存器,函數(shù)執(zhí)行過(guò)程中很少使用,而且每次需要壓棧。整體來(lái)說(shuō)帶來(lái)了額外的內(nèi)存開(kāi)銷(xiāo)。現(xiàn)代編譯器在開(kāi)啟編譯優(yōu)化的情況下不再使用 framepointer,這個(gè)時(shí)候我們的 bp 寄存器不再保存棧幀基址,而是作為通用寄存器使用,提高了內(nèi)存效率。
不使用 framepointer 的函數(shù)在調(diào)用時(shí),不再壓棧 bp 寄存器了
但是函數(shù)調(diào)試/異常處理都需要用到回溯信息,沒(méi)有 framepointer 的函數(shù),它的回溯信息會(huì)在編譯期間通過(guò)插入 cfi 指令的方式記錄,cfi 指令最終會(huì)生成可執(zhí)行 elf 文件中的 .eh_frame 段。
cfi 指令示例
每當(dāng)發(fā)生棧變量分配和回收時(shí),編譯器生成一條 cfi 指令更新如何從棧頂找到棧基址的信息
每當(dāng)寄存器壓棧時(shí),編譯器生成一條 cfi 指令更新如何從棧基址恢復(fù)寄存器內(nèi)容的信息
回溯的思路類(lèi)似 framepointer 方式,先拿到棧基址,通過(guò)棧基址偏移獲取其他關(guān)心的寄存器內(nèi)容。
cfi 指令一般記錄棧基址到棧頂?shù)木嚯x,每次回溯時(shí),我們讀取 sp 寄存器的內(nèi)容與對(duì)應(yīng)的 cfi 指令信息找到?;?。有了?;吩偻ㄟ^(guò)偏移找到 下一輪回溯使用的 bp ip sp 寄存器。
4.1.2.1. 使用回溯表簡(jiǎn)化 cfi 指令使用
由于需要在內(nèi)核側(cè) eBPF 程序中完成回溯,直接解析 cfi 指令過(guò)于復(fù)雜,我們將 cfi 指令生成 key 為指令地址的一張表,告訴回溯程序當(dāng)執(zhí)行到任意指令時(shí)如何找到?;?,如何恢復(fù)寄存器內(nèi)容,表內(nèi)容如下圖:
4.1.2.2. 回溯表結(jié)構(gòu)設(shè)計(jì)
可執(zhí)行文件大小不一,指令數(shù)差異大,生成的回溯表大小不一,但 eBPF Map 的Key、Value 都是固定大小,為了高效存儲(chǔ)回溯表,我們使用兩個(gè) Map 分別作為數(shù)據(jù)表、索引表,如下圖所示:
數(shù)據(jù)表:用來(lái)保存具體回溯信息,由若干個(gè)shard組成,每個(gè)shard有數(shù)據(jù)量上限。對(duì)某個(gè)Pid開(kāi)啟Profiling時(shí),對(duì)Pid的所有可執(zhí)行文件進(jìn)行遍歷和解析,生成回溯表后,將回溯表數(shù)據(jù)append 寫(xiě)入數(shù)據(jù)表。寫(xiě)入數(shù)據(jù)表過(guò)程是按 shard 依次寫(xiě)滿(mǎn)。
索引表:提供可執(zhí)行文件到數(shù)據(jù)表之間的索引,定位可執(zhí)行文件關(guān)聯(lián)了數(shù)據(jù)表中分段。每次數(shù)據(jù)表寫(xiě)滿(mǎn)一個(gè)shard 或當(dāng)前可執(zhí)行文件的回溯表寫(xiě)完,在索引表中記錄一條數(shù)據(jù)表分段信息。
回溯表查找時(shí),首先根據(jù) pc (指令地址),在索引表找到可執(zhí)行文件對(duì)應(yīng)的所有分段,根據(jù)包含關(guān)系確定具體分段;最后根據(jù)分段對(duì)應(yīng)的數(shù)據(jù)表信息,在數(shù)據(jù)表中二分查找。
4.2 用戶(hù)態(tài)
4.2.1. 生成回溯表
讀取 Profiling 進(jìn)程的 Mapping (內(nèi)存地址分布,/proc/$pid/maps 文件),將用到的可執(zhí)行文件生成回溯表,寫(xiě)入 eBPF Map 傳遞到內(nèi)核態(tài)。
進(jìn)程的 Mapping 不是固定的,部分情況下會(huì)發(fā)生改變,比如動(dòng)態(tài)鏈接庫(kù)加載,jit 代碼生成,這些都會(huì)在運(yùn)行時(shí)改變進(jìn)程 Mapping。
為了保證采樣數(shù)據(jù)的完整性和正確性,每次采集 Profiling 數(shù)據(jù)時(shí)我們先檢查 Mapping 可執(zhí)行的部分有沒(méi)有發(fā)生變化,如果變化就廢棄這一次采集,更新 eBPF Map 的內(nèi)容到 Mapping 的最新?tīng)顟B(tài)。
4.2.2. 獲取性能采樣數(shù)據(jù)
定時(shí)采集內(nèi)核態(tài)暴露出來(lái)的調(diào)用棧和采樣次數(shù),按 Pid 聚合,為每個(gè) Pid 生成 pprof 格式的性能采樣數(shù)據(jù), 通過(guò) http 接口暴露給采集側(cè)。
4.2.2.1. 內(nèi)核函數(shù)符號(hào)解析
我們?cè)?Agent 側(cè)完成內(nèi)核函數(shù)的符號(hào)解析(/proc/kallsyms 文件),因?yàn)椴煌?jié)點(diǎn)的內(nèi)核符號(hào)不同,內(nèi)核函數(shù)必須在本地解析。另外內(nèi)核函數(shù)的查找較為簡(jiǎn)單輕量。
用戶(hù)態(tài)函數(shù)我們不在 Agent 側(cè)解析,因?yàn)閮?nèi)聯(lián)函數(shù)的符號(hào)解析依賴(lài) dwarf, 是個(gè)比較重的查找過(guò)程,要解析 debug_info 段,占用大量?jī)?nèi)存和CPU, 對(duì)于 Agent 來(lái)說(shuō)負(fù)載過(guò)重。而且對(duì)不同節(jié)點(diǎn)部署的相同服務(wù)來(lái)說(shuō),符號(hào)解析是個(gè)重復(fù)動(dòng)作,放在采集側(cè)完成能更有效利用緩存,避免重復(fù)計(jì)算。
4.2.2.2. 關(guān)聯(lián)元數(shù)據(jù)標(biāo)簽
在發(fā)現(xiàn) Profiling 進(jìn)程的過(guò)程中,我們已經(jīng)保留了進(jìn)程的元數(shù)據(jù),包括 Pod 名稱(chēng)、鏡像版本、可用區(qū)等等,這些信息作為標(biāo)簽附加在性能采樣數(shù)據(jù)中,方便后續(xù)實(shí)現(xiàn)過(guò)濾下鉆查詢(xún)。
為何使用 pprof 格式:有豐富工具類(lèi)庫(kù)可使用;序列化壓縮效率高,所有字符串通過(guò) id 引用;opentelemetry 規(guī)范中 profiling 數(shù)據(jù)模型是基于 pprof 格式設(shè)計(jì)的, 使用 pprof 方便后續(xù)對(duì)接業(yè)界規(guī)范。
4.2.3 精簡(jiǎn)可執(zhí)行文件
精簡(jiǎn)可執(zhí)行文件,抽取包含符號(hào)信息的分段,傳遞給采集側(cè)供符號(hào)解析時(shí)使用。
符號(hào)信息有2種來(lái)源,一種是可執(zhí)行文件的 .symtab 段,另一種是 dwarf。我們抽取這些分段合成一個(gè)精簡(jiǎn)過(guò)的 debuginfo 文件,通過(guò) http 接口暴露給采集側(cè)。debuginfo 文件可執(zhí)行文件 buildid 緩存,避免相同的可執(zhí)行文件被重復(fù)抽取。
4.3 采集側(cè)
采集側(cè)主要對(duì)各個(gè)Agent的性能采樣數(shù)據(jù)進(jìn)行集中采集和處理:
基本流程:
- 服務(wù)發(fā)現(xiàn)并定時(shí)抓取所有eBPF Agent 的性能采樣數(shù)據(jù)
- 抓取性能采樣數(shù)據(jù)后,對(duì)缺失名稱(chēng)的函數(shù)地址進(jìn)行查找補(bǔ)足函數(shù)名
- 如果是第一次遇到的可執(zhí)行文件,異步下載與構(gòu)造符號(hào)索引,在索引 ready 之前始終忽略當(dāng)前采樣,直接返回;
- 如果索引可用,先在 dwarf 中查找當(dāng)前地址關(guān)聯(lián)的函數(shù)和內(nèi)聯(lián)函數(shù),以及所屬文件和行號(hào);
- 如果當(dāng)前地址不在 dwarf 的范圍里,回退到 symtab 中查找函數(shù)名稱(chēng)
- 符號(hào)關(guān)聯(lián)完成后我們就拿到了完整的生成火焰圖所需的所有數(shù)據(jù),這份數(shù)據(jù)我們生成并上傳火焰圖供性能平臺(tái)訪問(wèn),并且寫(xiě)入 ck 存儲(chǔ)支持性能對(duì)比與性能退化監(jiān)測(cè)能力。
符號(hào)信息有2種來(lái)源,一種是可執(zhí)行文件的 .symtab 段, 提供了函數(shù)地址到函數(shù)名的簡(jiǎn)單映射,但是不包含內(nèi)聯(lián)函數(shù)信息。symtab 形式的的符號(hào)查找非常簡(jiǎn)單,地址和函數(shù)名一一對(duì)應(yīng)。
另一種是查找 dwarf 信息, .debug_xxx 段包含了每個(gè)函數(shù)覆蓋的指令范圍,函數(shù)名稱(chēng),調(diào)用了哪些內(nèi)聯(lián)函數(shù),屬于哪個(gè)文件,行號(hào)等豐富信息。
下面重點(diǎn)介紹dwarf的結(jié)構(gòu)和符號(hào)查找過(guò)程。
4.3.1. dwarf 結(jié)構(gòu)&符號(hào)查找
如下圖所示,結(jié)合一個(gè)例子,我們具體介紹下dwarf的結(jié)構(gòu)和符合查找過(guò)程:
結(jié)構(gòu):
dwarf 是ELF文件的debug_info section,用來(lái)表示源碼結(jié)構(gòu)信息,整體是樹(shù)狀結(jié)構(gòu),由DIE(debug info entry) 構(gòu)成,每個(gè) DIE 有Tag 字段來(lái)區(qū)分類(lèi)型,且各自帶有不同屬性信息。
最外層的 DIE 表示代碼文件(DW_TAG_compile_unit,cu),如上圖中的server.c。文件下層的DIE是各種數(shù)據(jù)結(jié)構(gòu)、函數(shù)的聲明,其中我們主要關(guān)心兩種類(lèi)型:函數(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ù),都有屬性來(lái)代表指令范圍(如上圖中的[0x40000,0x500000])。指令范圍指的是,源代碼編譯生成的機(jī)器指令,在 .text 代碼段中的偏移范圍。函數(shù)編譯生成的機(jī)器指令分布不一定是連續(xù)的一段,可能由多段范圍構(gòu)成,查找時(shí)每段都參與匹配。內(nèi)聯(lián)函數(shù)的生成的指令是調(diào)用它的函數(shù)的一部分,它的指令范圍被它的 caller 函數(shù)覆蓋。
我們會(huì)使用這個(gè)屬性與待查找的指令地址做匹配。
查找過(guò)程:
查找函數(shù)的邏輯類(lèi)似addr2line:
- 定位文件DIE:對(duì)給定的 pc (指令地址),定位文件DIE (指令范圍包含此地址的);
- 文件DIE的子節(jié)點(diǎn)遍歷:指令范圍包含地址的原則,對(duì)給定的pc,在各個(gè)子節(jié)點(diǎn)中進(jìn)行遍歷和查找;得到所有匹配的結(jié)點(diǎn),包括內(nèi)聯(lián)函數(shù)的結(jié)點(diǎn);
- 內(nèi)聯(lián)函數(shù):函數(shù) DIE 提供了屬性可獲取函數(shù)名稱(chēng)、所屬文件與行號(hào),內(nèi)聯(lián)函數(shù)不包含這些信息,需要通過(guò) DW_AT_abstrct_origin 屬性 (如上圖中綠色序號(hào))找到原始函數(shù)聲明。
將所有函數(shù)信息按調(diào)用層級(jí)返回即完成查找,輸出函數(shù)調(diào)用鏈信息。整個(gè)過(guò)程中,文件DIE的定位和子結(jié)點(diǎn)遍歷如上圖中pc和藍(lán)色結(jié)點(diǎn)所示;內(nèi)聯(lián)函數(shù)的定位如圖中綠色地址的對(duì)應(yīng)關(guān)系所示。
4.3.2. 查找優(yōu)化
4.3.2.1. dwarf 索引
dwarf 符號(hào)查找的整個(gè)查找過(guò)程需要加載整個(gè) dwarf 結(jié)構(gòu),對(duì)于復(fù)雜項(xiàng)目來(lái)說(shuō),文件數(shù)量多且空間大。支持查找需要耗費(fèi)很多內(nèi)存,跳轉(zhuǎn)過(guò)程也較為復(fù)雜。對(duì)于執(zhí)行一次性命令的工具適合這種方法,對(duì)于持續(xù)常態(tài)化運(yùn)行的服務(wù)來(lái)說(shuō)就比較浪費(fèi)。
為了保證查找的性能、節(jié)省內(nèi)存,我們將 dwarf 的內(nèi)容先做一次讀取,取出我們關(guān)心的信息來(lái)構(gòu)造成索引,后續(xù)的查找就可以基于索引來(lái),這樣 dwarf 結(jié)構(gòu)就可以從內(nèi)存中釋放。索引構(gòu)建的過(guò)程分兩步:
- 構(gòu)建全量的函數(shù)信息集合
- 對(duì) dwarf 進(jìn)行深度優(yōu)先遍歷,取出每個(gè)遇到的函數(shù)和內(nèi)聯(lián)函數(shù)的信息,包括函數(shù)名、文件名、指令范圍等屬性,如下圖藍(lán)色部分所示;
- 遍歷完成后,對(duì)內(nèi)聯(lián)函數(shù)查找函數(shù)定義,補(bǔ)足信息
- 構(gòu)建地址范圍索引
獲取所有函數(shù)的指令范圍屬性,每段指令范圍關(guān)聯(lián)到函數(shù)數(shù)組的下標(biāo);
對(duì)范圍進(jìn)行排序:首先比較每個(gè)范圍的開(kāi)始地址,這一步是為了支持函數(shù)地址的二分查找;如果開(kāi)始地址相同(如一個(gè)函數(shù)的第一行就是執(zhí)行一個(gè)內(nèi)聯(lián)函數(shù)),比較它們的樹(shù)層級(jí)(子函數(shù)的層級(jí)更深),這一步是為了讓函數(shù)和內(nèi)聯(lián)函數(shù)可以直接按調(diào)用順序返回,得到正確的調(diào)用關(guān)系。如圖中黃色部分(ranges)所示
排序后的范圍數(shù)組和函數(shù)數(shù)組即可作為索引查找。
對(duì)于一個(gè)指令(上圖中pc)來(lái)說(shuō),在上文4.3.1中常規(guī)的查找過(guò)程,需要遍歷整個(gè)樹(shù),同時(shí)全量的dwarf都被加載到內(nèi)存中并一直持有。我們優(yōu)化后的過(guò)程,對(duì)于一個(gè)指令,首先在ranges中,使用二分查找找到匹配的指令范圍;再根據(jù)關(guān)聯(lián)關(guān)系,得到對(duì)應(yīng)的函數(shù)信息;最后根據(jù)排序先后,得到函數(shù)調(diào)用鏈。優(yōu)化之后的過(guò)程中,僅依賴(lài)少量的屬性,大幅降低內(nèi)存使用量;并且基于索引信息,加快查找速度。
由于有些函數(shù)會(huì)在多個(gè)文件聲明,這一步完成后可能會(huì)匹配到多個(gè)名稱(chēng)一樣的函數(shù)。我們將找到的函數(shù)按所屬的 cu 分組,選擇 cu offset最小的那組函數(shù),這個(gè)行為對(duì)齊了 llvm-addr2line 中的選擇入口 cu 的邏輯。
4.3.2.2. 符號(hào)緩存
長(zhǎng)時(shí)間運(yùn)行的服務(wù)采樣得到的函數(shù)地址有固定范圍,適合緩存,我們?cè)诜?hào)索引前加了一道緩存后,穩(wěn)定運(yùn)行情況下緩存命中率達(dá)到了 99.9%,緩存后的索引變?yōu)榘葱璨檎?,大幅降低了采集服?wù)的 CPU 開(kāi)銷(xiāo)。我們使用了可持久化的緩存保證服務(wù)重啟升級(jí)時(shí)的緩存命中率。
4.3.3. 存儲(chǔ)
在地址關(guān)聯(lián)后,我們得到了完整的Profiling Sample數(shù)據(jù)。我們對(duì)Profiling數(shù)據(jù),以Pod粒度進(jìn)行處理:如根據(jù)函數(shù)調(diào)用鏈統(tǒng)計(jì) Sample 數(shù)、過(guò)濾占比過(guò)低的函數(shù)調(diào)用鏈等。處理后,我們將數(shù)據(jù)進(jìn)行存儲(chǔ),用于后續(xù)的分析。我們的存儲(chǔ)方案選擇的是 Clickhouse,在存儲(chǔ) Profiling 的數(shù)據(jù)之外,同時(shí)會(huì)把相關(guān)的環(huán)境變量信息一起存儲(chǔ),如應(yīng)用名、應(yīng)用版本、機(jī)房等。
此外,根據(jù)Profiling Sample,當(dāng)前會(huì)一起生成單 Pod 的火焰圖,將火焰圖壓縮并保存在對(duì)象存儲(chǔ)中。
4.4 產(chǎn)品化 & 落地場(chǎng)景
4.4.1 實(shí)時(shí)火焰圖
提供 C++ 服務(wù)各個(gè) Pod 、各歷史版本的近實(shí)時(shí)火焰圖,例子如下:
后續(xù)基于 Clickhouse 存儲(chǔ)可實(shí)現(xiàn)實(shí)時(shí)的火焰圖查詢(xún),實(shí)現(xiàn)任意范圍的火焰圖生成和展示。
4.4.2 性能對(duì)比分析
對(duì)于接入的服務(wù),提供當(dāng)前、歷史版本之間的性能diff分析,無(wú)須人工對(duì)比火焰圖,例子如下:
在性能diff火焰圖中,展示潛在的性能退化點(diǎn)的調(diào)用鏈、對(duì)應(yīng)的資源漲幅情況:
4.4.3.性能退化監(jiān)測(cè)
當(dāng)前支持對(duì)接入的服務(wù),進(jìn)行天級(jí)別的自動(dòng)化性能退化巡檢并推送。
當(dāng)前已經(jīng)接入推薦排序服務(wù)、以及廣告業(yè)務(wù)線的C++服務(wù)。上線近一個(gè)月以來(lái),發(fā)現(xiàn)多起疑似性能退化的Case,已經(jīng)反饋給業(yè)務(wù)方并跟進(jìn)排查中。
05總結(jié)與展望
在過(guò)去的半年時(shí)間內(nèi),我們從零開(kāi)始,嘗試將eBPF技術(shù)與可觀測(cè)的實(shí)際需求結(jié)合,來(lái)解決之前的一些疑難問(wèn)題,比如流量來(lái)源分析、C++服務(wù)的Profiling等。這些能力在推薦、廣告、Redis、Redkv等業(yè)務(wù)線的核心服務(wù)中得到了應(yīng)用,接入服務(wù)過(guò)千,覆蓋近五萬(wàn)個(gè)Node,實(shí)現(xiàn)日常常態(tài)化的運(yùn)行。
在當(dāng)前基礎(chǔ)上,未來(lái)我們計(jì)劃在以下方面繼續(xù)演化:
- 流量分析:支持繪制服務(wù)拓?fù)洌a(bǔ)充 C++ 等多語(yǔ)言拓?fù)渑c鏈路數(shù)據(jù);
- Profiling的應(yīng)用上:支持Off-CPU、內(nèi)存泄露排查等更多的事件類(lèi)型;支持實(shí)時(shí)的火焰圖查詢(xún),實(shí)現(xiàn)任意范圍的火焰圖生成和展示。
06作者簡(jiǎn)介
- 韓柏
小紅書(shū)可觀測(cè)技術(shù)工程師,畢業(yè)于上海交通大學(xué),從事推薦架構(gòu)、基礎(chǔ)架構(gòu)工作,在可觀測(cè)、云原生、推薦工程、中間件、性能優(yōu)化等方面有較為豐富的經(jīng)驗(yàn)。
- 布克
小紅書(shū)可觀測(cè)技術(shù)工程師,畢業(yè)于南京大學(xué),從事基礎(chǔ)架構(gòu)可觀測(cè)相關(guān)工作,熟悉監(jiān)控基礎(chǔ)組件、時(shí)序數(shù)據(jù)庫(kù)相關(guān)的研發(fā)。
- 科米
小紅書(shū)可觀測(cè)技術(shù)工程師,畢業(yè)于浙江大學(xué),從事基礎(chǔ)架構(gòu)可觀測(cè)相關(guān)工作,熟悉可觀測(cè)日志、指標(biāo)相關(guān)工作。