浅用eBPF优化与案例介绍

2023-12-05 18:13:25 浏览数 (1)

eBPF(Extended Berkeley Packet Filter)是一种基于内核的虚拟机技术,可以在Linux内核运行时动态地加载和执行代码。eBPF在系统的网络、安全、性能等方面都有广泛的应用,其中最为重要的应用之一就是优化系统的性能。

eBPF可用与系统调用跟踪

系统调用是操作系统提供给用户程序的一种接口,可以访问底层的硬件和系统资源。eBPF可以用来跟踪系统调用,统计系统调用的调用次数、调用耗时等信息,并通过这些信息进行系统性能的优化。具体的优化过程如下:

代码语言:javascript复制
通过eBPF程序跟踪系统调用的调用次数和调用耗时;
将跟踪的信息发送到用户空间;
根据跟踪的信息进行性能分析,找出性能瓶颈;
优化性能瓶颈,提高系统的性能。

优化效果:通过eBPF跟踪系统调用,可以找出系统的性能瓶颈,并对性能瓶颈进行优化,提高系统的性能。

前面两个文章我们介绍了EBPF的底层原理与应用,这次我们使用EBPF实际写一个脚本来实践下基于eBPF的系统调用跟踪。

需求:

在实际的运维场景中,常常需要监控指定进程的 TCP 连接数量,并输出进程的内存、CPU、IO、打开文件和监听端口等信息,然后根据这些信息去判断应用与系统的运行状态与troubleshoot

先直接给出代码:

代码语言:javascript复制
from bcc import BPF
import os
import sys
import time
import psutil

# eBPF程序的代码,用BPF编写
program = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <bcc/proto.h>

struct key_t {
    u32 pid;
};
BPF_HASH(stats, struct key_t);

int probe(struct pt_regs *ctx, struct sock *sk) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    FILTER
    u64 *val, zero = 0;
    struct key_t key = {.pid = pid};
    val = stats.lookup_or_init(&key, &zero);
    (*val)  ;
    return 0;
}
"""

# 过滤器,用于过滤不需要的进程
filter = """
#define FILTER                                                           
    pid_t pid = bpf_get_current_pid_tgid() >> 32;                         
    if (FILTER_PID && pid != FILTER_PID)                                  
        return 0;                                                         
    if (FILTER_COMM && FILTER_COMM_STRCMP != 0)                           
        return 0;
"""

# 获取传入的参数
if len(sys.argv) < 2:
    print("Usage: python3 %s <pid>" % sys.argv[0])
    exit(1)
pid = int(sys.argv[1])

# 创建eBPF程序
bpf = BPF(text=program.replace('FILTER', '').replace('FILTER_PID', str(pid)).replace('FILTER_COMM', '1').replace('FILTER_COMM_STRCMP', 'strcmp(bpf_get_current_comm(), "'   os.path.basename(sys.argv[0])   '")'))

# 将函数probe挂载到kprobe/tcp_v4_connect上
bpf.attach_kprobe(event="tcp_v4_connect", fn_name="probe")

# 获取当前进程的句柄
p = psutil.Process(os.getpid())

# 定义输出函数
def print_stats():
    stats = bpf["stats"]
    for k, v in stats.items():
        pid = k.pid
        if pid == 0:  # 跳过init进程
            continue
        try:
            proc = psutil.Process(pid)
        except psutil.NoSuchProcess:
            continue
        if proc.parent() is None:  # 跳过孤儿进程
            continue
        if proc.status() == psutil.STATUS_ZOMBIE:  # 跳过僵尸进程
            continue
        print("pid: %d, connection count: %d, memory usage: %.2f MB, cpu usage: %.2f%%, io read/write: (%.2f MB/s, %.2f MB/s), open files: %d, listening ports: %s" % (
            pid,
            v.value,
            proc.memory_info().rss / 1024 / 1024,
            proc.cpu_percent(),
            proc.io_counters().read_bytes / 1024 / 1024,
            proc.io_counters().write_bytes / 1024 / 1024,
            len(proc.open_files()),
            [conn.laddr.port for conn in proc.connections() if conn.status == psutil.CONN_LISTEN]
        ))
    stats.clear()

# 执行输出函数
while True:
    try:
        print_stats()
        time.sleep(1)
    except KeyboardInterrupt:
        exit()

运行结果

代码语言:javascript复制
pid: 1885193, connection count: 1, memory usage: 51.49 MB, cpu usage: 0.00%, io read/write: (871332.24 MB/s, 19085.70 MB/s), open files: 1, listening ports: []
pid: 1302, connection count: 1, memory usage: 30.05 MB, cpu usage: 0.00%, io read/write: (796263.84 MB/s, 8186.78 MB/s), open files: 12, listening ports: []
pid: 1885193, connection count: 1, memory usage: 51.49 MB, cpu usage: 0.00%, io read/write: (871332.24 MB/s, 19085.70 MB/s), open files: 1, listening ports: []
pid: 1302, connection count: 1, memory usage: 30.05 MB, cpu usage: 0.00%, io read/write: (796263.84 MB/s, 8186.78 MB/s), open files: 12, listening ports: []
pid: 1885193, connection count: 1, memory usage: 51.49 MB, cpu usage: 0.00%, io read/write: (871332.24 MB/s, 19085.70 MB/s), open files: 1, listening ports: []
pid: 1885193, connection count: 1, memory usage: 51.49 MB, cpu usage: 0.00%, io read/write: (871332.24 MB/s, 19085.70 MB/s), open files: 1, listening ports: []
pid: 1302, connection count: 1, memory usage: 30.05 MB, cpu usage: 0.00%, io read/write: (796263.84 MB/s, 8186.78 MB/s), open files: 12, listening ports: []
pid: 1149118, connection count: 1, memory usage: 118.54 MB, cpu usage: 0.00%, io read/write: (4.80 MB/s, 889.46 MB/s), open files: 655, listening ports: [8888, 6666, 443, 80, 6668, 6667]

代码解析

1,引入所需的模块和库

代码语言:javascript复制
from bcc import BPF  # 用于编写和加载eBPF程序
import os  # 用于获取当前脚本的文件名
import sys  # 用于获取传入的参数
import time  # 用于休眠
import psutil  # 用于获取进程相关信息

这里引入了 BPF 模块,用于创建和加载 eBPF 程序,以及 ossystimepsutil 等标准库和第三方库。

2,定义 eBPF 程序

代码语言:javascript复制
program = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <bcc/proto.h>

struct key_t {
    u32 pid;
};
BPF_HASH(stats, struct key_t);

int probe(struct pt_regs *ctx, struct sock *sk) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    FILTER
    u64 *val, zero = 0;
    struct key_t key = {.pid = pid};
    val = stats.lookup_or_init(&key, &zero);
    (*val)  ;
    return 0;
}
"""

