【玩轉騰訊雲】ebpf 學習梳理和測試使用

  • 2020 年 3 月 26 日
  • 筆記

前言

周五下午在公司的服務網格月度討論會上,一位同事為大家分享了在服務網格中使用 ebpf 來優化提升 istio 中 sidecar 和 RS 間的通信效率。聽過之後手癢難,想測試一把 ebpf。當這位同事在這方面做的還是比較深入的,而且給內核和 istio 中提交了pr。有興趣的同學可以看看他的 github:https://github.com/ChenLingPeng 還有他的 blog

我前幾天也在測試這塊,本來是想在公司的 dev 機器上測試一把,但是因為內核還是 3.xx 的版本,無奈放棄了。如果大家想體驗功能較為完善的 ebpf,那麼至少是 4.xx 以上的版本,隨着內核版本的發展支持的功能也越來越豐富,相關函數支持大家可以在這裡看:https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md

下午事情不多,所以又想測試搞起一把。也剛好是年前買了一個台騰訊雲的主機,一直沒有怎麼使用,今天看了,可以升級操作系統到 ubuntu 18.04,18.04 的內核模式就是 4.15 了,基本可以玩 ebpf,不過安裝好之後發現可以直接升級內核到 5.3,到 5.3 就有很多函數功能了。一下面先做了一些操作流水帳,記錄整個過程。

ebpf 簡單介紹

這裡介紹也主要是從昨天分享得到,雖然之前也看過,但是畢竟沒有使用過這玩意,所以認知還是不夠深入。另外從今天下午建盛大哥組織的技術寫作分享課上學到的 learning by doing。所以這東西不做,理解始終是不夠深入的。今天就實際操刀做了一遍。理論學習千百遍,不如實操來一遍。所以搞起。

ebpf 是什麼

  1. eBPF: extended Berkeley Packet Filter, BPF 提供了強大的網絡包過濾規則,可以確定應該檢查哪些流量、忽略哪些流量等,而內核近幾年發展的 Extended BPF, eBPF 實際上將應用範圍,處理效率進行了更新。通過一個內核內置的位元組碼虛擬機,完成數據包過濾、調用棧跟蹤、耗時統計、熱點分析等等高級功能。
  2. 『內核虛擬機』,內核內置JIT,這個虛擬機除了藉助於 llvm 的技術實現了 jit 之外,最主要要的就是實現了規定指令的檢測了安全執行。不會因為應用層下發的指令而導致內核效率異常低下或者奔潰。
  3. C語言編寫,llvm+clang編譯,這個其實是和上面一條是一致的,這個 jit 的實際上就是 llvm 提供的,多年前我們在大數據多維計算引擎的實時就用過這種技術,非常靈活高效
  4. Map types: 多種類型的存儲,程序間共享,用戶態內核態共享
  5. Program Types:不同類型的 bpf 程序完成不同的事,不同的類型具有不同的功能,有點像模版的概念。
  6. helper functions: 與內核數據交互,這裡可以看到目前的:http://man7.org/linux/man-pages/man7/bpf-helpers.7.html。

這裏面編程更重要的是後面的 3 點:Map types ,Program Types,helper functions。下面先學習一下基本理論,雖然上面那麼說,但是畢竟不經過理論的實操,那就成瞎操作了。所以還是理論上先看看。

Map types

Map types 是 ebpf 中主要的數據存儲類型,目前隨着內核的發展已經有 20 多種的類型,通用型的,針對 CPU,socket,cgroup 等。很多 map 類型都有一些特殊的使用方式。BPF 程序可以通過 helper function 讀寫 map,用戶態程序也可以通過 bpf(…)系統調用讀寫 map,因此可以通過 map 來達到 BPF 程序之間,BPF 程序與用戶態程序之間的數據交互與控制。具體的類型定義可以看這個文件中定義: include/uapi/linux/bpf.h

enum bpf_map_type {  	BPF_MAP_TYPE_UNSPEC,  	BPF_MAP_TYPE_HASH,  	BPF_MAP_TYPE_ARRAY,  	BPF_MAP_TYPE_PROG_ARRAY,  	BPF_MAP_TYPE_PERF_EVENT_ARRAY,  	BPF_MAP_TYPE_PERCPU_HASH,  	BPF_MAP_TYPE_PERCPU_ARRAY,  	BPF_MAP_TYPE_STACK_TRACE,  	BPF_MAP_TYPE_CGROUP_ARRAY,  	BPF_MAP_TYPE_LRU_HASH,  	BPF_MAP_TYPE_LRU_PERCPU_HASH,  	BPF_MAP_TYPE_LPM_TRIE,  	BPF_MAP_TYPE_ARRAY_OF_MAPS,  	BPF_MAP_TYPE_HASH_OF_MAPS,  	BPF_MAP_TYPE_DEVMAP,  	BPF_MAP_TYPE_SOCKMAP,  	BPF_MAP_TYPE_CPUMAP,  	BPF_MAP_TYPE_XSKMAP,  	BPF_MAP_TYPE_SOCKHASH,  	BPF_MAP_TYPE_CGROUP_STORAGE,  	BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,  	BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,  	BPF_MAP_TYPE_QUEUE,  	BPF_MAP_TYPE_STACK,  	BPF_MAP_TYPE_SK_STORAGE,  	BPF_MAP_TYPE_DEVMAP_HASH,  	BPF_MAP_TYPE_STRUCT_OPS,  };

