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 程序,以及 os
、sys
、time
和 psutil
等标准库和第三方库。
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_PID
和 FILTER_COMM
。FILTER_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程序的代码。在这个代码中,将FILTER
、FILTER_PID
、FILTER_COMM
和FILTER_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,可以监控容器和虚拟机的操作行为,保护系统的安全性。