[大廠實踐] DoorDash 基於 eBPF 的監控實踐
eBPF 是監控雲原生應用的強大工具,本文介紹了 DoorDash 構建基於 eBPF 的監控系統的實踐。原文: BPFAgent: eBPF for Monitoring at DoorDash[1]
隨着 DoorDash 在過去幾年中經歷了快速增長,我們開始看到傳統監控方法的侷限性。度量、日誌和跟蹤提供了服務生態系統的重要信息,但這些信號幾乎完全依賴應用程序級別的檢測,不同系統可能會互相沖突。因此我們決定尋找能夠提供更完整、統一的網絡拓撲的潛在解決方案。
其中一種解決方案是基於 eBPF 進行監控,該機制允許開發人員編寫直接注入內核的程序,並能夠跟蹤內核操作。這些程序可以輕量級訪問大多數內核組件,程序運行在內核沙箱內,並且在執行之前會進行安全性驗證。DoorDash 對通過名爲 kprobes(內核動態跟蹤)和跟蹤點 (tracepoint) 的鉤子跟蹤網絡流量特別感興趣,通過這些鉤子,可以攔截和理解跨多個 Kubernetes 集羣的 TCP、UDP 連接。
通過在內核構建基礎設施級別的網絡流量監控,讓我們對 DoorDash 獨立於服務業務流的後端生態系統有了新的認識。
爲了運行這些 eBPF 探針,我們開發了一個名爲 BPFAgent 的 Golang 應用程序,將其作爲所有 Kubernetes 集羣中的守護進程運行。本文將介紹如何構建 BPFAgent,構建和維護探針的過程,以及各個 DoorDash 團隊如何使用收集到的數據。
構建 BPFAgent
我們用bcc
和iovisor/gobpf
庫開發了第一版 BPFAgent,這個初始版本幫助我們瞭解瞭如何在 Kubernetes 環境中開發和部署 eBPF 探針。
雖然可以很快確認投資開發 BPFAgent 的價值,但我們也經歷了糟糕的開發生命週期以及緩慢的啓動時間等多個痛點。使用bcc
意味着探針是在運行時編譯的,這會大大增加部署新版本的啓動時間,從而使得新版本的平滑升級變得困難,因爲部署監控需要相當長時間。此外,探針對 Kubernetes 節點的 Linux 內核版本有很強的依賴性,所有內核版本都必須在 Docker 鏡像中考慮。很多情況下,對 Kubernetes 節點底層操作系統的升級會導致 BPFagent 停止工作,直到更新到支持新的 Linux 版本爲止。
我們很高興的發現,社區已經開始通過 BPF CO-RE(一次編譯,到處運行) 來解決這些痛點。使用 CO-RE,我們從運行時的bcc
編譯,轉變爲在 BPFAgent Golang 應用程序的構建過程中使用 Clang 編譯。這一更改依賴於 Clang 支持以 BPF 類型格式 (BTF,BPF Type Format) 編譯的能力,這種能力通過利用libbpf
和內存重定位信息創建可執行的探針版本,這些版本在很大程度上獨立於內核版本。這個更改可以防止大多數操作系統和內核更新影響到 BPFAgent 應用或探針。有關 BPF 可移植性和 CO-RE 的更詳細介紹,請參閱 Andrii Nakryiko 關於該主題的博客文章 [2]。
Cilium 項目有一個特殊的cilium/ebpf
Golang 庫,可以編譯 Golang 代碼中的 eBPF 探針並與之交互。它提供了易於使用的go:generate
集成,可以通過 Clang 將 eBPF C 代碼編譯成 BTF 格式,然後將 BTF 工件封裝在易於使用的 go 包中以加載探針。
在切換到 CO-RE 和 cilium/ebpf 後,我們發現內存使用量減少了 40%,由於 oomkill 導致的容器重啓減少了 98%,每個 Kubernetes 集羣的部署時間減少了 80%。總的來說,單個 BPFAgent 實例保留的 CPU 內核和內存不到典型節點的 0.3%。
BPFAgent 內部組件
BPFAgent 應用由三個主要組件組成。如圖 1 所示,BPFAgent 首先通過 eBPF 探針檢測內核,以捕獲和生成事件。然後將這些事件發送給處理器,以根據進程和 Kubernetes 信息進行填充。最後,通過導出器將豐富的事件發送到數據存儲。
讓我們深入瞭解如何構建和維護探針。每個探針都是一個 Go 模塊,包含三個主要組件: eBPF C 代碼及其生成的工件、探針執行器和事件類型。
探針執行器遵循標準模式。在初始探針創建期間,通過生成的代碼 (下面代碼片段中的loadBpfObjects
函數) 加載 BPF 代碼,併爲事件創建管道,這些事件將被髮送給 bpfagent 的處理器和導出函數進行處理。
type Probe struct {
objs bpfObjects
link link.Link
rdr *ringbuf.Reader
events chan Event
}
func New(bufferLimit int) (*Probe, error) {
var objs bpfObjects
if err := loadBpfObjects(&objs, nil); err != nil {
return nil, err
}
events := make(chan Event, bufferLimit)
return &Probe{
objs: objs,
events: events,
}, nil
}
然後,該對象作爲 BPFagent Attach()
過程的一部分被注入內核。探針被加載、附加並鏈接到所需的 Linux 系統調用 (如skb_consume_udp
)。成功後,將創建一個新的環形緩衝區讀取器,並引用我們的 BPF 環形緩衝區。最後,啓動程序來輪詢要解析併發布到管道的新事件。
func (p *Probe) Attach() (<-chan *Event, error) {
l, err := link.Kprobe("skb_consume_udp", p.objs.KprobeSkbConsumeUdp, nil)
// ...
rdr, err := ringbuf.NewReader(p.objs.Events)
// ...
p.link = l
p.rdr = rdr
go p.run()
return p.events, nil
}
func (p *Probe) run() {
for {
record, err := p.rdr.Read()
// ...
var event Event
if err = event.Unmarshal(record.RawSample, binary.LittleEndian); err != nil {
// ...
}
select {
case p.events <- event:
continue
default:
// ...
}
}
...
}
事件本身很簡單。例如,DNS 探測是一個僅包含網絡命名空間 id (netns)、進程 id (pid) 和原始數據包數據的事件。我們通過一個解析函數,將內核中的原始字節轉換爲我們的數據結構。
type Event struct {
Netns uint64
Pid uint32
Pkt [4084]uint8
}
func (e *Event) Unmarshal(buf []byte, order binary.ByteOrder) error {
if len(buf) < 4096 {
return fmt.Errorf("expected input too small, len([]byte) = %d", len(buf))
}
e.Netns = order.Uint64(buf[0:8])
e.Pid = order.Uint32(buf[8:12])
copy(e.Pkt[:], buf[12:4096])
return nil
}
我們一開始使用編碼 / 二進制來解碼。然而通過 profiling,不出所料的發現大量 CPU 時間用於解碼。這促使我們創建一個自定義的數據解碼過程來代替基於反射的數據解碼。基準測試改進驗證了這一決定,並幫助我們保持 BPFAgent 的輕量。
pkg: github.com/doordash/bpfagent/pkg/tracing/dns
cpu: Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz
BenchmarkEventUnmarshal-12 8289015 127.0 ns/op 0 B/op 0 allocs/op
BenchmarkEventUnmarshalReflect-12 33640 35379 ns/op 8240 B/op 3 allocs/op
接下我們來討論 eBPF 探針本身。探針大多數是 kprobe,提供了跟蹤 Linux 系統調用的優化訪問。使用 kprobe,我們可以攔截特定系統調用並檢索提供的參數和執行上下文。在此之前,我們使用的是 fentry 版本的探針,但由於我們用的是基於 ARM 的 Kubernetes 節點,而當前的 Linux 內核版本不支持基於 ARM 架構優化的入口探測,所以改用 kprobe。
對於網絡監控,探針可以捕獲以下事件:
-
DNS
-
kprobe/skb_consume_udp
-
TCP
-
kprobe/tcp_connect
-
kprobe/tcp_close
-
Exit
-
tracepoint/sched/sched_process_exit
爲了捕獲 DNS 查詢和響應,由於大多數 DNS 流量都是通過 UDP 傳輸的,因此可以通過skb_consume_udp
探針攔截 UDP 數據包。
struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
struct sk_buff *skb = (struct sk_buff *)PT_REGS_PARM2(ctx);
// ...
evt->netns = BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum);
unsigned char *data = BPF_CORE_READ(skb, data);
size_t buflen = BPF_CORE_READ(skb, len);
if (buflen > MAX_PKT) {
buflen = MAX_PKT;
}
bpf_core_read(&evt->pkt, buflen, data);
如上所示,skb_consume_udp
可以訪問套接字和套接字緩衝區,然後可以使用BPF_CORE_READ
等輔助函數從結構中讀取所需數據。這些幫助程序特別重要,因爲它們支持跨多個 Linux 版本使用相同的編譯探針,並且可以處理跨內核版本內存中的任何數據重定位。
對於 TCP,我們使用兩個探針來跟蹤連接何時啓動和關閉。爲了創建連接,我們探測tcp_connect
,它同時處理 TCPv4 和 TCPv6 連接。該探針主要用於隱藏對套接字的引用,以獲取有關連接源的基本上下文信息。
struct source {
u64 ts;
u32 pid;
u64 netns;
u8 task[16];
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 1 << 16);
__type(key, u64);
__type(value, struct source);
} socks SEC(".maps");
爲了獲取 TCP 連接事件,我們等待與tcp_connect
相關聯的tcp_close
調用。我們用struct sock *
作爲鍵查詢bpf_map_lookup_elem
。這麼做的目的是因爲來自bpf_get_current_comm()
等 bpf 幫助程序的上下文信息在tcp_close
探測中並不總是準確。
struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
if (!sk) {
return 0;
}
u64 key = (u64)sk;
struct source *src;
src = bpf_map_lookup_elem(&socks, &key);
在捕獲連接關閉事件時,我們需要獲取連接發送和接收的字節數。爲此,我們根據套接字的網絡族將套接字轉換爲tcp_sock
(TCPv4) 或tcp6_sock
(TCPv6)。這些結構包含 RFC 4898[3] 中描述的擴展 TCP 統計信息,因此有可能讓我們獲取到需要的統計數據。
u16 family = BPF_CORE_READ(sk, __sk_common.skc_family);
if (family == AF_INET) {
BPF_CORE_READ_INTO(&evt->saddr_v4, sk, __sk_common.skc_rcv_saddr);
BPF_CORE_READ_INTO(&evt->daddr_v4, sk, __sk_common.skc_daddr);
struct tcp_sock *tsk = (struct tcp_sock *)(sk);
evt->sent_bytes = BPF_CORE_READ(tsk, bytes_sent);
evt->recv_bytes = BPF_CORE_READ(tsk, bytes_received);
} else {
BPF_CORE_READ_INTO(&evt->saddr_v6, sk, __sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
BPF_CORE_READ_INTO(&evt->daddr_v6, sk, __sk_common.skc_v6_daddr.in6_u.u6_addr32);
struct tcp6_sock *tsk = (struct tcp6_sock *)(sk);
evt->sent_bytes = BPF_CORE_READ(tsk, tcp.bytes_sent);
evt->recv_bytes = BPF_CORE_READ(tsk, tcp.bytes_received);
}
最後,我們用 tracepoint 探針跟蹤進程何時退出。tracepoint 由內核開發人員添加,用於 hook 內核中發生的特定事件。因爲不需要綁定到特定系統調用,因此其設計比 kprobe 更穩定。該探針的事件用於從內存緩存中取出數據。
所有探針都在 CI 流水線中基於cilium/ebpf
並用 clang 編譯。
所有原始事件都必須添加有用的識別信息。由於 BPFAgent 是部署在節點進程 ID 命名空間中的 Kubernetes 守護進程,因此可以直接從/proc/:id/cgroup
中讀取進程 cgroup。因爲節點上運行的大多數進程都是 Kubernetes pod,所以大多數 cgroup 標識符看起來像這樣:
/kubepods.slice/kubepods-pod8c1087f5_5bc3_42f9_b214_fff490864b44.slice/cri-containerd-cedaf026bf376abf6d5c4200bfe3c4591f5eb3316af3d874653b0569f5208e2b.scope.
基於約定,我們可以提取 pod 的 UID(在/kubepods-pod
和.slice
之間) 以及容器 ID(在cri-containerd-
和.scope
之間)。
有了這兩個 id,我們就可以檢查 Kubernetes pod 信息的內存緩存,找到綁定連接的 pod 和容器。每個事件都用容器、pod 和命名空間名稱進行註釋。
最後,使用 google/gopacket[4] 庫對前面提到的 DNS 事件進行解碼。通過解碼數據包,可以導出事件,其中包括 DNS 查詢類型、查詢問題和響應代碼。在此處理過程中,我們使用 DNS 數據創建 (netns, ip) 到(hostname)的內存緩存映射。此緩存用於使用與連接關聯的可能主機名進一步豐富 TCP 事件中的目標 IP。簡單的 IP 到主機名查找是不實際的,因爲單個 IP 可能由多個主機名共享。
BPFAgent 導出的數據被髮送到可觀測 Kafka 集羣,在那裏每個數據類型被分配一個 topic。然後,這些大批量的數據被儲存到 ClickHouse 集羣中。團隊可以通過 Grafana 儀表板與數據進行交互。
使用 BPFAgent 的好處
可以看到,到目前爲止,上面所介紹的數據是有幫助的,eBPF 數據在提供獨立於所部署的應用程序的見解方面確實表現出色。以下是 DoorDash 團隊如何使用 BPFAgent 數據的一些示例:
-
在我們向單一服務所有權推進的過程中,我們的存儲團隊使用這些數據來調查共享數據庫。可以根據常見的數據庫端口 (如 PostgreSQL 的 5432) 進行 TCP 連接過濾,然後根據目標主機名和 Kubernetes 命名空間進行聚合,以檢測多個命名空間使用的數據庫。這些數據可以使他們避免將不同服務的指標混淆起來,因爲指標可能有一樣的命名約定。
-
我們的流量團隊使用這些數據來檢測髮夾 (hairpin) 流量,即在從公共互聯網重新進入虛擬私有云之前退出的內部流量,這會產生額外的成本和延遲。BPF 數據使我們能夠快速找到針對面向外部主機名 (如 api.doordash.com) 的內部流量,一旦能夠消除這種流量,團隊就能自信的建立流量策略,禁止未來的髮夾流量。
-
我們的計算團隊用 DNS 數據來更好的理解 DNS 流量的峯值。雖然以前也有節點級的 DNS 流量指標,但並沒有基於特定的 DNS 問題或源 pod 分解。有了 BPF 數據,就能夠找到行爲不良的 pod,並與團隊一起優化 DNS 流量。
-
產品工程團隊使用這些數據來支持向市場分片 Kubernetes 集羣的遷移。這種遷移需要服務的所有依賴項都採用基於 Consul 的服務發現 [5]。BPF 數據是一個重要的事實來源,可以突出顯示任何意外交互,並驗證所有客戶端都已轉移到新的服務發現方法。
結論
實現 BPFAgent 使我們能夠理解網絡層的服務依賴關係,並更好的控制微服務和基礎設施。我們對新的見解感到興奮,這促使我們擴展 BPFAgent,以支持網絡流量監視之外的其他用例。首先要做的是構建探針以從共享配置卷中捕獲對文件系統的讀取,從而在所有應用程序中推動最佳實踐。
我們期待加入更多用例,並推動平臺在未來支持性能分析和按需探測。我們還希望探索新的探測類型以及 Linux 內核團隊創建的任何新鉤子,以幫助開發人員更深入瞭解他們的系統。
你好,我是俞凡,在 Motorola 做過研發,現在在 Mavenir 做技術工作,對通信、網絡、後端架構、雲原生、DevOps、CICD、區塊鏈、AI 等技術始終保持着濃厚的興趣,平時喜歡閱讀、思考,相信持續學習、終身成長,歡迎一起交流學習。爲了方便大家以後能第一時間看到文章,請朋友們關注公衆號 "DeepNoMind",並設個星標吧,如果能一鍵三連 (轉發、點贊、在看),則能給我帶來更多的支持和動力,激勵我持續寫下去,和大家共同成長進步!
參考資料
[1]
BPFAgent: eBPF for Monitoring at DoorDash: https://doordash.engineering/2023/08/15/bpfagent-ebpf-for-monitoring-at-doordash
[2]
BPF Portability and CO-RE: https://nakryiko.com/posts/bpf-portability-and-co-re
[3]
RFC 4898: TCP Extended Statistics MIB: https://www.rfc-editor.org/rfc/rfc4898.html
[4]
google/gopacket: https://github.com/google/gopacket
[5]
Consul: https://www.consul.io
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qjrqI__mpwNakGwHur0rHQ