Program Types

每個BPF程序都屬於某個特定的程序類型,目前內核支持20+不同類型的BPF程序類型,可以大致分為網絡,跟蹤,安全等幾大類,BPF程序的輸入參數也根據類型有所不同。 具體的定義可以看這個文件中定義: include/uapi/linux/bpf.h

enum bpf_prog_type {  	BPF_PROG_TYPE_UNSPEC,  	BPF_PROG_TYPE_SOCKET_FILTER,  	BPF_PROG_TYPE_KPROBE,  	BPF_PROG_TYPE_SCHED_CLS,  	BPF_PROG_TYPE_SCHED_ACT,  	BPF_PROG_TYPE_TRACEPOINT,  	BPF_PROG_TYPE_XDP,  	BPF_PROG_TYPE_PERF_EVENT,  	BPF_PROG_TYPE_CGROUP_SKB,  	BPF_PROG_TYPE_CGROUP_SOCK,  	BPF_PROG_TYPE_LWT_IN,  	BPF_PROG_TYPE_LWT_OUT,  	BPF_PROG_TYPE_LWT_XMIT,  	BPF_PROG_TYPE_SOCK_OPS,  	BPF_PROG_TYPE_SK_SKB,  	BPF_PROG_TYPE_CGROUP_DEVICE,  	BPF_PROG_TYPE_SK_MSG,  	BPF_PROG_TYPE_RAW_TRACEPOINT,  	BPF_PROG_TYPE_CGROUP_SOCK_ADDR,  	BPF_PROG_TYPE_LWT_SEG6LOCAL,  	BPF_PROG_TYPE_LIRC_MODE2,  	BPF_PROG_TYPE_SK_REUSEPORT,  	BPF_PROG_TYPE_FLOW_DISSECTOR,  	BPF_PROG_TYPE_CGROUP_SYSCTL,  	BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE,  	BPF_PROG_TYPE_CGROUP_SOCKOPT,  	BPF_PROG_TYPE_TRACING,  	BPF_PROG_TYPE_STRUCT_OPS,  	BPF_PROG_TYPE_EXT,  };

helper functions