这段程序用 C 语言编写,可以通过 BPF 编译器编译成内核模块,再用 BPF 加载器加载到内核中运行。它的功能是在每个 TCP 连接建立时,统计连接数并保存在哈希表 stats 中。

程序使用 BPF_HASH 定义了一个名为 stats 的哈希表,键为 struct key_t 结构体,其中只包含一个 pid 字段。在 probe 函数中,使用 bpf_get_current_pid_tgid() 获取当前进程的 PID,然后通过 FILTER 宏来过滤不需要监控的进程。在这个例子中,使用 pid 参数来指定需要监控的进程,如果当前进程的 PID 不是指定的 PID,就直接返回。使用 stats.lookup_or_init() 函数来查找或创建哈希表中 pid 对应的值,然后自增。最后,返回 0。

3,定义过滤器

代码语言:javascript复制
filter = """
#define FILTER                                                           
    pid_t pid = bpf_get_current_pid_tgid() >> 32;                         
    if (FILTER_PID && pid != FILTER_PID)                                  
        return 0;                                                         
    if (FILTER_COMM && FILTER_COMM_STRCMP != 0)                           
        return 0;
"""

这是过滤器的代码,定义了两个宏:FILTER_PIDFILTER_COMMFILTER_PID 用于判断当前进程是否为指定的 PID,如果是,就返回 1,否则返回 0。FILTER_COMM 用于判断当前进程的名称是否与脚本的名称相同,如果相同,就返回 1,否则返回 0。这里使用 FILTER_PID 来过滤指定的进程。

4,创建 eBPF 程序

代码语言:javascript复制
bpf = BPF(text=program.replace('FILTER', '').replace('FILTER_PID', str(pid)).replace('FILTER_COMM', '1').replace('FILTER_COMM_STRCMP', 'strcmp(bpf_get_current_comm(), "'   os.path.basename(sys.argv[0])   '")'))

根据定义的eBPF程序和过滤器,通过BCC库创建eBPF程序对象bpf。在创建过程中,程序和过滤器会被编译为二进制代码,并在需要的地方被调用。

这里使用BPF类创建eBPF程序,传入的参数text是一个字符串类型,表示eBPF程序的代码。在这个代码中,将FILTERFILTER_PIDFILTER_COMMFILTER_COMM_STRCMP替换为对应的参数值,其中FILTER表示过滤条件,FILTER_PID表示过滤进程ID,FILTER_COMM表示是否过滤指定进程,FILTER_COMM_STRCMP表示需要过滤的指定进程名。

5,挂载eBPF程序

代码语言:javascript复制
bpf.attach_kprobe(event="tcp_v4_connect", fn_name="probe")

使用attach_kprobe函数将eBPF函数probe挂载到kprobe/tcp_v4_connect上,以便在每次网络连接建立时触发eBPF程序(即当tcp_v4_connect函数被调用时),会触发probe函数的执行。

6,获取当前进程句柄

