eBPF (擴(kuò)展的伯克利數(shù)據(jù)包過(guò)濾器) 是一項(xiàng)強(qiáng)大的網(wǎng)絡(luò)和性能分析工具,被廣泛應(yīng)用在 Linux 內(nèi)核上。eBPF 使得開(kāi)發(fā)者能夠動(dòng)態(tài)地加載、更新和運(yùn)行用戶定義的代碼,而無(wú)需重啟內(nèi)核或更改內(nèi)核源代碼。這個(gè)特性使得 eBPF 能夠提供極高的靈活性和性能,使其在網(wǎng)絡(luò)和系統(tǒng)性能分析方面具有廣泛的應(yīng)用。此外,eBPF 還支持使用 USDT (用戶級(jí)靜態(tài)定義跟蹤點(diǎn)) 捕獲用戶態(tài)的應(yīng)用程序行為。
在我們的 eBPF 入門實(shí)踐教程系列的這一篇,我們將介紹如何使用 eBPF 和 USDT 來(lái)捕獲和分析 Java 的垃圾回收 (GC) 事件的耗時(shí)。
USDT 介紹
USDT 是一種在應(yīng)用程序中插入靜態(tài)跟蹤點(diǎn)的機(jī)制,它允許開(kāi)發(fā)者在程序的關(guān)鍵位置插入可用于調(diào)試和性能分析的探針。這些探針可以在運(yùn)行時(shí)被 DTrace、SystemTap 或 eBPF 等工具動(dòng)態(tài)激活,從而在不重啟應(yīng)用程序或更改程序代碼的情況下,獲取程序的內(nèi)部狀態(tài)和性能指標(biāo)。USDT 在很多開(kāi)源軟件,如 MySQL、PostgreSQL、Ruby、Python 和 Node.js 等都有廣泛的應(yīng)用。
用戶層面的追蹤機(jī)制:用戶級(jí)動(dòng)態(tài)跟蹤和 USDT
在用戶層面進(jìn)行動(dòng)態(tài)跟蹤,即用戶級(jí)動(dòng)態(tài)跟蹤(User-Level Dynamic Tracing)允許我們對(duì)任何用戶級(jí)別的代碼進(jìn)行插樁。比如,我們可以通過(guò)在 MySQL 服務(wù)器的 dispatch_command()
函數(shù)上進(jìn)行插樁,來(lái)跟蹤服務(wù)器的查詢請(qǐng)求:
# ./uprobe 'p:cmd /opt/bin/mysqld:_Z16dispatch_command19enum_server_commandP3THDPcj +0(%dx):string'
Tracing uprobe cmd (p:cmd /opt/bin/mysqld:0x2dbd40 +0(%dx):string). Ctrl-C to end.
mysqld-2855 [001] d... 19957757.590926: cmd: (0x6dbd40) arg1="show tables"
mysqld-2855 [001] d... 19957759.703497: cmd: (0x6dbd40) arg1="SELECT * FROM numbers"
[...]
這里我們使用了 uprobe
工具,它利用了 Linux 的內(nèi)置功能:ftrace(跟蹤器)和 uprobes(用戶級(jí)動(dòng)態(tài)跟蹤,需要較新的 Linux 版本,例如 4.0 左右)。其他的跟蹤器,如 perf_events 和 SystemTap,也可以實(shí)現(xiàn)此功能。
許多其他的 MySQL 函數(shù)也可以被跟蹤以獲取更多的信息。我們可以列出和計(jì)算這些函數(shù)的數(shù)量:
# ./uprobe -l /opt/bin/mysqld | more
account_hash_get_key
add_collation
add_compiled_collation
add_plugin_noargs
adjust_time_range
[...]
# ./uprobe -l /opt/bin/mysqld | wc -l
21809
這有 21,000 個(gè)函數(shù)。我們也可以跟蹤庫(kù)函數(shù),甚至是單個(gè)的指令偏移。
用戶級(jí)動(dòng)態(tài)跟蹤的能力是非常強(qiáng)大的,它可以解決無(wú)數(shù)的問(wèn)題。然而,使用它也有一些困難:需要確定需要跟蹤的代碼,處理函數(shù)參數(shù),以及應(yīng)對(duì)代碼的更改。
用戶級(jí)靜態(tài)定義跟蹤(User-level Statically Defined Tracing, USDT)則可以在某種程度上解決這些問(wèn)題。USDT 探針(或者稱為用戶級(jí) “marker”)是開(kāi)發(fā)者在代碼的關(guān)鍵位置插入的跟蹤宏,提供穩(wěn)定且已經(jīng)過(guò)文檔說(shuō)明的 API。這使得跟蹤工作變得更加簡(jiǎn)單。
使用 USDT,我們可以簡(jiǎn)單地跟蹤一個(gè)名為 mysql:query__start
的探針,而不是去跟蹤那個(gè)名為 _Z16dispatch_command19enum_server_commandP3THDPcj
的 C++ 符號(hào),也就是 dispatch_command()
函數(shù)。當(dāng)然,我們?nèi)匀豢梢栽谛枰臅r(shí)候去跟蹤 dispatch_command()
以及
其他 21,000 個(gè) mysqld 函數(shù),但只有當(dāng) USDT 探針無(wú)法解決問(wèn)題的時(shí)候我們才需要這么做。
在 Linux 中的 USDT,無(wú)論是哪種形式的靜態(tài)跟蹤點(diǎn),其實(shí)都已經(jīng)存在了幾十年。它最近由于 Sun 的 DTrace 工具的流行而再次受到關(guān)注,這使得許多常見(jiàn)的應(yīng)用程序,包括 MySQL、PostgreSQL、Node.js、Java 等都加入了 USDT。SystemTap 則開(kāi)發(fā)了一種可以消費(fèi)這些 DTrace 探針的方式。
你可能正在運(yùn)行一個(gè)已經(jīng)包含了 USDT 探針的 Linux 應(yīng)用程序,或者可能需要重新編譯(通常是 --enable-dtrace)。你可以使用 readelf
來(lái)進(jìn)行檢查,例如對(duì)于 Node.js:
# readelf -n node
[...]
Notes at offset 0x00c43058 with length 0x00000494:
Owner Data size Description
stapsdt 0x0000003c NT_STAPSDT (SystemTap probe descriptors)
Provider: node
Name: gc__start
Location: 0x0000000000bf44b4, Base: 0x0000000000f22464, Semaphore: 0x0000000001243028
Arguments: 4@%esi 4@%edx 8@%rdi
[...]
stapsdt 0x00000082 NT_STAPSDT (SystemTap probe descriptors)
Provider: node
Name: http__client__request
Location: 0x0000000000bf48ff, Base: 0x0000000000f22464, Semaphore: 0x0000000001243024
Arguments: 8@%rax 8@%rdx 8@-136(%rbp) -4@-140(%rbp) 8@-72(%rbp) 8@-80(%rbp) -4@-144(%rbp)
[...]
這就是使用 --enable-dtrace 重新編譯的 node,以及安裝了提供 “dtrace” 功能來(lái)構(gòu)建 USDT 支持的 systemtap-sdt-dev 包。這里顯示了兩個(gè)探針:node:gc__start(開(kāi)始進(jìn)行垃圾回收)和 node:http__client__request。
在這一點(diǎn)上,你可以使用 SystemTap 或者 LTTng 來(lái)跟蹤這些探針。然而,內(nèi)置的 Linux 跟蹤器,比如 ftrace 和 perf_events,目前還無(wú)法做到這一點(diǎn)(盡管 perf_events 的支持正在開(kāi)發(fā)中)。
Java GC 介紹
Java 作為一種高級(jí)編程語(yǔ)言,其自動(dòng)垃圾回收(GC)是其核心特性之一。Java GC 的目標(biāo)是自動(dòng)地回收那些不再被程序使用的內(nèi)存空間,從而減輕程序員在內(nèi)存管理方面的負(fù)擔(dān)。然而,GC 過(guò)程可能會(huì)引發(fā)應(yīng)用程序的停頓,對(duì)程序的性能和響應(yīng)時(shí)間產(chǎn)生影響。因此,對(duì) Java GC 事件進(jìn)行監(jiān)控和分析,對(duì)于理解和優(yōu)化 Java 應(yīng)用的性能是非常重要的。
在接下來(lái)的教程中,我們將演示如何使用 eBPF 和 USDT 來(lái)監(jiān)控和分析 Java GC 事件的耗時(shí),希望這些內(nèi)容對(duì)你在使用 eBPF 進(jìn)行應(yīng)用性能分析方面的工作有所幫助。
eBPF 實(shí)現(xiàn)機(jī)制
Java GC 的 eBPF 程序分為內(nèi)核態(tài)和用戶態(tài)兩部分,我們會(huì)分別介紹這兩部分的實(shí)現(xiàn)機(jī)制。
內(nèi)核態(tài)程序
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* Copyright (c) 2022 Chen Tao */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/usdt.bpf.h>
#include "javagc.h"
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 100);
__type(key, uint32_t);
__type(value, struct data_t);
} data_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__type(key, int);
__type(value, int);
} perf_map SEC(".maps");
__u32 time;
static int gc_start(struct pt_regs *ctx)
{
struct data_t data = {};
data.cpu = bpf_get_smp_processor_id();
data.pid = bpf_get_current_pid_tgid() >> 32;
data.ts = bpf_ktime_get_ns();
bpf_map_update_elem(&data_map, &data.pid, &data, 0);
return 0;
}
static int gc_end(struct pt_regs *ctx)
{
struct data_t data = {};
struct data_t *p;
__u32 val;
data.cpu = bpf_get_smp_processor_id();
data.pid = bpf_get_current_pid_tgid() >> 32;
data.ts = bpf_ktime_get_ns();
p = bpf_map_lookup_elem(&data_map, &data.pid);
if (!p)
return 0;
val = data.ts - p->ts;
if (val > time) {
data.ts = val;
bpf_perf_event_output(ctx, &perf_map, BPF_F_CURRENT_CPU, &data, sizeof(data));
}
bpf_map_delete_elem(&data_map, &data.pid);
return 0;
}
SEC("usdt")
int handle_gc_start(struct pt_regs *ctx)
{
return gc_start(ctx);
}
SEC("usdt")
int handle_gc_end(struct pt_regs *ctx)
{
return gc_end(ctx);
}
SEC("usdt")
int handle_mem_pool_gc_start(struct pt_regs *ctx)
{
return gc_start(ctx);
}
SEC("usdt")
int handle_mem_pool_gc_end(struct pt_regs *ctx)
{
return gc_end(ctx);
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
首先,我們定義了兩個(gè)映射(map):
-
data_map
:這個(gè) hashmap 存儲(chǔ)每個(gè)進(jìn)程 ID 的垃圾收集開(kāi)始時(shí)間。data_t
結(jié)構(gòu)體包含進(jìn)程 ID、CPU ID 和時(shí)間戳。 -
perf_map
:這是一個(gè) perf event array,用于將數(shù)據(jù)發(fā)送回用戶態(tài)程序。
然后,我們有四個(gè)處理函數(shù):gc_start
、gc_end
和兩個(gè) USDT 處理函數(shù) handle_mem_pool_gc_start
和 handle_mem_pool_gc_end
。這些函數(shù)都用 BPF 的 SEC("usdt")
宏注解,以便在 Java 進(jìn)程中捕獲到與垃圾收集相關(guān)的 USDT 事件。
gc_start
函數(shù)在垃圾收集開(kāi)始時(shí)被調(diào)用。它首先獲取當(dāng)前的 CPU ID、進(jìn)程 ID 和時(shí)間戳,然后將這些數(shù)據(jù)存入 data_map
。
gc_end
函數(shù)在垃圾收集結(jié)束時(shí)被調(diào)用。它執(zhí)行與 gc_start
類似的操作,但是它還從 data_map
中檢索開(kāi)始時(shí)間,并計(jì)算垃圾收集的持續(xù)時(shí)間。如果持續(xù)時(shí)間超過(guò)了設(shè)定的閾值(變量 time
),那么它將數(shù)據(jù)發(fā)送回用戶態(tài)程序。
handle_gc_start
和 handle_gc_end
是針對(duì)垃圾收集開(kāi)始和結(jié)束事件的處理函數(shù),它們分別調(diào)用了 gc_start
和 gc_end
。
handle_mem_pool_gc_start
和 handle_mem_pool_gc_end
是針對(duì)內(nèi)存池的垃圾收集開(kāi)始和結(jié)束事件的處理函數(shù),它們也分別調(diào)用了 gc_start
和 gc_end
。
最后,我們有一個(gè) LICENSE
數(shù)組,聲明了該 BPF 程序的許可證,這是加載 BPF 程序所必需的。
用戶態(tài)程序
用戶態(tài)程序的主要目標(biāo)是加載和運(yùn)行eBPF程序,以及處理來(lái)自內(nèi)核態(tài)程序的數(shù)據(jù)。它是通過(guò) libbpf 庫(kù)來(lái)完成這些操作的。這里我們省略了一些通用的加載和運(yùn)行 eBPF 程序的代碼,只展示了與 USDT 相關(guān)的部分。
第一個(gè)函數(shù) get_jvmso_path
被用來(lái)獲取運(yùn)行的Java虛擬機(jī)(JVM)的 libjvm.so
庫(kù)的路徑。首先,它打開(kāi)了 /proc/<pid>/maps
文件,該文件包含了進(jìn)程地址空間的內(nèi)存映射信息。然后,它在文件中搜索包含 libjvm.so
的行,然后復(fù)制該行的路徑到提供的參數(shù)中。
static int get_jvmso_path(char *path)
{
char mode[16], line[128], buf[64];
size_t seg_start, seg_end, seg_off;
FILE *f;
int i = 0;
sprintf(buf, "/proc/%d/maps", env.pid);
f = fopen(buf, "r");
if (!f)
return -1;
while (fscanf(f, "%zx-%zx %s %zx %*s %*d%[^\n]\n",
&seg_start, &seg_end, mode, &seg_off, line) == 5) {
i = 0;
while (isblank(line[i]))
i++;
if (strstr(line + i, "libjvm.so")) {
break;
}
}
strcpy(path, line + i);
fclose(f);
return 0;
}
接下來(lái),我們看到的是將 eBPF 程序(函數(shù) handle_gc_start
和 handle_gc_end
)附加到Java進(jìn)程的相關(guān)USDT探針上。每個(gè)程序都通過(guò)調(diào)用 bpf_program__attach_usdt
函數(shù)來(lái)實(shí)現(xiàn)這一點(diǎn),該函數(shù)的參數(shù)包括BPF程序、進(jìn)程ID、二進(jìn)制路徑以及探針的提供者和名稱。如果探針掛載成功,bpf_program__attach_usdt
將返回一個(gè)鏈接對(duì)象,該對(duì)象將存儲(chǔ)在skeleton的鏈接成員中。如果掛載失敗,程序?qū)⒋蛴″e(cuò)誤消息并進(jìn)行清理。
skel->links.handle_mem_pool_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid,
binary_path, "hotspot", "mem__pool__gc__begin", NULL);
if (!skel->links.handle_mem_pool_gc_start) {
err = errno;
fprintf(stderr, "attach usdt mem__pool__gc__begin failed: %s\n", strerror(err));
goto cleanup;
}
skel->links.handle_mem_pool_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid,
binary_path, "hotspot", "mem__pool__gc__end", NULL);
if (!skel->links.handle_mem_pool_gc_end) {
err = errno;
fprintf(stderr, "attach usdt mem__pool__gc__end failed: %s\n", strerror(err));
goto cleanup;
}
skel->links.handle_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid,
binary_path, "hotspot", "gc__begin", NULL);
if (!skel->links.handle_gc_start) {
err = errno;
fprintf(stderr, "attach usdt gc__begin failed: %s\n", strerror(err));
goto cleanup;
}
skel->links.handle_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid,
binary_path, "hotspot", "gc__end", NULL);
if (!skel->links.handle_gc_end) {
err = errno;
fprintf(stderr, "attach usdt gc__end failed: %s\n", strerror(err));
goto cleanup;
}
最后一個(gè)函數(shù) handle_event
是一個(gè)回調(diào)函數(shù),用于處理從perf event array收到的數(shù)據(jù)。這個(gè)函數(shù)會(huì)被 perf event array 觸發(fā),并在每次接收到新的事件時(shí)調(diào)用。函數(shù)首先將數(shù)據(jù)轉(zhuǎn)換為 data_t
結(jié)構(gòu)體,然后將當(dāng)前時(shí)間格式化為字符串,并打印出事件的時(shí)間戳、CPU ID、進(jìn)程 ID,以及垃圾回收的持續(xù)時(shí)間。
static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
{
struct data_t *e = (struct data_t *)data;
struct tm *tm = NULL;
char ts[16];
time_t t;
time(&t);
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
printf("%-8s %-7d %-7d %-7lld\n", ts, e->cpu, e->pid, e->ts/1000);
}
安裝依賴
構(gòu)建示例需要 clang、libelf 和 zlib。包名在不同的發(fā)行版中可能會(huì)有所不同。
在 Ubuntu/Debian 上,你需要執(zhí)行以下命令:
sudo apt install clang libelf1 libelf-dev zlib1g-dev
在 CentOS/Fedora 上,你需要執(zhí)行以下命令:
sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel
編譯運(yùn)行
在對(duì)應(yīng)的目錄中,運(yùn)行 Make 即可編譯運(yùn)行上述代碼:
$ make
$ sudo ./javagc -p 12345
Tracing javagc time... Hit Ctrl-C to end.
TIME CPU PID GC TIME
10:00:01 10% 12345 50ms
10:00:02 12% 12345 55ms
10:00:03 9% 12345 47ms
10:00:04 13% 12345 52ms
10:00:05 11% 12345 50ms
完整源代碼:
- https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/15-javagc
參考資料:
- https://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html
- https://github.com/iovisor/bcc/blob/master/libbpf-tools/javagc.c
總結(jié)
通過(guò)本篇 eBPF 入門實(shí)踐教程,我們學(xué)習(xí)了如何使用 eBPF 和 USDT 動(dòng)態(tài)跟蹤和分析 Java 的垃圾回收(GC)事件。我們了解了如何在用戶態(tài)應(yīng)用程序中設(shè)置 USDT 跟蹤點(diǎn),以及如何編寫 eBPF 程序來(lái)捕獲這些跟蹤點(diǎn)的信息,從而更深入地理解和優(yōu)化 Java GC 的行為和性能。
此外,我們也介紹了一些關(guān)于 Java GC、USDT 和 eBPF 的基礎(chǔ)知識(shí)和實(shí)踐技巧,這些知識(shí)和技巧對(duì)于想要在網(wǎng)絡(luò)和系統(tǒng)性能分析領(lǐng)域深入研究的開(kāi)發(fā)者來(lái)說(shuō)是非常有價(jià)值的。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-470387.html
如果您希望學(xué)習(xí)更多關(guān)于 eBPF 的知識(shí)和實(shí)踐,可以訪問(wèn)我們的教程代碼倉(cāng)庫(kù) https://github.com/eunomia-bpf/bpf-developer-tutorial 以獲取更多示例和完整的教程。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-470387.html
到了這里,關(guān)于eBPF 入門實(shí)踐教程十五:使用 USDT 捕獲用戶態(tài) Java GC 事件耗時(shí)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!