具體的定義可以看這個文件中定義: include/uapi/linux/bpf.h,在文件的幾乎最下面,可以檢索 FN( 這個關鍵字來搜索。不同的內核版本支持的也是不一樣的。從函數看出來主要是就是協助處理用戶空間和內核空間的交互。比如從內核獲取數據(PID,GID,時間,處理器ID等),操作內核的對象。

#define __BPF_FUNC_MAPPER(FN)		  	FN(unspec),			  	FN(map_lookup_elem),		  	FN(map_update_elem),		  	FN(map_delete_elem),		  	FN(probe_read),			  	FN(ktime_get_ns),		  	FN(trace_printk),		  	FN(get_prandom_u32),		  	FN(get_smp_processor_id),	  	FN(skb_store_bytes),		  	FN(l3_csum_replace),		  	FN(l4_csum_replace),		  	FN(tail_call),			  	FN(clone_redirect),		  	FN(get_current_pid_tgid),	  	FN(get_current_uid_gid),	  	FN(get_current_comm),		  	FN(get_cgroup_classid),		  	FN(skb_vlan_push),		  	FN(skb_vlan_pop),		  	FN(skb_get_tunnel_key),		  	FN(skb_set_tunnel_key),		  	FN(perf_event_read),		    ......

ebpf 主要有哪些功能

  1. Tracing: kprobe/uprobe/tracepoint
  2. Security: seccomp
  3. Net: bpfilter, tc, sockmap, XDP

可以從這張圖上看看不同版本的內核和其支持的功能。

ubuntu 升級內核版本到 5.3

在騰訊雲可以自助重新安裝操作系統,這個具體怎麼操作就不說了哈,如果真不會可以發郵件或者微信問我。我根據內核版本和自己的熟悉程度,選擇了 ubuntu 18.04 的鏡像。重新安裝非常快。

18.04 默認安裝的內核鏡像是 4.15,但是我們還可以根據自己的需要自己升級內核版本,我根據當前的的信息升級到了 5.3 的內核。下面這個命令可以看目前還可以支持安裝的內核版本。

apt-cache search linux-image  ....  linux-image-5.0.0-1010-oem-osp1 - Signed kernel image oem-osp1  linux-image-5.0.0-1012-oem-osp1 - Signed kernel image oem-osp1  linux-image-5.0.0-1013-gke - Signed kernel image GKE  linux-image-5.0.0-1015-oem-osp1 - Signed kernel image oem-osp1  linux-image-5.0.0-1033-oem-osp1 - Signed kernel image oem-osp1  linux-image-5.3.0-1011-gke - Signed kernel image GKE  linux-image-unsigned-5.0.0-1010-oem-osp1 - Linux kernel image for version 5.0.0 on 64 bit x86 SMP  linux-image-unsigned-5.0.0-1012-oem-osp1 - Linux kernel image for version 5.0.0 on 64 bit x86 SMP  linux-image-unsigned-5.0.0-1013-gke - Linux kernel image for version 5.0.0 on 64 bit x86 SMP  linux-image-unsigned-5.0.0-1015-oem-osp1 - Linux kernel image for version 5.0.0 on 64 bit x86 SMP  linux-image-unsigned-5.0.0-1033-oem-osp1 - Linux kernel image for version 5.0.0 on 64 bit x86 SMP  linux-image-unsigned-5.3.0-1011-gke - Linux kernel image for version 5.3.0 on 64 bit x86 SMP  linux-modules-nvidia-390-5.0.0-1033-oem-osp1 - Linux kernel nvidia modules for version 5.0.0-1033  linux-modules-nvidia-418-5.0.0-1033-oem-osp1 - Linux kernel nvidia modules for version 5.0.0-1033

目前騰訊的源中的內核版本還是比較多的,最高的是 5.3.0, 所以我就選了這個最高版本:linux-image-5.3.0-42-generic。這個是個通用版本,其中看還有 gke,aws 等等內核。

安裝過程就非常簡單了,直接執行下面的命令。

apt-get install linux-image-5.3.0-42-generic

安裝完成之後直接重啟就可以了。重啟之後可以使用 uname 命令看目前使用的內核版本。

reboot  uname -a  Linux VM-0-13-ubuntu 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

安裝 ebpf 的相關庫和工具

接下來就可以進行 ebpf 的相關庫和工具安裝了,這部分安裝也比較簡單,我直接使用了二進制安裝方式,沒有使用源碼安裝,因為目前根據版本不一樣,所需要的相關庫和工具的代碼也有所不一樣,這塊 ebpf 做的還是不夠好的。安裝過程參考這裡 https://github.com/iovisor/bcc/blob/master/INSTALL.md#ubuntu—binary

build tools install

# For Bionic (18.04 LTS)  apt-get -y install bison build-essential cmake flex git libedit-dev     libllvm6.0 llvm-6.0-dev libclang-6.0-dev python zlib1g-dev libelf-dev

Install BCC

Upstream Stable and Signed Packages

apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD  echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/iovisor.list  apt-get update  apt-get install bcc-tools libbcc-examples linux-headers-$(uname -r)  apt-get install bpfcc-tools python-bpfcc libbpfcc

開始測試使用了哈

激動不激動,興喜不興喜,一起安裝順利,接下來就要測試驗證了。程序員學習的老規矩,首先來一波 hello world 測試,再來高級的。如果 hello world 都過不了,說明有問題,更別說其它的高級測試了。

hello world 測試

這個例子來自:https://github.com/iovisor/bcc,python 寫的例子,git clone bcc 的代碼後,就可以直接使用下面這個例子。

root@VM-0-13-ubuntu:/data/ebpf/bcc# ./examples/hello_world.py       barad_agent-1567  [000] ....  2487.213681: 0: Hello, World!       barad_agent-10789 [000] ....  2487.214536: 0: Hello, World!                sh-10790 [000] ....  2487.216496: 0: Hello, World!                sh-10790 [000] ....  2487.216720: 0: Hello, World!                sh-10790 [000] ....  2487.216849: 0: Hello, World!       barad_agent-1549  [000] ....  2487.221836: 0: Hello, World!                sh-10794 [000] ....  2487.224820: 0: Hello, World!                sh-10794 [000] ....  2487.225282: 0: Hello, World!                sh-10794 [000] ....  2487.225745: 0: Hello, World!          nslookup-10795 [000] ....  2487.233478: 0: Hello, World!          nslookup-10795 [000] ....  2487.233754: 0: Hello, World!          nslookup-10795 [000] ....  2487.233921: 0: Hello, World!       barad_agent-1567  [000] ....  2488.213850: 0: Hello, World!              sshd-1332  [000] ....  2489.585280: 0: Hello, World!              sshd-10802 [000] ....  2489.590194: 0: Hello, World!       barad_agent-1567  [000] ....  2490.213807: 0: Hello, World!       barad_agent-10804 [000] ....  2490.214692: 0: Hello, World!                sh-10805 [000] ....  2490.216457: 0: Hello, World!                sh-10805 [000] ....  2490.216655: 0: Hello, World!              sshd-1332  [000] ....  2491.263759: 0: Hello, World!              sshd-1332  [000] ....  2491.432832: 0: Hello, World!              sshd-10808 [000] ....  2491.495384: 0: Hello, World!              sshd-10809 [000] ....  2491.728332: 0: Hello, World!

執行順利,看到結果,那再來看看這個例子的源代碼。非常簡單,實際代碼只有一行,目的是對 clone 系統調用進行跟蹤,如果有 clone 系統調用就打印 「Hello, World!」。

#!/usr/bin/python  # Copyright (c) PLUMgrid, Inc.  # Licensed under the Apache License, Version 2.0 (the "License")    # run in project examples directory with:  # sudo ./hello_world.py"  # see trace_fields.py for a longer example    from bcc import BPF    # This may not work for 4.17 on x64, you need replace kprobe__sys_clone with kprobe____x64_sys_clone  BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\n"); return 0; }').trace_print()

gobpf 中的例子編譯&使用

接下來測試一個稍微高級一點的,我目前主要使用 go 語言,所以偏向於使用 go 的庫來做這方面的事情,使用的是 gobpf 這個庫,也是先 git clone 下來。

git clone https://github.com/iovisor/gobpf

這裡使用的例子是其 examples 中的一個例子 bash_readline,直接拷貝出來,作為獨立項目編譯執行。

root@VM-0-13-ubuntu:/data/ebpf# cp -rf gobpf/examples/bcc/bash_readline ./  root@VM-0-13-ubuntu:/data/ebpf# cd bash_readline/  root@VM-0-13-ubuntu:/data/ebpf/bash_readline# ls  bash_readline.go

因為牆的原因,這裡要設置一下 GOPROXY,在騰訊雲的機器上也是一樣的,具體設置和編譯如下。

root@VM-0-13-ubuntu:/data/ebpf/bash_readline# export GOPROXY=https://goproxy.io  root@VM-0-13-ubuntu:/data/ebpf/bash_readline# go mod init bash_readline  go: creating new go.mod: module bash_readline  root@VM-0-13-ubuntu:/data/ebpf/bash_readline# go build bash_readline.go  go: finding module for package github.com/iovisor/gobpf/bcc  go: found github.com/iovisor/gobpf/bcc in github.com/iovisor/gobpf v0.0.0-20200311173154-8078b203833d  root@VM-0-13-ubuntu:/data/ebpf/bash_readline# ls  bash_readline  bash_readline.go  go.mod  go.sum

接下來是執行程序,這個程序主要做的就獲取 bash 命令行的輸入命令。在一個終端執行這個程序,在另外一個終端中隨便輸入命令,就會獲取執行命令的 PID 和命令。

root@VM-0-13-ubuntu:/data/ebpf/bash_readline# ./bash_readline         PID	COMMAND  as          5264	sudo        5264	ls        5264	asfasdf

總結

目前來說 ebpf 的開發門檻是越來越低了,現在有 go 的庫,python 的庫,很快的就可以開發測試。在開發體驗上是有非常大的進步。所以這裡關鍵就是要找合適你的應用場景了,目前最合適的仍然是內核調試,安全,網絡數據轉發等。

我從本文開始的時候提出的解決 istio 中 sidecar 和 RS 之間的傳輸效率提升也是可以的,基本的解決思路這裡也簡單說一下: 在 sidcar 和 RS 之間的通信是經過了本地網絡協議棧的,這部分可以利用 ebpf 來進行優化。 原本的通行方式是這樣的

我們可以利用 ebpf 的能力進行 socket 映射,使用 socketmap 和 sockethash 進行發送和接受端口映射。形成如下的數據通信方式。

這個方案在實際操作上是沒有問題的,但是我們同事得出的最終結論是

  1. 從整體上來說效率提升並不明顯,在有些情況下來要低一些,因為這塊並不是服務網格的最大瓶頸,使用量不多,所以優化效果提升有限。
  2. 但是在高並發請求的場景下還是有不少的提升的。

所以 ebpf 最大的用途目前應該還是在上面提到的幾個方面:內核調試,安全,網絡數據轉發。比如目前 facebook 發佈的 l4LB 和 DDoS 防護,Google 的用於追蹤分析的 BPFd。

參考

  1. http://www.brendangregg.com/ebpf.html
  2. http://chenlingpeng.github.io/2020/01/07/ebpf-intro/