代码语言:javascript复制
p = psutil.Process(os.getpid())
使用psutil模块获取当前进程的句柄。
定义输出函数:
def print_stats():
    #获取BPF程序中的hash table stats,用于存储每个进程的连接数
    stats = bpf["stats"]
    for k, v in stats.items():
        pid = k.pid
        if pid == 0:  # 如果进程id为0(即init进程),则跳过
            continue
        try:
            proc = psutil.Process(pid) #获取当前进程的句柄。
        except psutil.NoSuchProcess:
            continue
        if proc.parent() is None:  # 跳过孤儿进程
            continue
        if proc.status() == psutil.STATUS_ZOMBIE:  # 跳过僵尸进程
            continue
        #输出当前进程的连接数、内存使用量、CPU使用率、IO读写速度、打开文件数以及监听端口号等信息。
        print("pid: %d, connection count: %d, memory usage: %.2f MB, cpu usage: %.2f%%, io read/write: (%.2f MB/s, %.2f MB/s), open files: %d, listening ports: %s" % (
            pid,
            v.value,
            proc.memory_info().rss / 1024 / 1024,
            proc.cpu_percent(),
            proc.io_counters().read_bytes / 1024 / 1024,
            proc.io_counters().write_bytes / 1024 / 1024,
            len(proc.open_files()),
            [conn.laddr.port for conn in proc.connections() if conn.status == psutil.CONN_LISTEN]
        ))
    #清空hash table stats,以便下一轮统计。
    stats.clear()

这个函数将从eBPF程序中获取到的统计信息输出到控制台。具体实现过程如下:

代码首先通过bpf对象获取stats map,并遍历其中的键值对。键是一个pid,代表一个进程的唯一标识符;值是一个ctypes.c_uint64类型的对象,表示该进程当前的连接数量。需要注意的是,由于map的实现方式是哈希表,因此遍历结果是无序的。

接下来,代码通过psutil库获取该进程的详细信息和状态,包括进程的内存使用情况(rss属性)、CPU使用率(cpu_percent方法)、IO读写速度(io_counters方法)、打开文件数量(open_files方法)和监听端口号(connections方法)。这里需要注意的是,connections方法返回的是一个连接列表,其中每个连接都有一个status属性表示连接状态,psutil.CONN_LISTEN表示监听状态,而laddr.port则表示该连接的本地端口号。

最后,代码使用print函数将所有进程的信息以格式化的方式输出到控制台,并清空stats map以便下一次监控。需要注意的是,由于此代码段是作为一个函数,因此需要在程序其他地方调用才会执行。

eBPF在历史运维场景中的优化案例介绍

关于使用eBPF进行系统调用跟踪优化是一个很复杂而且深入内核的话题,这里只是使用一个很小的例子体验,现在已经有很多基于eBPF开的开源优秀项目,我们可以进行学习与运用。

eBPF可以用来做系统调用跟踪、网络优化、安全优化等方面的优化,通过对系统的优化,可以提高系统的性能、可靠性和安全性能。在实际应用中,eBPF已经被广泛应用于云计算、大数据、容器化等领域。

提高网络吞吐量

在网络流量高峰期,如果系统的网络带宽无法满足业务需求,就会导致网络拥堵、请求超时等问题。eBPF可以用来优化系统的网络性能,提高网络吞吐量。例如,Facebook就使用eBPF实现了一个网络优化工具——Katran,可以在高流量情况下提高系统的网络吞吐量。

提高容器性能

在容器化场景下,如果容器的网络、存储等资源不合理分配,就会导致容器的性能问题。eBPF可以用来优化容器的性能,例如提高容器网络性能、优化容器存储性能等。例如,Red Hat就使用eBPF实现了一个容器网络优化工具——Cilium,可以提高容器的网络性能和安全性。

实现入侵检测

在安全性方面,eBPF可以用来实现入侵检测、访问控制等安全机制,保护系统的安全性。例如,Google就使用eBPF实现了一个入侵检测工具——gVisor,可以防止容器和虚拟机中的攻击,提高系统的安全性。

实现系统调用跟踪

eBPF可以用来实现系统调用跟踪,通过监控系统调用的使用情况,可以了解系统的运行情况,及时发现问题,优化系统性能。例如,Netflix就使用eBPF实现了一个系统调用跟踪工具——bcc,可以监控系统的运行状态,提高系统的可靠性和性能。

实现内核性能分析

eBPF可以用来实现内核性能分析,通过分析内核的运行情况,可以找到内核性能瓶颈,进行优化,提高系统性能。例如,Google就使用eBPF实现了一个内核性能分析工具——BPFTrace,可以监控内核的运行状态,找到性能瓶颈,优化系统性能。

实现安全审计

eBPF可以用来实现安全审计,通过监控系统的运行情况,记录系统的操作行为,可以及时发现异常操作,保护系统的安全性。例如,Uber就使用eBPF实现了一个安全审计工具——Horizon,可以监控容器和虚拟机的操作行为,保护系统的安全性。

0 人点赞